Files
GroundedHelper/frontend/lib/Grounded/see/reportcard/ReportCardScreenState.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

375 lines
11 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: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(12),
),
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(12),
),
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;
});
}
}