Files
GroundedHelper/frontend/lib/Grounded/see/excuse/ExcuseReportState.dart
alvocool d3431ca5de Switch to Inter, round the corners softly and make the nav switch tabs
Inter replaces General Sans (Inter Display for titles and big numbers).
Corners come back from before the squaring commit, scaled to a soft set
(16 cards, 14 buttons, 12 rows, 8 chips) and applied to the screens built
since. The nav is now a cluster of round frosted buttons that switches
Today, Routines, Goals, Groups and Progress in place inside Home: no
pushed pages, no back arrows, and Android back returns to Today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-09 07:08:46 +03:00

217 lines
6.7 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../about/external/data/ExcuseCluster.dart';
import '../../about/internal/application/TextType.dart';
import '../../designs/Component.dart';
import '../../designs/Responsive.dart';
import '../../designs/Shell.dart';
import '../../designs/text/Text.dart';
import '../../utils/Colors.dart';
import '../../utils/CommonUtils.dart';
import 'ConnectExcuseReport.dart';
import 'ExcuseReport.dart';
import 'ViewExcuseReport.dart';
class ExcuseReportState extends State<ExcuseReport>
implements ConnectExcuseReport {
ViewExcuseReport? _model;
List<ExcuseCluster> _clusters = <ExcuseCluster>[];
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewExcuseReport>.reactive(
viewModelBuilder: () => ViewExcuseReport(context, this),
onViewModelReady: (viewModel) {
_model = viewModel;
_initiate();
},
builder: (context, viewModel, child) => LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Responsive(
mobile: _mobileView(constraints),
tablet: _mobileView(constraints),
desktop: _mobileView(constraints),
);
},
),
);
}
void _initiate() {
_model?.loadClusters();
}
void _onBack() {
Navigator.pop(context);
}
Widget _mobileView(BoxConstraints constraints) {
final int total = _clusters.fold(0, (sum, item) => sum + item.occurrences);
return Sheet(
eyebrow: "Last 30 days",
title: "Excuses",
onBack: _onBack,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
displayTitle("What you tell\nyourself."),
const SizedBox(height: 14),
text(
"Every deferral you wrote, grouped. Read the concentrations rather than the totals — that is where the pattern is.",
14,
TextType.Regular,
color: colorGrey2,
height: 1.55,
),
const SizedBox(height: 28),
if (_clusters.isEmpty)
emptyState(
CupertinoIcons.text_quote,
"Nothing to confront yet",
"Excuses appear here once you have deferred a few things. There is no shame in an empty page.",
accent: colorPositive,
)
else ...[
card(
background: colorPrimaryDark,
borderColor: colorPrimaryDark,
child: Row(
children: [
Expanded(
child: labelled(
"Total excuses",
"$total",
valueSize: 30,
valueType: TextType.Light,
valueColor: colorWhite,
labelColor: colorWhite.withValues(alpha: 0.45),
),
),
Expanded(
child: labelled(
"Distinct kinds",
"${_clusters.length}",
valueSize: 30,
valueType: TextType.Light,
valueColor: colorWhite,
labelColor: colorWhite.withValues(alpha: 0.45),
),
),
],
),
),
const SizedBox(height: 24),
sectionBreak("The taxonomy", caption: "most frequent first"),
..._clusters.map(_clusterCard),
],
],
),
);
}
Widget _clusterCard(ExcuseCluster cluster) {
final MapEntry<int, int>? peakDay = _peak(cluster.byWeekday);
final MapEntry<int, int>? peakHour = _peak(cluster.byHour);
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: text(cluster.label, 19, TextType.Light,
color: colorPrimaryDark),
),
pill("${cluster.occurrences}×", colorPrimaryDark, colorMuted,
textSize: 10),
],
),
if (cluster.insight.isNotEmpty) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: colorInset,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: colorBorder, width: 1),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(CupertinoIcons.quote_bubble_fill,
size: 14, color: colorGrey),
const SizedBox(width: 10),
Expanded(
child: text(cluster.insight, 13, TextType.Regular,
color: colorPrimaryDark, height: 1.55),
),
],
),
),
],
hairline(margin: const EdgeInsets.symmetric(vertical: 16)),
Row(
children: [
Expanded(
child: labelled(
"Worst day",
peakDay == null ? "—" : weekdayName(peakDay.key),
valueSize: 13,
),
),
Expanded(
child: labelled(
"Worst hour",
peakHour == null ? "—" : hourLabel(peakHour.key),
valueSize: 13,
),
),
Expanded(
child: labelled(
"Category",
cluster.dominantCategory.isEmpty
? "—"
: cluster.dominantCategory,
valueSize: 13,
),
),
],
),
],
),
),
);
}
MapEntry<int, int>? _peak(Map<int, int> histogram) {
if (histogram.isEmpty) {
return null;
}
MapEntry<int, int>? peak;
for (MapEntry<int, int> entry in histogram.entries) {
if (peak == null || entry.value > peak.value) {
peak = entry;
}
}
return peak;
}
@override
void onClustersLoaded(List<ExcuseCluster> clusters) {
setState(() {
_clusters = clusters;
});
}
}