import 'package:flutter/material.dart'; import '../data/models.dart'; import '../data/repository.dart'; import '../theme/reclaim_tokens.dart'; import '../widgets/demo_banner.dart'; import '../widgets/heatmap_grid.dart'; import '../widgets/reclaim_card.dart'; import '../widgets/tier_badge.dart'; import 'norm_detail_page.dart'; class HeatmapPage extends StatefulWidget { const HeatmapPage({super.key, required this.repository}); final EvaluationRepository repository; @override State createState() => _HeatmapPageState(); } class _HeatmapPageState extends State { late Future> _future; @override void initState() { super.initState(); _future = widget.repository.list(); } @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return Column( children: [ const DemoBanner(), Expanded( child: Padding( padding: const EdgeInsets.all(ReclaimSpace.xl), child: FutureBuilder>( future: _future, builder: (context, snap) { if (!snap.hasData) { return const Center( child: CircularProgressIndicator()); } final items = snap.data!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Heatmap', style: t.displaySmall), const SizedBox(height: ReclaimSpace.xs), Text( 'Schaden × Nutzen × Betroffenheit pro Norm. ' 'Klick auf einen Punkt öffnet das Detail mit ' 'Pflichten, Quellen und Gesetzestext.', style: t.bodyLarge?.copyWith( color: ReclaimColors.mute, ), ), const SizedBox(height: ReclaimSpace.lg), Expanded( child: ReclaimCard( accent: true, expand: true, child: HeatmapGrid( evaluations: items, onTap: (e) { Navigator.of(context).push( MaterialPageRoute( builder: (_) => NormDetailPage( repository: widget.repository, evaluation: e, ), ), ); }, ), ), ), const SizedBox(height: ReclaimSpace.md), const _LegendCard(), const SizedBox(height: ReclaimSpace.lg), Wrap( spacing: ReclaimSpace.md, runSpacing: ReclaimSpace.sm, children: [ for (final e in items) InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => NormDetailPage( repository: widget.repository, evaluation: e, ), ), ); }, child: ReclaimCard( padding: const EdgeInsets.symmetric( horizontal: ReclaimSpace.md, vertical: ReclaimSpace.sm, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ TierBadge( tier: e.tierLowest, compact: true), const SizedBox( width: ReclaimSpace.sm), Text( '${e.norm.jurabk} ${e.norm.paragraph}', style: t.labelLarge, ), ], ), ), ), ], ), ], ); }, ), ), ), ], ); } } /// Axis + circle legend below the heatmap. Spells out what is /// plotted and how — the four-line top description deliberately /// stays short and high-level, this card carries the precise /// reading instructions a reviewer needs to defend the plot /// against "what does that even mean?" class _LegendCard extends StatelessWidget { const _LegendCard(); @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Legende', style: t.headlineSmall), const SizedBox(height: ReclaimSpace.md), _LegendRow( symbol: '↔', axis: 'Abszisse (X)', label: 'Schaden', description: 'Bürokratiekosten pro Jahr in Euro, logarithmisch ' 'skaliert (10⁴ = 10 k, 10⁶ = 1 Mio, 10⁹ = 1 Mrd). ' 'Berechnet nach Standardkostenmodell ' 'P × F × T × h (Studie §3.1).', ), const SizedBox(height: ReclaimSpace.sm), _LegendRow( symbol: '↕', axis: 'Ordinate (Y)', label: 'Nutzen', description: 'Komposit-Score von 0–5, Mittelwert aus den sieben ' 'Dimensionen Schutz · Markt · Rechtssicherheit · ' 'EU-Binnenmarkt · Sicherheit · Umwelt · Einnahmen ' '(Studie §3.2). Jede Dimension einzeln ' 'quellen-begründet.', ), const SizedBox(height: ReclaimSpace.sm), _LegendRow( symbol: '●', axis: 'Punktgröße', label: 'Betroffenheit', description: 'Anzahl der Adressaten (KMU, Bürger, Behörden), ' 'wurzel-skaliert. Mehr Adressaten → größerer Kreis. ' 'Quellen: DESTATIS Unternehmensregister, ' 'IHK-/Handwerkskammer-Mitgliederzahlen.', ), const SizedBox(height: ReclaimSpace.sm), _LegendRow( symbol: '◐', axis: 'Farb-Kante', label: 'Evidenz-Stufe', description: 'Jeder Wert trägt eine von vier Stufen — ' 'T1 amtlich (NKR, DESTATIS, ifo, ZEW), ' 'T2 Verband (DIHK, IHK, ZDH, BDI), ' 'T3 eigene Erhebung, ' 'T4 qualitatives Signal. ' 'Eine Zahl bekommt die Stufe ihrer schwächsten ' 'Eingangs-Variable — wenn h nur geschätzt ist (T4), ' 'ist die ganze SKM-Zahl T4. Petrol → warm = stark → ' 'schwach.', ), ], ), ); } } class _LegendRow extends StatelessWidget { const _LegendRow({ required this.symbol, required this.axis, required this.label, required this.description, }); final String symbol; final String axis; final String label; final String description; @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 28, child: Text( symbol, style: t.titleMedium?.copyWith( color: ReclaimColors.signal, fontSize: 18, ), ), ), SizedBox( width: 140, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( axis, style: t.labelSmall?.copyWith( color: ReclaimColors.mute, letterSpacing: 0.4, ), ), Text( label, style: t.titleMedium, ), ], ), ), const SizedBox(width: ReclaimSpace.md), Expanded( child: Padding( padding: const EdgeInsets.only(top: 2), child: Text( description, style: t.bodyLarge?.copyWith( color: ReclaimColors.mute, height: 1.5, ), ), ), ), ], ); } }