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