feat(ui): heatmap view modes (all / jurist-reviewed / marked)
A segmented toggle over the heatmap: 'Alle' shows every evaluation, 'Geprüft (N)' filters to jurist-confirmed ones, 'Markiert' shows all with a confirm-green check on the reviewed points. Jurists are optional — the platform works without them and the default is 'Alle'; review only confirms a figure. Empty filtered view gets its own hint. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
bc3a001374
commit
4e00c83591
2 changed files with 161 additions and 3 deletions
|
|
@ -10,6 +10,19 @@ import '../widgets/reclaim_card.dart';
|
|||
import '../widgets/tier_badge.dart';
|
||||
import 'norm_detail_page.dart';
|
||||
|
||||
/// Heatmap view modes. The platform works WITHOUT jurists — a jurist
|
||||
/// pass only *confirms* an evaluation — so the default shows all.
|
||||
enum _HeatmapView {
|
||||
/// Every evaluation, jurist-reviewed or not.
|
||||
all,
|
||||
|
||||
/// Only evaluations a jurist has confirmed.
|
||||
reviewedOnly,
|
||||
|
||||
/// All evaluations, but reviewed ones carry a confirm marker.
|
||||
marked,
|
||||
}
|
||||
|
||||
class HeatmapPage extends StatefulWidget {
|
||||
const HeatmapPage({super.key, required this.repository});
|
||||
|
||||
|
|
@ -21,6 +34,7 @@ class HeatmapPage extends StatefulWidget {
|
|||
|
||||
class _HeatmapPageState extends State<HeatmapPage> {
|
||||
late Future<List<Evaluation>> _future;
|
||||
_HeatmapView _view = _HeatmapView.all;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -45,6 +59,9 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||
child: CircularProgressIndicator());
|
||||
}
|
||||
final items = snap.data!;
|
||||
final shown = _view == _HeatmapView.reviewedOnly
|
||||
? items.where((e) => e.isReviewed).toList()
|
||||
: items;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -58,15 +75,29 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||
color: ReclaimColors.mute,
|
||||
),
|
||||
),
|
||||
if (items.isNotEmpty) ...[
|
||||
const SizedBox(height: ReclaimSpace.md),
|
||||
_ViewModeSelector(
|
||||
view: _view,
|
||||
reviewedCount:
|
||||
items.where((e) => e.isReviewed).length,
|
||||
total: items.length,
|
||||
onChanged: (v) => setState(() => _view = v),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: ReclaimSpace.lg),
|
||||
Expanded(
|
||||
child: items.isEmpty
|
||||
? _EmptyHubState(repository: widget.repository)
|
||||
: shown.isEmpty
|
||||
? const _NoReviewedState()
|
||||
: ReclaimCard(
|
||||
accent: true,
|
||||
expand: true,
|
||||
child: HeatmapGrid(
|
||||
evaluations: items,
|
||||
evaluations: shown,
|
||||
markReviewed:
|
||||
_view == _HeatmapView.marked,
|
||||
onTap: (e) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
|
|
@ -88,7 +119,7 @@ class _HeatmapPageState extends State<HeatmapPage> {
|
|||
spacing: ReclaimSpace.md,
|
||||
runSpacing: ReclaimSpace.sm,
|
||||
children: [
|
||||
for (final e in items)
|
||||
for (final e in shown)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
|
|
@ -375,6 +406,97 @@ class _EmptyHubState extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Heatmap view-mode toggle: all / jurist-reviewed only / marked.
|
||||
class _ViewModeSelector extends StatelessWidget {
|
||||
const _ViewModeSelector({
|
||||
required this.view,
|
||||
required this.reviewedCount,
|
||||
required this.total,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final _HeatmapView view;
|
||||
final int reviewedCount;
|
||||
final int total;
|
||||
final ValueChanged<_HeatmapView> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Theme.of(context).textTheme;
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.visibility_outlined,
|
||||
size: 16, color: ReclaimColors.mute),
|
||||
const SizedBox(width: ReclaimSpace.sm),
|
||||
Text('Ansicht',
|
||||
style: t.labelLarge?.copyWith(color: ReclaimColors.mute)),
|
||||
const SizedBox(width: ReclaimSpace.md),
|
||||
SegmentedButton<_HeatmapView>(
|
||||
showSelectedIcon: false,
|
||||
style: const ButtonStyle(visualDensity: VisualDensity.compact),
|
||||
segments: [
|
||||
const ButtonSegment(
|
||||
value: _HeatmapView.all,
|
||||
label: Text('Alle'),
|
||||
icon: Icon(Icons.scatter_plot_outlined, size: 15),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _HeatmapView.reviewedOnly,
|
||||
label: Text('Geprüft ($reviewedCount)'),
|
||||
icon: const Icon(Icons.verified_outlined, size: 15),
|
||||
),
|
||||
const ButtonSegment(
|
||||
value: _HeatmapView.marked,
|
||||
label: Text('Markiert'),
|
||||
icon: Icon(Icons.label_important_outline, size: 15),
|
||||
),
|
||||
],
|
||||
selected: {view},
|
||||
onSelectionChanged: (s) => onChanged(s.first),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shown when the 'jurist-reviewed only' filter leaves nothing.
|
||||
class _NoReviewedState extends StatelessWidget {
|
||||
const _NoReviewedState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Theme.of(context).textTheme;
|
||||
return ReclaimCard(
|
||||
accent: true,
|
||||
expand: true,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 440),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.verified_outlined,
|
||||
size: 40, color: ReclaimColors.mute),
|
||||
const SizedBox(height: ReclaimSpace.md),
|
||||
Text('Noch keine juristen-geprüften Auswertungen',
|
||||
style: t.headlineSmall),
|
||||
const SizedBox(height: ReclaimSpace.sm),
|
||||
Text(
|
||||
'Die Plattform funktioniert auch ohne Juristen — wechsle '
|
||||
'auf „Alle", um alle Auswertungen zu sehen. Ein '
|
||||
'Juristen-Review bestätigt eine Auswertung zusätzlich.',
|
||||
style: t.bodyLarge
|
||||
?.copyWith(color: ReclaimColors.mute, height: 1.4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiniLabel extends StatelessWidget {
|
||||
const _MiniLabel({required this.symbol, required this.text});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@ class HeatmapGrid extends StatefulWidget {
|
|||
super.key,
|
||||
required this.evaluations,
|
||||
this.onTap,
|
||||
this.markReviewed = false,
|
||||
});
|
||||
|
||||
final List<Evaluation> evaluations;
|
||||
final void Function(Evaluation)? onTap;
|
||||
|
||||
/// When true, points whose evaluation a jurist confirmed get a
|
||||
/// confirm marker (used by the heatmap's "Markiert" view mode).
|
||||
final bool markReviewed;
|
||||
|
||||
@override
|
||||
State<HeatmapGrid> createState() => _HeatmapGridState();
|
||||
}
|
||||
|
|
@ -45,6 +50,7 @@ class _HeatmapGridState extends State<HeatmapGrid> {
|
|||
painter: _HeatmapPainter(
|
||||
evaluations: widget.evaluations,
|
||||
hoverIndex: _hoverIndex,
|
||||
markReviewed: widget.markReviewed,
|
||||
theme: Theme.of(context),
|
||||
),
|
||||
size: size,
|
||||
|
|
@ -110,11 +116,13 @@ class _HeatmapPainter extends CustomPainter {
|
|||
_HeatmapPainter({
|
||||
required this.evaluations,
|
||||
required this.hoverIndex,
|
||||
required this.markReviewed,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
final List<Evaluation> evaluations;
|
||||
final int? hoverIndex;
|
||||
final bool markReviewed;
|
||||
final ThemeData theme;
|
||||
|
||||
@override
|
||||
|
|
@ -217,6 +225,33 @@ class _HeatmapPainter extends CustomPainter {
|
|||
canvas.drawCircle(c, r, body);
|
||||
canvas.drawCircle(c, r, ring);
|
||||
|
||||
// Jurist-confirmed marker (heatmap "Markiert" mode): a small
|
||||
// confirm-green dot with a white check at the point's NE edge.
|
||||
if (markReviewed && e.isReviewed) {
|
||||
final m = Offset(c.dx + r * 0.72, c.dy - r * 0.72);
|
||||
canvas.drawCircle(
|
||||
m, 5.5, Paint()..color = ReclaimColors.reviewConfirmed);
|
||||
canvas.drawCircle(
|
||||
m,
|
||||
5.5,
|
||||
Paint()
|
||||
..color = theme.colorScheme.surface
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.5);
|
||||
final check = Path()
|
||||
..moveTo(m.dx - 2.4, m.dy + 0.2)
|
||||
..lineTo(m.dx - 0.6, m.dy + 2.0)
|
||||
..lineTo(m.dx + 2.6, m.dy - 2.2);
|
||||
canvas.drawPath(
|
||||
check,
|
||||
Paint()
|
||||
..color = theme.colorScheme.surface
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.4
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round);
|
||||
}
|
||||
|
||||
_drawText(
|
||||
canvas,
|
||||
'${e.norm.jurabk} ${e.norm.paragraph}',
|
||||
|
|
@ -303,5 +338,6 @@ class _HeatmapPainter extends CustomPainter {
|
|||
@override
|
||||
bool shouldRepaint(covariant _HeatmapPainter old) =>
|
||||
old.hoverIndex != hoverIndex ||
|
||||
old.markReviewed != markReviewed ||
|
||||
old.evaluations.length != evaluations.length;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue