Initial commit: Grounded Flutter frontend
A to-do app that doesn't believe you — an enforcement layer rather than a
neutral ledger.
Architecture ported from Autoreceptives/Frontend/Receptive: stacked MVVM with
the mandatory 4-file screen pattern, one ParentViewModel owning the loading /
network / error overlays and the handleError decision tree, one AppDataManager
gateway, dio comms carrying the three identity headers, secure storage with
random-suffixed keys, and a single-chokepoint Navigator. Package root and Dart
package name are both Grounded; org is nya.
The enforcement engine, one unit per formula in utils/:
- DebtEngine w(class) x severity(d) x decay(t), sublinear severity so old
misses cannot swamp the score; abandonment 2x with 30-day
decay immunity; late complete retains 30%
- StandingEngine Good -> Warned -> Grounded -> Lockdown, derived not set;
Grounded replaces home with the overdue queue
- CapacityEngine blocks over-scheduling against p50 of historically completed
minutes, with a learned per-category estimation multiplier
- IntegrityEngine session integrity, weekly volume, plyometric contact ceiling
and enforced recovery gaps
- ExcuseAnalyser on-device excuse clustering plus the confrontation copy
- GuardrailEngine distress detection and rationed amnesty
- ToneEngine all enforcement copy, so the tone cap lives in one place
CommitmentEvent is append-only and is the source of truth rather than the
status field, which is what makes honest history and excuse analysis possible.
Goals contain commitments via parentId, and a task can be run from a
full-screen runner that derives elapsed time from wall-clock so screen-off
cannot lose time. Backgrounding pauses the clock and is counted. The runner is
mirrored into an ongoing notification, with alarm-class full-screen intents
reserved for non-negotiables.
Design language, fonts, icon and native splash are in place; Mason bricks are
retargeted to this project and verified end-to-end.
flutter analyze lib/ reports no errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mfu2gQLSFN21YRBcU2NrTt
This commit is contained in:
374
frontend/lib/Grounded/see/reportcard/ReportCardScreenState.dart
Normal file
374
frontend/lib/Grounded/see/reportcard/ReportCardScreenState.dart
Normal file
@@ -0,0 +1,374 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../about/external/data/ReportCard.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 'ConnectReportCardScreen.dart';
|
||||
import 'ReportCardScreen.dart';
|
||||
import 'ViewReportCardScreen.dart';
|
||||
|
||||
class ReportCardScreenState extends State<ReportCardScreen>
|
||||
implements ConnectReportCardScreen {
|
||||
ViewReportCardScreen? _model;
|
||||
|
||||
ReportCard _report = ReportCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewReportCardScreen>.reactive(
|
||||
viewModelBuilder: () => ViewReportCardScreen(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?.loadReport();
|
||||
}
|
||||
|
||||
void _onBack() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
Widget _mobileView(BoxConstraints constraints) {
|
||||
return Sheet(
|
||||
eyebrow: "This week",
|
||||
title: "Report card",
|
||||
onBack: _onBack,
|
||||
banner: _gradeBanner(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_report.praise.isNotEmpty) ...[
|
||||
_praiseCard(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
_assignedAction(),
|
||||
const SizedBox(height: 28),
|
||||
sectionBreak("Debt", caption: "across the week"),
|
||||
_debtTrend(),
|
||||
const SizedBox(height: 28),
|
||||
sectionBreak("Completion", caption: "by class"),
|
||||
..._report.completionByClass.entries.map(_completionRow),
|
||||
const SizedBox(height: 20),
|
||||
_worstHour(),
|
||||
const SizedBox(height: 28),
|
||||
if (_report.deferralLeaderboard.isNotEmpty) ...[
|
||||
sectionBreak("Most dodged", caption: "the leaderboard"),
|
||||
..._report.deferralLeaderboard.take(5).map(_deferralRow),
|
||||
const SizedBox(height: 28),
|
||||
],
|
||||
if (_report.estimationAccuracy.isNotEmpty) ...[
|
||||
sectionBreak("Your estimates", caption: "against reality"),
|
||||
..._report.estimationAccuracy.entries.map(_estimationRow),
|
||||
const SizedBox(height: 28),
|
||||
],
|
||||
if (_report.trainingAdherence > 0) ...[
|
||||
sectionBreak("Training"),
|
||||
_trainingCard(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The grade sits in the chrome. Cosmetic, but it is the thing people
|
||||
/// actually react to.
|
||||
Widget _gradeBanner() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("PERIOD", 9, TextType.Bold,
|
||||
color: colorWhite.withValues(alpha: 0.45),
|
||||
letterSpacing: 1.2),
|
||||
const SizedBox(height: 6),
|
||||
text(
|
||||
"${formatDate(_report.periodStart)} — ${formatDate(_report.periodEnd)}",
|
||||
13,
|
||||
TextType.Medium,
|
||||
color: colorWhite,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_report.grade.isNotEmpty)
|
||||
Container(
|
||||
width: 54,
|
||||
height: 54,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: colorWhite,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: text(_report.grade, 26, TextType.Light,
|
||||
color: colorPrimaryDark),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Rationed but real. Only rendered when something specific was earned.
|
||||
Widget _praiseCard() {
|
||||
return card(
|
||||
background: colorStandingGoodBg,
|
||||
borderColor: colorPositive.withValues(alpha: 0.20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(CupertinoIcons.checkmark_seal_fill,
|
||||
size: 18, color: colorPositive),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: text(_report.praise, 14, TextType.Regular,
|
||||
color: colorPrimaryDark, height: 1.55),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// One assigned action for next week. Not five.
|
||||
Widget _assignedAction() {
|
||||
return card(
|
||||
background: colorPrimaryDark,
|
||||
borderColor: colorPrimaryDark,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("NEXT WEEK, ONE THING", 9, TextType.Bold,
|
||||
color: colorWhite.withValues(alpha: 0.45), letterSpacing: 1.2),
|
||||
const SizedBox(height: 12),
|
||||
text(
|
||||
_report.assignedAction.isEmpty
|
||||
? "Not enough history yet to assign anything."
|
||||
: _report.assignedAction,
|
||||
19,
|
||||
TextType.Light,
|
||||
color: colorWhite,
|
||||
height: 1.4,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _debtTrend() {
|
||||
if (_report.debtTrend.isEmpty) {
|
||||
return card(
|
||||
child: text("No debt recorded this week.", 13, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
);
|
||||
}
|
||||
|
||||
final List<FlSpot> spots = <FlSpot>[];
|
||||
for (int index = 0; index < _report.debtTrend.length; index++) {
|
||||
spots.add(FlSpot(index.toDouble(), _report.debtTrend[index]));
|
||||
}
|
||||
|
||||
return card(
|
||||
padding: const EdgeInsets.fromLTRB(8, 20, 16, 8),
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (value) =>
|
||||
const FlLine(color: colorChartGrid, strokeWidth: 1),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: true, reservedSize: 32)),
|
||||
bottomTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: <LineChartBarData>[
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: true,
|
||||
barWidth: 2.5,
|
||||
color: colorDebtLine,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(show: true, color: colorDebtFill),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _completionRow(MapEntry<String, double> entry) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
text(entry.key, 13, TextType.Medium, color: colorPrimaryDark),
|
||||
text("${(entry.value * 100).round()}%", 13, TextType.Bold,
|
||||
color: colorGrey2),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
meter(
|
||||
entry.value,
|
||||
fill: entry.value >= 0.8
|
||||
? colorPositive
|
||||
: entry.value >= 0.5
|
||||
? colorStandingWarned
|
||||
: colorStandingGrounded,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The recurring window where things go to die.
|
||||
Widget _worstHour() {
|
||||
if (_report.worstHour < 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return card(
|
||||
background: colorStandingWarnedBg,
|
||||
borderColor: colorStandingWarned.withValues(alpha: 0.20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text("YOUR WORST HOUR", 9, TextType.Bold,
|
||||
color: colorStandingWarned, letterSpacing: 1.2),
|
||||
const SizedBox(height: 10),
|
||||
text(hourLabel(_report.worstHour), 30, TextType.Light,
|
||||
color: colorPrimaryDark),
|
||||
const SizedBox(height: 8),
|
||||
text(
|
||||
"This is where things go to die. Stop scheduling anything that matters into it.",
|
||||
13,
|
||||
TextType.Regular,
|
||||
color: colorGrey2,
|
||||
height: 1.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _deferralRow(DeferralCount item) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: card(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: text(item.title, 13, TextType.Regular,
|
||||
color: colorPrimaryDark,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
pill("${item.count}×", colorStandingGrounded,
|
||||
colorStandingGroundedBg, textSize: 9),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Your estimates are wrong, and this is by how much.
|
||||
Widget _estimationRow(MapEntry<String, double> entry) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: card(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: text(entry.key, 13, TextType.Regular,
|
||||
color: colorPrimaryDark),
|
||||
),
|
||||
text("${entry.value.toStringAsFixed(1)}×", 15, TextType.Bold,
|
||||
color: entry.value > 1.4 ? colorStandingGrounded : colorGrey2),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _trainingCard() {
|
||||
return card(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: labelled(
|
||||
"Adherence",
|
||||
"${(_report.trainingAdherence * 100).round()}%",
|
||||
valueSize: 26,
|
||||
valueType: TextType.Light,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: labelled(
|
||||
"Integrity",
|
||||
"${(_report.programIntegrity * 100).round()}%",
|
||||
valueSize: 26,
|
||||
valueType: TextType.Light,
|
||||
valueColor: _report.programIntegrity < 0.7
|
||||
? colorStandingGrounded
|
||||
: colorPrimaryDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onReportLoaded(ReportCard report) {
|
||||
setState(() {
|
||||
_report = report;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user