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:
@@ -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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user