A new 'Freigaben' nav destination lists the hub's pending
system.approval tasks (listApprovals), shows each prompt + norm
payload, and lets a jurist confirm ('Korrekt — freigeben') or reject
with a reason via the SDK (approve/reject). The reviewer's name is
carried into the hub's hash-chained audit log. Review is optional and
live-only; the trusted-jurist registry + login is enforced hub-side
(chain handoff). Verified against the live hub: lists 3 pending.
Signed-off-by: flemming-it <sf@flemming.it>
111 lines
3.7 KiB
Dart
111 lines
3.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../data/repository.dart';
|
|
import '../theme/reclaim_tokens.dart';
|
|
import '../widgets/brand_wordmark.dart';
|
|
import '../widgets/delta_mark.dart';
|
|
import 'about_page.dart';
|
|
import 'heatmap_page.dart';
|
|
import 'hub_status_page.dart';
|
|
import 'jurist_review_page.dart';
|
|
import 'legal_page.dart';
|
|
import 'norms_list_page.dart';
|
|
|
|
/// App shell with a NavigationRail. Each rail destination is one
|
|
/// page; pages do their own scrolling.
|
|
class ShellPage extends StatefulWidget {
|
|
const ShellPage({super.key, required this.repository});
|
|
|
|
final EvaluationRepository repository;
|
|
|
|
@override
|
|
State<ShellPage> createState() => _ShellPageState();
|
|
}
|
|
|
|
class _ShellPageState extends State<ShellPage> {
|
|
int _index = 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final t = Theme.of(context).textTheme;
|
|
final pages = <Widget>[
|
|
HeatmapPage(repository: widget.repository),
|
|
NormsListPage(repository: widget.repository),
|
|
JuristReviewPage(repository: widget.repository),
|
|
HubStatusPage(repository: widget.repository),
|
|
const AboutPage(),
|
|
const LegalPage(),
|
|
];
|
|
|
|
return Scaffold(
|
|
body: Row(
|
|
children: [
|
|
NavigationRail(
|
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
extended: MediaQuery.of(context).size.width > 1100,
|
|
selectedIndex: _index,
|
|
onDestinationSelected: (i) => setState(() => _index = i),
|
|
leading: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: ReclaimSpace.lg,
|
|
horizontal: ReclaimSpace.md,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Static in the persistent nav rail: a 60fps pulse
|
|
// here repaints every scroll frame and stutters web
|
|
// scrolling. The breathing mark stays on the
|
|
// landing page (which isn't scrolled).
|
|
const DeltaMark(size: 36, animated: false),
|
|
const SizedBox(height: ReclaimSpace.sm),
|
|
const BrandWordmark(size: 18),
|
|
Text(
|
|
'F∆I',
|
|
style: t.labelSmall?.copyWith(
|
|
letterSpacing: 2,
|
|
color: ReclaimColors.mute,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
destinations: const [
|
|
NavigationRailDestination(
|
|
icon: Icon(Icons.grid_view_outlined),
|
|
selectedIcon: Icon(Icons.grid_view),
|
|
label: Text('Heatmap'),
|
|
),
|
|
NavigationRailDestination(
|
|
icon: Icon(Icons.list_alt_outlined),
|
|
selectedIcon: Icon(Icons.list_alt),
|
|
label: Text('Normen'),
|
|
),
|
|
NavigationRailDestination(
|
|
icon: Icon(Icons.fact_check_outlined),
|
|
selectedIcon: Icon(Icons.fact_check),
|
|
label: Text('Freigaben'),
|
|
),
|
|
NavigationRailDestination(
|
|
icon: Icon(Icons.hub_outlined),
|
|
selectedIcon: Icon(Icons.hub),
|
|
label: Text('Hub'),
|
|
),
|
|
NavigationRailDestination(
|
|
icon: Icon(Icons.info_outline),
|
|
selectedIcon: Icon(Icons.info),
|
|
label: Text('Methodik'),
|
|
),
|
|
NavigationRailDestination(
|
|
icon: Icon(Icons.gavel_outlined),
|
|
selectedIcon: Icon(Icons.gavel),
|
|
label: Text('Recht'),
|
|
),
|
|
],
|
|
),
|
|
const VerticalDivider(width: 1),
|
|
Expanded(child: pages[_index]),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|