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

231
test/service_card_test.dart Normal file
View File

@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/data/service_profile.dart';
import 'package:miler/views/Dashboard/home/trip.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/views/Dashboard/home/trip_card.dart';
import 'package:miler/views/helpers/widgets/app_widgets.dart';
/// P2 — what a stop card says on a service tenant.
///
/// The three facts under test are the three a rider reads before he moves: what
/// kind of work this is, where the order has got to, and which bag out of ten in
/// his box belongs at this door.
void main() {
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
group('the work-type chip', () {
test('Doormile still names each stop kind separately', () {
ServiceProfile.setActive(ServiceProfile.parcel);
expect(StopKind.pickup.chipLabel, 'PICKUP');
expect(StopKind.delivery.chipLabel, 'DELIVERY');
expect(StopKind.combined.chipLabel, 'P&D');
expect(StopKind.pickup.chipColor, ColorConstants.pickupChip);
});
test('a milk run wears one mark, and it is not the line’s name', () {
// ── This assertion changed with product direction ──
//
// It used to require the word MILK. That is the app's internal name for
// a *service profile* — the thing that decides which capabilities exist
// and whose records belong to whom — and it was being printed on the
// rider's cards and on his Activity record as `Type: MILK`. He did not
// choose it, cannot change it and gains nothing from reading it.
//
// What has NOT changed is the property the mark exists for: on this line
// every stop wears the *same* mark, because the rider handles a kitchen
// crate and a subscriber's doorstep identically and a chip that flipped
// between them would imply a difference in handling that does not exist.
ServiceProfile.setActive(ServiceProfile.meals);
for (final kind in StopKind.values) {
expect(kind.chipLabel, 'DELIVERY', reason: '$kind');
expect(kind.badgeLabel, 'DELIVERY', reason: '$kind');
expect(kind.chipColor, ColorConstants.serviceChip, reason: '$kind');
}
// Sentence case where the type is stated as a label rather than stamped.
expect(StopKind.delivery.titleLabel, 'Delivery');
// And the internal name reaches no screen at all.
for (final kind in StopKind.values) {
expect(kind.chipLabel.toUpperCase(), isNot(contains('MILK')));
expect(kind.titleLabel.toUpperCase(), isNot(contains('MILK')));
}
});
test('the stop keeps its real kind underneath the one label', () {
ServiceProfile.setActive(ServiceProfile.meals);
// P3's collection gate depends on this still being true: a kitchen stop
// is a pickup and a subscriber drop is a delivery, whatever the chip says.
expect(stopKindOf({'type': 'delivery'}), StopKind.delivery);
expect(stopKindOf({'type': 'pickup'}), StopKind.pickup);
});
});
group('the status tag', () {
test('a parcel route names a pending stop as a decision it awaits', () {
ServiceProfile.setActive(ServiceProfile.parcel);
expect(StopState.pending.label, 'Awaiting your decision');
expect(StopState.done.label, 'Completed');
});
test('a meal rider is told the state, not asked for a decision', () {
ServiceProfile.setActive(ServiceProfile.meals);
// He accepted the whole assignment at once — there is no per-stop
// decision left to await.
expect(StopState.pending.label, 'Pending');
expect(StopState.active.label, 'Out for delivery');
expect(StopState.done.label, 'Delivered');
expect(StopState.skipped.label, 'Skipped');
});
});
group('source and bag label', () {
test('read the spellings the backend might send', () {
expect(stopSourceName({'kitchenname': 'Kitchen 1'}), 'Kitchen 1');
expect(stopSourceName({'SourceName': ' Anna Nagar '}), 'Anna Nagar');
expect(stopSourceId({'kitchenid': 'K-77'}), 'K-77');
expect(stopBagLabel({'baglabel': 'DG-1042'}), 'DG-1042');
expect(stopBagLabel({'labelcode': 'DG-9'}), 'DG-9');
});
test(
'absent on a parcel stop rather than empty-stringed onto the card',
() {
expect(stopSourceName({'orderid': '7'}), '');
expect(stopBagLabel({'orderid': '7'}), '');
},
);
});
group('StopSourceLine', () {
Future<void> pump(WidgetTester tester, String source, String bag) async {
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: StopSourceLine(source: source, bagLabel: bag),
),
),
),
);
await tester.pumpAndSettle();
}
testWidgets('takes no space at all on a stop with neither', (tester) async {
await pump(tester, '', '');
// A Doormile card must be exactly what it was — not what it was plus an
// empty row's padding.
final size = tester.getSize(find.byType(StopSourceLine));
expect(size.height, 0);
});
testWidgets('shows both when the stop carries both', (tester) async {
await pump(tester, 'Kitchen 1', 'DG-1042');
expect(find.text('Kitchen 1'), findsOneWidget);
expect(find.text('Bag DG-1042'), findsOneWidget);
});
testWidgets('shows the bag alone when there is no source', (tester) async {
await pump(tester, '', 'DG-1042');
expect(find.text('Bag DG-1042'), findsOneWidget);
expect(
tester.getSize(find.byType(StopSourceLine)).height,
greaterThan(0),
);
});
});
group('the stop row on a service tenant', () {
/// [withSource] mirrors the payload difference between the two tenants: a
/// parcel booking has no kitchen and no bag label to send, which is why a
/// Doormile card is unchanged by P2 — the line is driven by the data, not
/// by who the rider works for.
Map<String, dynamic> stop({bool withSource = true}) => {
'orderid': '1',
'step': 1,
'customername': 'Priya Sundaram',
'pickupaddress': '2B Lakshmi Flats, 4th Main Road, Anna Nagar East',
'status': 'ASSIGNED',
'type': 'delivery',
'quantity': 1,
if (withSource) 'kitchenname': 'Kitchen 1',
if (withSource) 'baglabel': 'DG-1042',
'pickuplat': 13.08,
'pickuplon': 80.21,
};
Future<void> pumpRoute(
WidgetTester tester, {
bool withSource = true,
}) async {
tester.view.physicalSize = const Size(1080, 1800);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final trip = Trip.fromStops(
id: 't1',
stops: [stop(withSource: withSource)],
);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TripCard(trip: trip).buildStops(),
),
),
),
),
);
await tester.pumpAndSettle();
}
// ── The row's marks moved up to the place ──
//
// A card carried a work-type chip and a state chip of its own, and the
// truncation test above was about the two of them sharing one line. Neither
// is on a timeline row: the type is progressive disclosure in the detail
// sheet, and the *state* belongs to the place rather than to each order
// under it — five orders off one counter are all on the same rung, so five
// copies of the same chip said nothing five times.
//
// What survives is the half that was always the contract: the kitchen is
// named once, and the bag the order travels in is on the row.
testWidgets('the kitchen is named once and the bag rides on the row', (
tester,
) async {
ServiceProfile.setActive(ServiceProfile.meals);
await pumpRoute(tester);
// Named once, by the place — not again on every order under it.
expect(find.text('Kitchen 1'), findsOneWidget);
// The label the kitchen printed, straight from [BagManifest]. The `Bag `
// prefix belonged to the card's chip and went with it, which stopped a
// printed `DG-1042` reading as `Bag DG-1042`.
expect(find.text('DG-1042'), findsOneWidget);
// The count the rider checks the shelf against.
expect(find.text('1 bag'), findsOneWidget);
});
testWidgets('a parcel route keeps a bare pending row', (tester) async {
ServiceProfile.setActive(ServiceProfile.parcel);
await pumpRoute(tester, withSource: false);
// The row underneath carries the decision, so a chip would be a caption
// on a control.
expect(find.text('Awaiting your decision'), findsNothing);
// And nothing from the meal run leaks onto a parcel route: no counter to
// group under, so the stop is its own node and no bag arithmetic applies.
expect(find.text('Kitchen 1'), findsNothing);
expect(find.text('MILK'), findsNothing);
expect(find.text('1 order · 1 bag'), findsNothing);
expect(find.text('Priya Sundaram'), findsOneWidget);
});
});
}