reclaim/app/lib/pages/heatmap_page.dart
flemming-it 1ad523a91a fix(ui): make T1–T4 readable + lift Recl∆Im into the nav rail
Three small frictions a first-time reader hit at the same time,
fixed in one pass:

  1. The TierBadge said 'T1' without context. Default form is
     now 'T1 · amtlich' / 'T2 · Verband' / 'T3 · Erhebung' /
     'T4 · Signal'. Compact form (heatmap chip wrap) keeps the
     bare code. Tooltip spells out the long form + cites the
     Regel der niedrigsten Stufe (Studie §6.7) so the meaning
     of the cap is at hand on hover.

  2. The Legende's tier row was the abstract one of the four —
     it now inlines all four stage names (amtlich / Verband /
     Erhebung / Signal) and explains why a single T4 component
     drags a whole figure to T4. No more 'what is Tier?' on
     screen.

  3. The DemoBanner repeated 'Tier T4' without explaining what
     T4 is. Now: 'qualitatives Signal — bis NKR/Verbands-
     Pipeline + Juristen-Approval laufen'. The cause-and-cure
     is visible at a glance.

  4. The NavigationRail leading column showed only 'F∆I' as
     brand mark. Now shows 'Recl∆Im' prominently (petrol-signal,
     bold) with 'F∆I' as a small muted vendor tag below.
     Product first, vendor second.

EvidenceTierLabel extension grows a new 'headline' getter for
the one-word default ('amtlich'/'Verband'/…), and 'description'
gets longer, source-naming text (NKR / DESTATIS / DIHK / IHK /
ZDH / BDI). Code paths that used the old short-only badge keep
working because 'short' still returns 'T1'.

flutter pub get refreshed against chain_client_sdk 0.18, which
the fai_chain agent confirmed already carries the gRPC-Web
channel — no SDK bump needed for the web-hub-mode wire.

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-18 14:44:14 +02:00

268 lines
8.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<HeatmapPage> createState() => _HeatmapPageState();
}
class _HeatmapPageState extends State<HeatmapPage> {
late Future<List<Evaluation>> _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<List<Evaluation>>(
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 05, 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,
),
),
),
),
],
);
}
}