Compare commits
2 commits
94f4473686
...
ae9fc45177
| Author | SHA1 | Date | |
|---|---|---|---|
| ae9fc45177 | |||
| c6400302dd |
7 changed files with 177 additions and 28 deletions
|
|
@ -90,6 +90,12 @@ class Fixtures {
|
||||||
'T (Tarif) T1 — DESTATIS Verdiensterhebung kaufm. Tätigkeit ~30 €/h; '
|
'T (Tarif) T1 — DESTATIS Verdiensterhebung kaufm. Tätigkeit ~30 €/h; '
|
||||||
'h (Zeitaufwand) T3 — eigene Schätzung ~30 min, NKR-Validierung ausstehend. '
|
'h (Zeitaufwand) T3 — eigene Schätzung ~30 min, NKR-Validierung ausstehend. '
|
||||||
'Regel der niedrigsten Stufe: Gesamt → T3 (in der Demo konservativ als T4 markiert).',
|
'Regel der niedrigsten Stufe: Gesamt → T3 (in der Demo konservativ als T4 markiert).',
|
||||||
|
componentSummary: const [
|
||||||
|
'P · T1 — DESTATIS Gewerbeanzeigen',
|
||||||
|
'F · T1 — anlassbezogen, je Vorgang',
|
||||||
|
'T · T1 — DESTATIS Verdiensterhebung',
|
||||||
|
'h · T3 — eigene Schätzung ~30 min',
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final _kassenSichV = Evaluation(
|
static final _kassenSichV = Evaluation(
|
||||||
|
|
@ -177,6 +183,12 @@ class Fixtures {
|
||||||
'Laufende Folgekosten (TSE-Ausfallmeldungen, Hardware-Erneuerung): T2 '
|
'Laufende Folgekosten (TSE-Ausfallmeldungen, Hardware-Erneuerung): T2 '
|
||||||
'aus DIHK-Bürokratie-Index + DEHOGA-Gastronomie-Stichprobe. '
|
'aus DIHK-Bürokratie-Index + DEHOGA-Gastronomie-Stichprobe. '
|
||||||
'Hoher Frust-Score wegen Pluralität TSE-Hersteller × Finanzamt-Vollzug.',
|
'Hoher Frust-Score wegen Pluralität TSE-Hersteller × Finanzamt-Vollzug.',
|
||||||
|
componentSummary: const [
|
||||||
|
'P · T1 — Kassen-pflichtige Betriebe',
|
||||||
|
'F · T1 — kontinuierlich + Ausfälle',
|
||||||
|
'T · T2 — DIHK Bürokratie-Index',
|
||||||
|
'h · T2 — DEHOGA-Stichprobe',
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final _dsgvoArt30 = Evaluation(
|
static final _dsgvoArt30 = Evaluation(
|
||||||
|
|
@ -272,6 +284,13 @@ class Fixtures {
|
||||||
'pro Unternehmen liegt im Streit zwischen BfDI-Position (gering, weil '
|
'pro Unternehmen liegt im Streit zwischen BfDI-Position (gering, weil '
|
||||||
'<250-MA-Ausnahme) und DIHK-Position (hoch, weil 5-Abs.-Ausnahme '
|
'<250-MA-Ausnahme) und DIHK-Position (hoch, weil 5-Abs.-Ausnahme '
|
||||||
'praktisch nicht greift). Tier T2 für Magnitude.',
|
'praktisch nicht greift). Tier T2 für Magnitude.',
|
||||||
|
componentSummary: const [
|
||||||
|
'P · T1 — DESTATIS Unternehmensregister',
|
||||||
|
'F · T1 — jährlich überprüfen',
|
||||||
|
'T · T2 — DIHK-Position',
|
||||||
|
'h · T2 — Streit BfDI ↔ DIHK',
|
||||||
|
'EU-determiniert · Reform-Hebel Brüssel',
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final _berlinBauO60 = Evaluation(
|
static final _berlinBauO60 = Evaluation(
|
||||||
|
|
@ -351,6 +370,13 @@ class Fixtures {
|
||||||
'→ Genehmigung sind im Land getrennt geregelt). Höchster Frust-Score '
|
'→ Genehmigung sind im Land getrennt geregelt). Höchster Frust-Score '
|
||||||
'aus Verweistiefe + Behörden-Pluralität (Bezirksamt × SenStadt × '
|
'aus Verweistiefe + Behörden-Pluralität (Bezirksamt × SenStadt × '
|
||||||
'Beteiligungsverfahren).',
|
'Beteiligungsverfahren).',
|
||||||
|
componentSummary: const [
|
||||||
|
'P · T2 — Handwerkskammer Berlin',
|
||||||
|
'F · T1 — pro Bauvorhaben',
|
||||||
|
'T · T2 — Bauwirtschaft Berlin',
|
||||||
|
'h · T3 — Verfahren mehrstufig',
|
||||||
|
'Frust · T2 — Bezirk × Senat × Beteiligung',
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
static final _miLoG17 = Evaluation(
|
static final _miLoG17 = Evaluation(
|
||||||
|
|
@ -428,5 +454,12 @@ class Fixtures {
|
||||||
'dokumentierte den initial-Erfüllungsaufwand; aktuelle Zahlen '
|
'dokumentierte den initial-Erfüllungsaufwand; aktuelle Zahlen '
|
||||||
'aus ZDH Bürokratiekosten-Handwerk. Bürokratieentlastungs-'
|
'aus ZDH Bürokratiekosten-Handwerk. Bürokratieentlastungs-'
|
||||||
'gesetz IV adressierte den § 17 — Reform-Bewegung ist im Lauf.',
|
'gesetz IV adressierte den § 17 — Reform-Bewegung ist im Lauf.',
|
||||||
|
componentSummary: const [
|
||||||
|
'P · T1 — DESTATIS MiLoG-pflichtig',
|
||||||
|
'F · T1 — arbeitstäglich',
|
||||||
|
'T · T1 — DESTATIS Tariflohn',
|
||||||
|
'h · T1 — NKR-Stellungnahme 2014',
|
||||||
|
'Reform · BEG IV in Lauf',
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ class Evaluation {
|
||||||
required this.sources,
|
required this.sources,
|
||||||
required this.auditEventId,
|
required this.auditEventId,
|
||||||
this.notes,
|
this.notes,
|
||||||
|
this.componentSummary = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
final Norm norm;
|
final Norm norm;
|
||||||
|
|
@ -172,4 +173,10 @@ class Evaluation {
|
||||||
final String auditEventId;
|
final String auditEventId;
|
||||||
|
|
||||||
final String? notes;
|
final String? notes;
|
||||||
|
|
||||||
|
/// Compact lines for at-a-glance reading on list views.
|
||||||
|
/// One line per SKM-Komponente (P / F / T / h) plus optional
|
||||||
|
/// extras — each carries its tier-tag inline. Empty list ⇒
|
||||||
|
/// fall back to the notes field for the list view.
|
||||||
|
final List<String> componentSummary;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||||
import '../data/repository.dart';
|
import '../data/repository.dart';
|
||||||
import '../theme/reclaim_theme.dart';
|
import '../theme/reclaim_theme.dart';
|
||||||
import '../theme/reclaim_tokens.dart';
|
import '../theme/reclaim_tokens.dart';
|
||||||
|
import '../widgets/brand_wordmark.dart';
|
||||||
import '../widgets/delta_mark.dart';
|
import '../widgets/delta_mark.dart';
|
||||||
import 'shell_page.dart';
|
import 'shell_page.dart';
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ class LandingPage extends StatelessWidget {
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 720),
|
constraints: const BoxConstraints(maxWidth: 820),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: ReclaimSpace.xxl,
|
horizontal: ReclaimSpace.xxl,
|
||||||
|
|
@ -30,9 +31,18 @@ class LandingPage extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const DeltaMark(size: 96),
|
const Row(
|
||||||
const SizedBox(height: ReclaimSpace.xxl),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
Text('Recl∆Im', style: t.displaySmall),
|
children: [
|
||||||
|
DeltaMark(size: 96),
|
||||||
|
SizedBox(width: ReclaimSpace.xl),
|
||||||
|
BrandWordmark(
|
||||||
|
size: 56,
|
||||||
|
letterSpacing: -1.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: ReclaimSpace.xl),
|
||||||
const SizedBox(height: ReclaimSpace.sm),
|
const SizedBox(height: ReclaimSpace.sm),
|
||||||
Text(
|
Text(
|
||||||
'Pflicht-basierte Wirkungsanalyse von Rechtsnormen — '
|
'Pflicht-basierte Wirkungsanalyse von Rechtsnormen — '
|
||||||
|
|
|
||||||
|
|
@ -107,31 +107,42 @@ class _NormRow extends StatelessWidget {
|
||||||
color: ReclaimColors.mute,
|
color: ReclaimColors.mute,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: ReclaimSpace.sm),
|
||||||
|
TierBadge(tier: evaluation.tierLowest),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: ReclaimSpace.lg),
|
const SizedBox(width: ReclaimSpace.lg),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _MetricColumn(
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_MetricColumn(
|
||||||
label: 'Schaden / Jahr',
|
label: 'Schaden / Jahr',
|
||||||
value: _eur(evaluation.skmEurPerYear),
|
value: _eur(evaluation.skmEurPerYear),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: ReclaimSpace.sm),
|
||||||
Expanded(
|
_MetricColumn(
|
||||||
child: _MetricColumn(
|
|
||||||
label: 'Nutzen',
|
label: 'Nutzen',
|
||||||
value:
|
value:
|
||||||
'${evaluation.benefitScore.toStringAsFixed(1)} / 5',
|
'${evaluation.benefitScore.toStringAsFixed(1)} / 5',
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: ReclaimSpace.sm),
|
||||||
Expanded(
|
_MetricColumn(
|
||||||
child: _MetricColumn(
|
|
||||||
label: 'Betroffene',
|
label: 'Betroffene',
|
||||||
value: _kmu(evaluation.affectedCount),
|
value: _kmu(evaluation.affectedCount),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: ReclaimSpace.lg),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: _ComponentSummary(
|
||||||
|
summary: evaluation.componentSummary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: ReclaimSpace.md),
|
|
||||||
TierBadge(tier: evaluation.tierLowest),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -152,6 +163,44 @@ class _NormRow extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ComponentSummary extends StatelessWidget {
|
||||||
|
const _ComponentSummary({required this.summary});
|
||||||
|
|
||||||
|
final List<String> summary;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = Theme.of(context).textTheme;
|
||||||
|
if (summary.isEmpty) {
|
||||||
|
return Text(
|
||||||
|
'—',
|
||||||
|
style: t.labelSmall?.copyWith(color: ReclaimColors.mute),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Komponenten',
|
||||||
|
style: t.labelSmall?.copyWith(color: ReclaimColors.mute),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
for (final line in summary)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
line,
|
||||||
|
style: t.bodyLarge?.copyWith(
|
||||||
|
height: 1.35,
|
||||||
|
fontSize: 12.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _MetricColumn extends StatelessWidget {
|
class _MetricColumn extends StatelessWidget {
|
||||||
const _MetricColumn({required this.label, required this.value});
|
const _MetricColumn({required this.label, required this.value});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../data/repository.dart';
|
import '../data/repository.dart';
|
||||||
import '../theme/reclaim_tokens.dart';
|
import '../theme/reclaim_tokens.dart';
|
||||||
|
import '../widgets/brand_wordmark.dart';
|
||||||
import '../widgets/delta_mark.dart';
|
import '../widgets/delta_mark.dart';
|
||||||
import 'about_page.dart';
|
import 'about_page.dart';
|
||||||
import 'heatmap_page.dart';
|
import 'heatmap_page.dart';
|
||||||
|
|
@ -51,14 +52,7 @@ class _ShellPageState extends State<ShellPage> {
|
||||||
children: [
|
children: [
|
||||||
const DeltaMark(size: 36),
|
const DeltaMark(size: 36),
|
||||||
const SizedBox(height: ReclaimSpace.sm),
|
const SizedBox(height: ReclaimSpace.sm),
|
||||||
Text(
|
const BrandWordmark(size: 18),
|
||||||
'Recl∆Im',
|
|
||||||
style: t.labelLarge?.copyWith(
|
|
||||||
letterSpacing: 1.5,
|
|
||||||
color: ReclaimColors.signal,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
'F∆I',
|
'F∆I',
|
||||||
style: t.labelSmall?.copyWith(
|
style: t.labelSmall?.copyWith(
|
||||||
|
|
|
||||||
55
app/lib/widgets/brand_wordmark.dart
Normal file
55
app/lib/widgets/brand_wordmark.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../theme/reclaim_tokens.dart';
|
||||||
|
|
||||||
|
/// "Recl∆Im" wordmark with the ∆I glyphs petrol-coloured — the
|
||||||
|
/// brand rule of thumb in the fai_web universe: wherever the name
|
||||||
|
/// stands as a brand, the ∆I tinted petrol carries the F∆I-family
|
||||||
|
/// identity. As plain text it would all share the surrounding ink
|
||||||
|
/// colour.
|
||||||
|
///
|
||||||
|
/// Pass [size] for the base font size; the ∆I scales relative.
|
||||||
|
class BrandWordmark extends StatelessWidget {
|
||||||
|
const BrandWordmark({
|
||||||
|
super.key,
|
||||||
|
this.size = 24,
|
||||||
|
this.fontWeight = FontWeight.w600,
|
||||||
|
this.color,
|
||||||
|
this.letterSpacing = -0.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
final FontWeight fontWeight;
|
||||||
|
|
||||||
|
/// Color of the non-∆I letters. Defaults to the surface's
|
||||||
|
/// onSurface so the wordmark reads against whichever ink/paper
|
||||||
|
/// backdrop it sits on.
|
||||||
|
final Color? color;
|
||||||
|
final double letterSpacing;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final base = color ?? Theme.of(context).colorScheme.onSurface;
|
||||||
|
final style = TextStyle(
|
||||||
|
fontSize: size,
|
||||||
|
fontWeight: fontWeight,
|
||||||
|
fontFamily: ReclaimTypography.display,
|
||||||
|
color: base,
|
||||||
|
letterSpacing: letterSpacing,
|
||||||
|
height: 1.0,
|
||||||
|
);
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: style,
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: 'Recl'),
|
||||||
|
TextSpan(
|
||||||
|
text: '∆I',
|
||||||
|
style: style.copyWith(color: ReclaimColors.signal),
|
||||||
|
),
|
||||||
|
const TextSpan(text: 'm'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:reclaim_app/main.dart';
|
import 'package:reclaim_app/main.dart';
|
||||||
|
import 'package:reclaim_app/widgets/brand_wordmark.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Generous surface so NavigationRail + Heatmap fit without
|
// Generous surface so NavigationRail + Heatmap fit without
|
||||||
|
|
@ -20,7 +21,7 @@ void main() {
|
||||||
await tester.binding.setSurfaceSize(surfaceSize);
|
await tester.binding.setSurfaceSize(surfaceSize);
|
||||||
await tester.pumpWidget(const ReclaimApp());
|
await tester.pumpWidget(const ReclaimApp());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.text('Recl∆Im'), findsOneWidget);
|
expect(find.byType(BrandWordmark), findsWidgets);
|
||||||
expect(
|
expect(
|
||||||
find.widgetWithText(FilledButton, 'App öffnen (Demo-Modus)'),
|
find.widgetWithText(FilledButton, 'App öffnen (Demo-Modus)'),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue