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 implements ConnectExcuseReport { ViewExcuseReport? _model; List _clusters = []; @override Widget build(BuildContext context) { return ViewModelBuilder.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? peakDay = _peak(cluster.byWeekday); final MapEntry? 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(12), 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? _peak(Map histogram) { if (histogram.isEmpty) { return null; } MapEntry? peak; for (MapEntry entry in histogram.entries) { if (peak == null || entry.value > peak.value) { peak = entry; } } return peak; } @override void onClustersLoaded(List clusters) { setState(() { _clusters = clusters; }); } }