feat(ui): scope, freshness and jurist-review badges + live empty-state
Adds three dark-first chip badges in the TierBadge pattern: - Jurisdiction (Berlin/DE/EU/International) — derived from the ELI, surfaced on the norms list, detail header and heatmap chips/hover. - Freshness (aktuell/geaendert/ungeprueft) — flags norms amended upstream (standDate + sourceSha256); renders only when stale. - Jurist review — marks evaluations a trusted jurist has confirmed (optional; the figure stands on its own). Models gain Jurisdiction/Freshness enums, Norm.freshness/supersededNote and Evaluation.reviewedBy/reviewedAt; HubRepository parses them from the flow bag (ELI fallback). HeatmapPage shows a real empty-state (hub-down vs no-evaluations) instead of a blank plot, and connect() no longer throws on a failed probe. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
9dfa418e92
commit
aabff2b140
11 changed files with 604 additions and 26 deletions
|
|
@ -5,6 +5,7 @@ import '../data/repository.dart';
|
|||
import '../theme/reclaim_tokens.dart';
|
||||
import '../widgets/mode_banner.dart';
|
||||
import '../widgets/heatmap_grid.dart';
|
||||
import '../widgets/jurisdiction_badge.dart';
|
||||
import '../widgets/reclaim_card.dart';
|
||||
import '../widgets/tier_badge.dart';
|
||||
import 'norm_detail_page.dart';
|
||||
|
|
@ -59,26 +60,29 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||
),
|
||||
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,
|
||||
),
|
||||
child: items.isEmpty
|
||||
? _EmptyHubState(repository: widget.repository)
|
||||
: 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(),
|
||||
if (items.isNotEmpty) ...[
|
||||
const SizedBox(height: ReclaimSpace.md),
|
||||
const _LegendCard(),
|
||||
const SizedBox(height: ReclaimSpace.lg),
|
||||
Wrap(
|
||||
spacing: ReclaimSpace.md,
|
||||
|
|
@ -104,6 +108,11 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
JurisdictionBadge(
|
||||
jurisdiction: e.norm.jurisdiction,
|
||||
compact: true),
|
||||
const SizedBox(
|
||||
width: ReclaimSpace.xs),
|
||||
TierBadge(
|
||||
tier: e.tierLowest, compact: true),
|
||||
const SizedBox(
|
||||
|
|
@ -131,6 +140,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
@ -196,6 +206,8 @@ class _LegendCardState extends State<_LegendCard> {
|
|||
symbol: '●', text: 'Betroffenheit'),
|
||||
_MiniLabel(
|
||||
symbol: '◐', text: 'Evidenz-Stufe'),
|
||||
_MiniLabel(
|
||||
symbol: '⌖', text: 'Geltungsbereich'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -272,6 +284,21 @@ class _LegendCardState extends State<_LegendCard> {
|
|||
'SKM-Zahl T4. Petrol → warm = stark → '
|
||||
'schwach.',
|
||||
),
|
||||
SizedBox(height: ReclaimSpace.sm),
|
||||
_LegendRow(
|
||||
symbol: '⌖',
|
||||
axis: 'Chip-Symbol',
|
||||
label: 'Geltungsbereich',
|
||||
description:
|
||||
'Jede Norm trägt ein Scope-Chip — '
|
||||
'BE Berlin (Landesrecht, Senat/Bezirk), '
|
||||
'DE Deutschland (Bundesrecht, NKR/'
|
||||
'Bundestag), EU (Reform-Hebel in Brüssel), '
|
||||
'INT International (WTO/OECD/UN). Es zeigt '
|
||||
'an, auf welcher Ebene der Reform-Hebel '
|
||||
'ansetzt: eine EU-Norm lässt sich nicht '
|
||||
'durch nationale Streichung entlasten.',
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
@ -285,6 +312,69 @@ class _LegendCardState extends State<_LegendCard> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Shown in place of the heatmap when the repository returns zero
|
||||
/// evaluations — almost always live-hub mode against a hub that
|
||||
/// has no Recl∆Im `flow.completed` events yet (or is unreachable).
|
||||
/// Tells the reader exactly how to get data on screen instead of
|
||||
/// leaving a blank plot.
|
||||
class _EmptyHubState extends StatelessWidget {
|
||||
const _EmptyHubState({required this.repository});
|
||||
|
||||
final EvaluationRepository repository;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Theme.of(context).textTheme;
|
||||
final down = repository.mode == 'hub-down';
|
||||
return ReclaimCard(
|
||||
accent: true,
|
||||
expand: true,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 460),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
down ? Icons.cloud_off_outlined : Icons.inbox_outlined,
|
||||
size: 40,
|
||||
color: ReclaimColors.mute,
|
||||
),
|
||||
const SizedBox(height: ReclaimSpace.md),
|
||||
Text(
|
||||
down
|
||||
? 'Hub nicht erreichbar'
|
||||
: 'Noch keine Auswertungen im Hub',
|
||||
style: t.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: ReclaimSpace.sm),
|
||||
Text(
|
||||
down
|
||||
? 'Verbunden mit ${repository.hubLabel}. Läuft '
|
||||
'`chain serve`? Sonst im Hub-Reiter den '
|
||||
'Demo-Modus aktivieren — dann zeigt die '
|
||||
'Heatmap das Fixture-Korpus.'
|
||||
: 'Der Hub liefert keine flow.completed-Events. '
|
||||
'Einen Recl∆Im-Flow laufen lassen, z. B.\n'
|
||||
' chain run flows/durchstich-from-file.yaml '
|
||||
'--input content=@norm.xml --input '
|
||||
'cohort_id=berlin-kmu\n'
|
||||
'und im Hub/Studio freigeben — oder im '
|
||||
'Hub-Reiter den Demo-Modus aktivieren.',
|
||||
style: t.bodyLarge?.copyWith(
|
||||
color: ReclaimColors.mute,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiniLabel extends StatelessWidget {
|
||||
const _MiniLabel({required this.symbol, required this.text});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import '../data/repository.dart';
|
|||
import '../theme/reclaim_tokens.dart';
|
||||
import '../widgets/mode_banner.dart';
|
||||
import '../widgets/evidence_sidebar.dart';
|
||||
import '../widgets/freshness_badge.dart';
|
||||
import '../widgets/jurisdiction_badge.dart';
|
||||
import '../widgets/norm_text_card.dart';
|
||||
import '../widgets/reclaim_card.dart';
|
||||
import '../widgets/review_badge.dart';
|
||||
import '../widgets/tier_badge.dart';
|
||||
|
||||
class NormDetailPage extends StatelessWidget {
|
||||
|
|
@ -63,7 +66,27 @@ class NormDetailPage extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
TierBadge(tier: e.tierLowest),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.end,
|
||||
children: [
|
||||
JurisdictionBadge(
|
||||
jurisdiction: e.norm.jurisdiction),
|
||||
const SizedBox(height: ReclaimSpace.sm),
|
||||
TierBadge(tier: e.tierLowest),
|
||||
if (e.norm.freshness.needsBadge) ...[
|
||||
const SizedBox(
|
||||
height: ReclaimSpace.sm),
|
||||
FreshnessBadge(
|
||||
freshness: e.norm.freshness,
|
||||
note: e.norm.supersededNote,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: ReclaimSpace.sm),
|
||||
ReviewBadge(
|
||||
evaluation: e, showUnreviewed: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: ReclaimSpace.lg),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import '../data/models.dart';
|
|||
import '../data/repository.dart';
|
||||
import '../theme/reclaim_tokens.dart';
|
||||
import '../widgets/mode_banner.dart';
|
||||
import '../widgets/freshness_badge.dart';
|
||||
import '../widgets/jurisdiction_badge.dart';
|
||||
import '../widgets/reclaim_card.dart';
|
||||
import '../widgets/review_badge.dart';
|
||||
import '../widgets/tier_badge.dart';
|
||||
import 'norm_detail_page.dart';
|
||||
|
||||
|
|
@ -108,7 +111,20 @@ class _NormRow extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: ReclaimSpace.sm),
|
||||
TierBadge(tier: evaluation.tierLowest),
|
||||
Wrap(
|
||||
spacing: ReclaimSpace.sm,
|
||||
runSpacing: ReclaimSpace.xs,
|
||||
children: [
|
||||
JurisdictionBadge(
|
||||
jurisdiction: evaluation.norm.jurisdiction),
|
||||
TierBadge(tier: evaluation.tierLowest),
|
||||
FreshnessBadge(
|
||||
freshness: evaluation.norm.freshness,
|
||||
note: evaluation.norm.supersededNote,
|
||||
),
|
||||
ReviewBadge(evaluation: evaluation),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue