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,5 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'dart:async';
import 'package:miler/views/helpers/constants/Font_constant.dart';
@@ -6,7 +7,8 @@ import 'package:slide_to_submit_button/slide_to_submit_button.dart';
import 'package:image_picker/image_picker.dart';
import 'package:miler/views/helpers/constants/design_constants.dart';
import 'package:miler/views/helpers/widgets/app_widgets.dart';
import 'package:miler/views/helpers/widgets/page_transitions.dart';
import 'package:miler/views/helpers/widgets/miler_sheet_kit.dart';
import 'package:miler/data/service_profile.dart';
/// ------------------------------
/// MAIN WIDGET WITH TWO BUTTONS
@@ -118,12 +120,15 @@ class _OrderStatusRowState extends State<OrderStatusRow> {
// REJECT SHEET
// ------------------------------
void _showRejectSheet(BuildContext context) {
showModalBottomSheet(
context: context,
sheetAnimationStyle: kMilerSheetStyle,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
// Through the kit. Two things changed beyond the fabric:
//
// • The Close IconButton is gone — the handle and the barrier already
// dismiss, and a sheet's title row is not a toolbar.
// • A selected reason is a soft red field with a tick, not a solid red
// slab. Filled-primary selection made four buttons compete with the
// one destructive CTA under them, and colour was its only channel.
showMilerSheet<void>(
context,
builder: (context) {
String selectedReason = "";
bool isLoading = false;
@@ -134,100 +139,51 @@ class _OrderStatusRowState extends State<OrderStatusRow> {
"Other reason",
];
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Reject booking",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
IconButton(
tooltip: 'Close',
icon: Icon(
Icons.close_rounded,
color: ColorConstants.onSurfaceVariant,
),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 10),
for (String reason in reasons)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: GestureDetector(
onTap: () => setModalState(() {
selectedReason = reason;
}),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
height: ButtonSizes.primary,
width: double.infinity,
decoration: BoxDecoration(
color: selectedReason == reason
? ColorConstants.primary
: ColorConstants.neutralLight,
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
border: Border.all(
color: selectedReason == reason
? ColorConstants.primary
: ColorConstants.dividerColor,
),
),
alignment: Alignment.center,
child: Text(
reason,
style: TextStyle(
color: selectedReason == reason
? Colors.white
: ColorConstants.onSurface,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
fontSize: 15,
),
),
),
),
return StatefulBuilder(
builder: (context, setModalState) {
return MilerSheetScaffold(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const MilerSheetHeader(
title: 'Reject booking',
subtitle: 'The hub re-routes it once it knows why.',
),
const SizedBox(height: 14),
for (String reason in reasons)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: MilerSheetChoiceRow(
label: reason,
selected: selectedReason == reason,
onTap: () => setModalState(() {
selectedReason = reason;
}),
),
const SizedBox(height: 20),
MilerButton(
label: "Reject booking",
color: ColorConstants.error,
loading: isLoading,
onPressed: selectedReason.isEmpty
? null
: () async {
setModalState(() => isLoading = true);
final success = await _onStatusChanged(
"REJECTED",
notes: selectedReason,
);
if (context.mounted) {
Navigator.pop(context, success);
}
},
),
],
),
);
},
),
const SizedBox(height: 10),
MilerButton(
label: "Reject booking",
color: ColorConstants.error,
loading: isLoading,
onPressed: selectedReason.isEmpty
? null
: () async {
setModalState(() => isLoading = true);
final success = await _onStatusChanged(
"REJECTED",
notes: selectedReason,
);
if (context.mounted) {
Navigator.pop(context, success);
}
},
),
],
),
);
},
);
},
);
@@ -237,87 +193,68 @@ class _OrderStatusRowState extends State<OrderStatusRow> {
// CANCEL SHEET
// ------------------------------
void _showCancelSheet(BuildContext context) {
showModalBottomSheet(
context: context,
sheetAnimationStyle: kMilerSheetStyle,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
showMilerSheet<void>(
context,
builder: (context) {
bool confirmCancel = false;
bool isLoading = false;
return SafeArea(
top: false,
left: false,
right: false,
bottom: true,
child: StatefulBuilder(
builder: (context, setModalState) {
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Cancel this Booking?",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
fontFamily: FontConstants.fontFamily,
),
),
const SizedBox(height: 10),
const Text(
"Once cancelled, this Booking will return to pending state.",
textAlign: TextAlign.center,
),
const SizedBox(height: 25),
CheckboxListTile(
value: confirmCancel,
onChanged: (value) =>
setModalState(() => confirmCancel = value ?? false),
title: const Text("I confirm to cancel this Booking"),
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 20),
MilerButton(
label: "Confirm cancel",
color: ColorConstants.warning,
loading: isLoading,
onPressed: !confirmCancel
? null
: () async {
setModalState(() => isLoading = true);
// Capture the messenger BEFORE popping — using the
// sheet's context after pop looks up a deactivated
// widget and can crash.
final messenger = ScaffoldMessenger.of(context);
// Actually cancel via the real status-change path
// instead of just showing a fake success snackbar.
final success = await _onStatusChanged(
"CANCELLED",
notes: "Cancelled by rider",
);
if (context.mounted) {
Navigator.pop(context, success);
}
messenger.showSnackBar(
SnackBar(
content: Text(
success
? "Booking cancelled"
: "Couldn't cancel. Please try again.",
),
return StatefulBuilder(
builder: (context, setModalState) {
return MilerSheetScaffold(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const MilerSheetHeader(
title: 'Cancel this booking?',
subtitle: 'It returns to pending and the hub reassigns it.',
),
const SizedBox(height: 14),
CheckboxListTile(
value: confirmCancel,
onChanged: (value) =>
setModalState(() => confirmCancel = value ?? false),
title: const Text("I confirm to cancel this Booking"),
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 20),
MilerButton(
label: "Confirm cancel",
color: ColorConstants.warning,
loading: isLoading,
onPressed: !confirmCancel
? null
: () async {
setModalState(() => isLoading = true);
// Capture the messenger BEFORE popping — using the
// sheet's context after pop looks up a deactivated
// widget and can crash.
final messenger = ScaffoldMessenger.of(context);
// Actually cancel via the real status-change path
// instead of just showing a fake success snackbar.
final success = await _onStatusChanged(
"CANCELLED",
notes: "Cancelled by rider",
);
if (context.mounted) {
Navigator.pop(context, success);
}
messenger.showSnackBar(
SnackBar(
content: Text(
success
? "Booking cancelled"
: "Couldn't cancel. Please try again.",
),
);
},
),
],
),
);
},
),
),
);
},
),
],
),
);
},
);
},
);
@@ -341,7 +278,7 @@ class _OrderStatusRowState extends State<OrderStatusRow> {
children: [
if (widget.onCall != null) ...[
_IconAction(
icon: Icons.phone_rounded,
icon: LucideIcons.phone,
color: ColorConstants.acceptGreen,
tooltip: 'Call customer',
onTap: widget.onCall,
@@ -350,7 +287,7 @@ class _OrderStatusRowState extends State<OrderStatusRow> {
],
if (widget.onDetails != null) ...[
_IconAction(
icon: Icons.receipt_long_rounded,
icon: LucideIcons.receiptText,
color: ColorConstants.onSurfaceVariant,
tooltip: 'Booking details',
onTap: widget.onDetails,
@@ -360,7 +297,7 @@ class _OrderStatusRowState extends State<OrderStatusRow> {
// LEFT PRIMARY (Reject / Cancel) — compact tonal destructive
_IconAction(
icon: showCancel ? Icons.undo_rounded : Icons.close_rounded,
icon: showCancel ? LucideIcons.undo2 : LucideIcons.x,
color: showCancel
? ColorConstants.warning
: ColorConstants.error,
@@ -391,7 +328,9 @@ class _OrderStatusRowState extends State<OrderStatusRow> {
child: Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.18),
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
borderRadius: BorderRadius.circular(
DesignConstants.radiusXl,
),
),
child: Center(
child: Container(
@@ -492,7 +431,7 @@ class StatusPill extends StatelessWidget {
style: TextStyle(
fontSize: 14.5,
color: fg,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
fontFamily: FontConstants.fontFamily,
),
@@ -585,13 +524,13 @@ class _OrderStatusButtonState extends State<OrderStatusButton> {
IconData _iconForStatus() {
switch (_buttonText) {
case "ARRIVED":
return Icons.inventory_2_rounded;
return LucideIcons.package;
case "PICKED":
return Icons.task_alt_rounded;
return LucideIcons.squareCheckBig;
case "ACCEPTED":
return Icons.directions_bike_rounded;
return LucideIcons.bike;
default:
return Icons.check_rounded;
return LucideIcons.check;
}
}
@@ -619,7 +558,14 @@ class _OrderStatusButtonState extends State<OrderStatusButton> {
case "ARRIVED":
return "Arrived";
case "PICKED":
return "Pick Up";
// The last rung is the same API call either way, but it is not the same
// act. On a parcel route the rider is taking something from a customer;
// on a service route he is giving something to one, and a slider
// reading "Pick Up" at a subscriber's door describes the opposite of
// what he is about to do.
return ServiceProfile.active.handsOffAtCollection
? "Handed over"
: "Pick Up";
default:
return _nextStatus ?? "";
}
@@ -634,7 +580,12 @@ class _OrderStatusButtonState extends State<OrderStatusButton> {
if (next == null || _advancing) return;
_advancing = true;
try {
if (next == "PICKED") {
// The camera on the last rung is proof a parcel changed hands. A tenant
// whose riders drop pre-paid subscription meals does not photograph
// lunch boxes, so the step is skipped rather than made optional — an
// optional camera on a ten-drop run is ten dismissals.
// See [ServiceProfile.needsProofPhoto].
if (next == "PICKED" && ServiceProfile.active.needsProofPhoto) {
final XFile? photo = await ImagePicker().pickImage(
source: ImageSource.camera,
imageQuality: 50,
@@ -707,11 +658,7 @@ class _OrderStatusButtonState extends State<OrderStatusButton> {
),
],
),
child: Icon(
Icons.arrow_forward_rounded,
size: 18,
color: color,
),
child: Icon(LucideIcons.arrowRight, size: 18, color: color),
),
),
hint: Align(
@@ -722,7 +669,7 @@ class _OrderStatusButtonState extends State<OrderStatusButton> {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w600,
fontFamily: FontConstants.fontFamily,
letterSpacing: 0.2,
color: color,