Miler rider app: surface system, visible design language, backend lifecycle

Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
  as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
  the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
  canvas wells for anything that opens, small filled tags for shelf labels,
  demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
  lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
  reserved for the live stop, disabled primaries go neutral rather than pale.

Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
  makes admin sequence the single ordering authority; service_day.dart, and
  stop_area.dart rewritten against live Coimbatore addresses (digit-token
  stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.

Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
  sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.

Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 05:40:35 +05:30
parent c8350563d9
commit d7348e253f
387 changed files with 80693 additions and 12272 deletions

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:miler/data/service_profile.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart' hide Marker;
import 'package:fl_chart/fl_chart.dart';
@@ -169,7 +171,7 @@ class _SummaryState extends State<Summary> {
_periods[index],
style: TextStyle(
fontSize: 13.sp,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected
? Colors.white
: ColorConstants.onSurfaceVariant,
@@ -238,12 +240,16 @@ class _SummaryState extends State<Summary> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${_periods[_selectedPeriod].toUpperCase()} · PICKUPS/DELIVERIES',
// `WEEK · PICKUPS/DELIVERIES` was a slug, not a heading —
// a slash between two nouns is a database field name. The
// rider is reading his own week.
'${_periods[_selectedPeriod]} · '
'${ServiceProfile.active.jobNounPlural}',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 11.sp,
fontWeight: FontWeight.w700,
letterSpacing: 1.2,
color: Colors.white.withValues(alpha: 0.85),
fontSize: 12.5.sp,
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
fontFamily: FontConstants.fontFamily,
),
),
@@ -300,7 +306,7 @@ class _SummaryState extends State<Summary> {
child: _buildHeroMetric(
'$_successRate%',
'Success rate',
icon: Icons.percent_rounded,
icon: LucideIcons.percent,
),
),
SizedBox(width: 12.w),
@@ -308,7 +314,7 @@ class _SummaryState extends State<Summary> {
child: _buildHeroMetric(
'$cancelled',
'Cancelled',
icon: Icons.do_not_disturb_on_rounded,
icon: LucideIcons.circleMinus,
),
),
SizedBox(width: 12.w),
@@ -316,7 +322,7 @@ class _SummaryState extends State<Summary> {
child: _buildHeroMetric(
'$points',
'Reward pts',
icon: Icons.stars_rounded,
icon: LucideIcons.star,
),
),
],
@@ -348,29 +354,28 @@ class _SummaryState extends State<Summary> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null) ...[
Icon(
icon,
size: 14.sp,
color: Colors.white.withValues(alpha: 0.85),
),
Icon(icon, size: 16.sp, color: Colors.white.withValues(alpha: 0.9)),
SizedBox(height: 6.h),
],
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: 15.sp,
fontWeight: FontWeight.w800,
fontSize: 17.sp,
fontWeight: FontWeight.w700,
letterSpacing: -0.3,
fontFamily: FontConstants.fontFamily,
),
),
SizedBox(height: 2.h),
SizedBox(height: 3.h),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 10.sp,
fontWeight: FontWeight.w500,
// 10 -> 12, and up from w500. A label at 65% white on maroon at
// ten points is decoration; the tile is worthless without it.
color: Colors.white.withValues(alpha: 0.78),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
),
),
@@ -414,134 +419,90 @@ class _SummaryState extends State<Summary> {
'LIFETIME',
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
letterSpacing: 1.0,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
),
),
// IntrinsicHeight so the three cards match height whatever their
// figures. `CrossAxisAlignment.stretch` alone would ask for infinite
// height inside this scroll view — the trap pinned by
// `card_stripe_layout_test.dart`.
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: _statCard(
icon: Icons.check_circle_rounded,
value: '$total',
label: 'Completed',
accent: ColorConstants.acceptGreen,
// ── A ruled band, not three cards ──
//
// These were three shadowed white cards, each with a tinted icon disc
// — the heaviest stat idiom left in the app, sitting directly under a
// brand hero that already carries its own three figures. Account's
// stats band set the system's shape for exactly this content: figures
// separated by hairlines on one quiet fill, no discs, no shadows.
// Same three facts, a quarter of the ink — and the band no longer
// competes with the hero above it for the reader's first look.
Container(
decoration: BoxDecoration(
color: ColorConstants.cardSurface,
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
),
padding: EdgeInsets.symmetric(vertical: 16.h),
child: IntrinsicHeight(
child: Row(
children: [
Expanded(child: _statFigure('$total', 'Completed')),
_statRule(),
Expanded(child: _statFigure('$cancelled', 'Fell through')),
_statRule(),
Expanded(
child: _statFigure(
attempts > 0 ? '$rate%' : '\u2014',
'Success',
),
),
),
SizedBox(width: 10.w),
Expanded(
child: _statCard(
// Filled, like the other two. This was the one *outlined*
// glyph in a row of three, so beside a solid tick it read as
// lighter and slightly unfinished — a set of icons has to
// agree on fill weight before it agrees on anything else.
icon: Icons.do_not_disturb_on_rounded,
value: '$cancelled',
label: 'Fell through',
// Grey, not red. A stop that fell through is usually a shut
// shop or an absent customer — a fact about the day's
// logistics, not the rider's fault — and this is the screen
// where he checks his earnings, not where he gets told off.
accent: ColorConstants.secondaryText,
),
),
SizedBox(width: 10.w),
Expanded(
child: _statCard(
// A percentage, not a trend. `trending_up` promises movement
// over time — an arrow climbing — and this is a ratio of two
// lifetime totals that mostly sits still. The glyph should
// say what the number is.
icon: Icons.percent_rounded,
value: attempts > 0 ? '$rate%' : '—',
label: 'Success',
accent: ColorConstants.acceptGreen,
),
),
],
],
),
),
),
],
);
}
/// One stat card. Mirrors `TripCard`'s metric card on Home, deliberately.
Widget _statCard({
required IconData icon,
required String value,
required String label,
required Color accent,
}) {
return Container(
padding: EdgeInsets.symmetric(vertical: 14.h, horizontal: 6.w),
decoration: BoxDecoration(
color: ColorConstants.pureSurface,
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
boxShadow: const [
BoxShadow(
color: Color(0x0F000000),
blurRadius: 14,
offset: Offset(0, 5),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 36.w,
height: 36.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.10),
shape: BoxShape.circle,
),
child: Icon(icon, size: 18.sp, color: accent),
),
SizedBox(height: 11.h),
// Fixed height so a four-digit total scales down inside its own card
// rather than making one of the three taller than the other two.
SizedBox(
height: 24.h,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
value,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w800,
letterSpacing: -0.6,
height: 1.05,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
),
),
),
),
SizedBox(height: 4.h),
Text(
label.toUpperCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
Widget _statRule() => VerticalDivider(
width: 1,
thickness: 1,
indent: 6.h,
endIndent: 6.h,
color: ColorConstants.borderSubtle,
);
/// One figure in the lifetime band. Monochrome on purpose: these are
/// lifetime ledger facts, and the day's colour belongs to the hero above.
Widget _statFigure(String value, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
value,
style: TextStyle(
fontSize: 9.5.sp,
fontWeight: FontWeight.w800,
letterSpacing: 0.7,
color: ColorConstants.secondaryText,
fontSize: 20.sp,
fontWeight: FontWeight.w700,
letterSpacing: -0.6,
height: 1.05,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
),
),
],
),
),
SizedBox(height: 4.h),
Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5.sp,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
),
],
);
}
}
@@ -607,8 +568,10 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
/// looking at the debug build is the one deciding whether the feature works.
/// The empty state says "No distance recorded yet", which is true and is what
/// makes a silent endpoint visible.
Map<String, dynamic> _fallbackWeeklyKms() =>
{'details': const <RiderWeeklyKms>[], 'total_kms': 0.0};
Map<String, dynamic> _fallbackWeeklyKms() => {
'details': const <RiderWeeklyKms>[],
'total_kms': 0.0,
};
String _shortDay(String day) => day.length <= 3 ? day : day.substring(0, 3);
@@ -641,7 +604,7 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
// kilometres covered, not how fast he went. A route glyph is
// both accurate and the one every delivery app uses for distance.
Icon(
Icons.route_rounded,
LucideIcons.route,
color: ColorConstants.primary,
size: 20.sp,
),
@@ -656,7 +619,7 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
color: ColorConstants.onSurface,
fontFamily: FontConstants.fontFamily,
),
@@ -785,7 +748,7 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
'${rod.toY.toStringAsFixed(1)} km',
TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
fontSize: 12.sp,
fontFamily: FontConstants.fontFamily,
),
@@ -836,7 +799,7 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
style: TextStyle(
fontSize: 11.sp,
fontWeight: isTouched
? FontWeight.w800
? FontWeight.w700
: FontWeight.w500,
color: isTouched
? ColorConstants.primary
@@ -876,23 +839,32 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
),
),
SizedBox(height: 12.h),
// ── Monochrome, like every other figure row ──
//
// These were amber / brand-red / blue — three hues for three ledger
// facts, on the one page family where colour is reserved for the hero
// and the chart's own bars. And "Best Day" rendered blank when the
// week's data carries no day names, leaving a caption with nothing
// above it; an absent fact prints the em dash like everywhere else.
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildChartStat(
'Best Day',
_shortDay(details[bestIdx].day),
ColorConstants.warning,
'Best day',
_shortDay(details[bestIdx].day).trim().isEmpty
? '\u2014'
: _shortDay(details[bestIdx].day),
ColorConstants.slateText,
),
_buildChartStat(
'Total',
'${totalKms.toStringAsFixed(1)} km',
ColorConstants.primary,
ColorConstants.slateText,
),
_buildChartStat(
'Avg/Day',
'Avg/day',
'${avgPerDay.toStringAsFixed(1)} km',
ColorConstants.tertiary,
ColorConstants.slateText,
),
],
),
@@ -907,7 +879,7 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
value,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
color: color,
fontFamily: FontConstants.fontFamily,
),