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>
211 lines
8.3 KiB
Dart
211 lines
8.3 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'package:miler/Models/stop_status.dart';
|
|
import 'package:miler/data/accepted_store.dart';
|
|
import 'package:miler/data/service_profile.dart';
|
|
import 'package:miler/data/milk_run.dart';
|
|
import 'package:miler/data/work_domain.dart';
|
|
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// ONE ORDER, ONE SCREEN
|
|
///
|
|
/// ```
|
|
/// ASSIGNED ─ accept ─▶ ACCEPTED ─ go ─▶ ARRIVED ─ pick up ─▶ PICKED
|
|
/// └────────────────── HOME owns all of this ──────────────┘ │
|
|
/// pickup-complete │
|
|
/// ▼
|
|
/// DELIVERIES owns from here on
|
|
/// ```
|
|
///
|
|
/// [WorkBoundary] is the single function that decides which side a row is on.
|
|
/// Deliveries asked it; **Home never did** — it assigned the raw fetch to its
|
|
/// route list and let each card work itself out. So on a kitchen line, where
|
|
/// `isWorkComplete` is `delivered || cancelled`, a *picked* stop stayed in
|
|
/// Home's route while simultaneously sitting in Deliveries' queue: one bag, two
|
|
/// screens, two different next actions.
|
|
///
|
|
/// These hold the guarantee itself rather than either screen's filter — the
|
|
/// two lists are provably disjoint and provably exhaustive, so no future filter
|
|
/// can drift back apart from the other.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
|
|
|
Map<String, dynamic> stop(String id, String status) => {
|
|
'orderid': id,
|
|
'pickupid': int.parse(id),
|
|
'sourcename': 'Vidhya Kitchen',
|
|
'orderstatus': status,
|
|
};
|
|
|
|
/// The whole day, in every state a row can reach.
|
|
List<Map<String, dynamic>> day() => [
|
|
stop('1001', 'assigned'),
|
|
stop('1002', 'accepted'),
|
|
stop('1003', 'arrived'),
|
|
stop('1004', 'picked'),
|
|
stop('1005', 'outfordelivery'),
|
|
stop('1006', 'delivered'),
|
|
stop('1007', 'cancelled'),
|
|
stop('1008', 'skipped'),
|
|
];
|
|
|
|
group('the two lists never overlap', () {
|
|
for (final profile in [ServiceProfile.milkMan, ServiceProfile.parcel]) {
|
|
test('on ${profile.line.name}', () {
|
|
ServiceProfile.setActive(profile);
|
|
const accepted = {'1002', '1003'};
|
|
const collected = {'1004'};
|
|
|
|
final home = WorkBoundary.pickupQueue(
|
|
day(),
|
|
acceptedIds: accepted,
|
|
collectedIds: collected,
|
|
).map((s) => s['orderid']).toSet();
|
|
final deliveries = WorkBoundary.deliveryQueue(
|
|
day(),
|
|
acceptedIds: accepted,
|
|
collectedIds: collected,
|
|
).map((s) => s['orderid']).toSet();
|
|
|
|
expect(
|
|
home.intersection(deliveries),
|
|
isEmpty,
|
|
reason: 'an order on both screens is one bag with two next actions',
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
group('the boundary is pickup-complete, never acceptance', () {
|
|
setUp(() => ServiceProfile.setActive(ServiceProfile.milkMan));
|
|
|
|
test('an accepted booking is Home\'s, not delivery work', () {
|
|
// The one rule the delivery screens hang off: accepting is a decision
|
|
// about work still to be done, not evidence anything was collected.
|
|
// Reading it as a hand-over opened a map, an I'VE ARRIVED and a
|
|
// confirmation sheet for a pickup the rider had not made.
|
|
expect(
|
|
WorkBoundary.domainOf(stop('1002', 'accepted'), acceptedIds: {'1002'}),
|
|
WorkDomain.pickup,
|
|
);
|
|
});
|
|
|
|
test('an arrived booking is still Home\'s', () {
|
|
expect(
|
|
WorkBoundary.domainOf(stop('1003', 'arrived'), acceptedIds: {'1003'}),
|
|
WorkDomain.pickup,
|
|
);
|
|
});
|
|
|
|
test('the collected record moves it, and only after the call landed', () {
|
|
// `addCollectedOrderIds` is written by Home *after* pickup-complete comes
|
|
// back successful — never optimistically.
|
|
expect(
|
|
WorkBoundary.domainOf(stop('1004', 'accepted')),
|
|
WorkDomain.pickup,
|
|
);
|
|
expect(
|
|
WorkBoundary.domainOf(stop('1004', 'accepted'), collectedIds: {'1004'}),
|
|
WorkDomain.delivery,
|
|
);
|
|
});
|
|
|
|
test('the backend status moves it on its own', () {
|
|
for (final s in ['picked', 'outfordelivery']) {
|
|
expect(
|
|
WorkBoundary.domainOf(stop('1004', s)),
|
|
WorkDomain.delivery,
|
|
reason: '$s is past the counter',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('closed work belongs to neither', () {
|
|
test('a delivered milk-run stop leaves both screens', () {
|
|
ServiceProfile.setActive(ServiceProfile.milkMan);
|
|
expect(
|
|
WorkBoundary.domainOf(stop('1006', 'delivered')),
|
|
WorkDomain.closed,
|
|
);
|
|
});
|
|
|
|
test('a picked parcel booking is closed; a picked milk run is not', () {
|
|
// Collecting IS the job on logistics — the hub delivers it. On a round it
|
|
// is the middle of the morning.
|
|
ServiceProfile.setActive(ServiceProfile.parcel);
|
|
expect(WorkBoundary.domainOf(stop('1004', 'picked')), WorkDomain.closed);
|
|
|
|
ServiceProfile.setActive(ServiceProfile.milkMan);
|
|
expect(
|
|
WorkBoundary.domainOf(stop('1004', 'picked')),
|
|
WorkDomain.delivery,
|
|
);
|
|
});
|
|
});
|
|
|
|
_sourceNameContract();
|
|
|
|
group('the consignment id survives the pivot', () {
|
|
test('it is filed under the order id the door reads', () async {
|
|
// `updatePickedStatus` sent no `orderid`, so the mapper filed the
|
|
// consignment under the *booking* id while `closeDelivery` looked it up
|
|
// by **order** id. Every collected stop was stored under a key nothing
|
|
// would ask for, and pressing Delivered answered "this order was never
|
|
// picked up on the system" for a bag in the rider's hands.
|
|
await rememberConsignmentId('DM-BK-16DA518D-6381', '55123');
|
|
final ids = await getConsignmentIds();
|
|
expect(ids['DM-BK-16DA518D-6381'], '55123');
|
|
});
|
|
|
|
test('an empty id is never stored, so nothing can be fabricated', () async {
|
|
await rememberConsignmentId('DM-1', '');
|
|
await rememberConsignmentId('', '55124');
|
|
expect(await getConsignmentIds(), isEmpty);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// ONE READER FOR A PLACE'S NAME
|
|
///
|
|
/// `MilkRun.sourceNameOf` read `sourcename` / `kitchenname`; `stopSourceName`
|
|
/// read those *and* the CamelCase `SourceName` / `KitchenName` the payload
|
|
/// sometimes carries. A booking with only the capitalised key had a counter
|
|
/// according to one and none according to the other — and the route card used
|
|
/// both in one expression, so the group was not flat (there is a counter) but
|
|
/// was titled "pickup" (there is no counter to name).
|
|
///
|
|
/// On a device that is a foldable place headed by a leg description, holding
|
|
/// one order named after the same stop — exactly the shape the `flat` flag
|
|
/// exists to prevent.
|
|
void _sourceNameContract() {
|
|
group('a place has one name', () {
|
|
for (final key in const [
|
|
'sourcename',
|
|
'SourceName',
|
|
'kitchenname',
|
|
'KitchenName',
|
|
]) {
|
|
test('$key is read by both readers', () {
|
|
final stop = <String, dynamic>{'orderid': '1', key: 'Vidhya Kitchen'};
|
|
expect(MilkRun.sourceNameOf(stop), 'Vidhya Kitchen');
|
|
expect(stopSourceName(stop), 'Vidhya Kitchen');
|
|
expect(stopSourceName(stop), MilkRun.sourceNameOf(stop));
|
|
});
|
|
}
|
|
|
|
test('no key means no counter, from both', () {
|
|
const stop = <String, dynamic>{'orderid': '1'};
|
|
expect(MilkRun.sourceNameOf(stop), isEmpty);
|
|
expect(stopSourceName(stop), isEmpty);
|
|
});
|
|
});
|
|
}
|