import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:miler/data/pickup_locations.dart'; import 'package:miler/data/service_profile.dart'; import 'package:miler/views/Dashboard/home/trip.dart'; import 'package:miler/views/Dashboard/home/route_timeline.dart'; import 'package:miler/views/Dashboard/home/trip_card.dart'; /// ───────────────────────────────────────────────────────────────────────── /// THE HUB IS A PICKUP LOCATION, THE SAME WAY A KITCHEN IS /// /// A milk round draws its orders under the counter they are loaded at: one node /// naming the kitchen, its address under it, and the drops in a list beneath. /// A logistics route drew nothing of the kind — every stop was its own node — /// because grouping was one flag for the whole line /// ([ServiceProfile.sourceIsKitchen]). /// /// That is right for a *collection*. The parcel line's pickups are one /// consignment per door and each genuinely is its own place. It is wrong for a /// **delivery**: a booking the rider hands over was loaded at the depot, which /// is the same relationship a kitchen has to a milk round. Six drops drew six /// nodes repeating one base address, and the fact that mattered — these come /// off one shelf, and here is that shelf — was the one thing missing. /// /// So the question is asked per stop. A route that mixes gets both shapes. /// ───────────────────────────────────────────────────────────────────────── void main() { Map drop(String id, String who) => { 'orderid': id, 'pickupid': 'HUB', 'pickuplocationid': 'HUB', 'sourcename': 'Doormile Hub — Peelamedu', 'pickupaddress': '4, Avinashi Road, Peelamedu, Coimbatore 641004', 'pickupcustomer': who, 'orderstatus': 'pending', 'type': 'delivery', 'Quantity': '1', }; Map collect(String id, String who) => { 'orderid': id, 'pickupid': id, 'pickupcustomer': who, 'pickupaddress': 'RS Puram, Coimbatore', 'orderstatus': 'pending', 'type': 'pickup', 'Quantity': '1', }; setUp(() { PickupLocations.seedForTest(const {'7': 'H1 — Peelamedu Hub'}); RiderHub.resetForTest(7); }); tearDown(() { ServiceProfile.setActive(ServiceProfile.parcel); RiderHub.resetForTest(); PickupLocations.seedForTest(const {}); }); test('the whole logistics run is one node, named by the rider\'s hub', () { ServiceProfile.setActive(ServiceProfile.parcel); final groups = TripCard( trip: Trip.fromStops( id: 't1', stops: [drop('1', 'Joe'), drop('2', 'Arun'), drop('3', 'Ravi')], ), ).routeGroups(); expect(groups, hasLength(1), reason: 'one hub, one node'); // ── The hub, not a booking's address ── // // A logistics day starts and ends at one building: it assigns the day's // bookings, hands over the route, and the rider accepts the lot before he // leaves. Reading the head off the stops titled it with whichever address // sorted first — right for a drop, which is loaded at the depot, and wrong // for a collection, which is not. See [RiderHub]. expect(groups.single.name, 'H1 — Peelamedu Hub'); expect(groups.single.stops, hasLength(3)); expect( groups.single.flat, isFalse, reason: 'a place holding orders opens a list; it is not the stop itself', ); // No street under it. The locations table names the hub and gives no // address, and a booking's road printed there reads as the hub's own. expect(groups.single.location, isEmpty); }); test('a collection belongs to the hub run too', () { ServiceProfile.setActive(ServiceProfile.parcel); final groups = TripCard( trip: Trip.fromStops( id: 't1', stops: [collect('9', 'Sri Balaji Stores')], ), ).routeGroups(); expect(groups, hasLength(1)); // A collection is picked up at the customer's door, but it is still a // booking the hub handed this rider and it is accepted at the hub with the // rest. So it sits under the same head, with the customer on its own rung. expect(groups.single.flat, isFalse); expect(groups.single.name, 'H1 — Peelamedu Hub'); expect(groups.single.manifest.single.customer, 'Sri Balaji Stores'); }); test('a mixed route is still one hub run', () { ServiceProfile.setActive(ServiceProfile.parcel); final groups = TripCard( trip: Trip.fromStops( id: 't1', stops: [ drop('1', 'Joe'), drop('2', 'Arun'), collect('9', 'Sri Balaji Stores'), ], ), ).routeGroups(); // Drops and collections alike: one building assigned them, so one node // holds them and each rung says which job it is — see the `D`/`P`/`P&D` // tags in `route_timeline.dart`. expect(groups, hasLength(1)); expect(groups.single.name, 'H1 — Peelamedu Hub'); expect(groups.single.stops, hasLength(3)); }); test('a milk round is unchanged — it grouped on the line, and still does', () { ServiceProfile.setActive(ServiceProfile.milkMan); final groups = TripCard( trip: Trip.fromStops( id: 't1', stops: [ { 'orderid': '1', 'pickupid': 'K1', 'kitchenid': 'K1', 'kitchenname': 'Vidhya Kitchen', 'pickupaddress': '12, SNS Colony, Peelamedu', 'pickupcustomer': 'Joe Mathew', 'orderstatus': 'pending', }, { 'orderid': '2', 'pickupid': 'K1', 'kitchenid': 'K1', 'kitchenname': 'Vidhya Kitchen', 'pickupaddress': '12, SNS Colony, Peelamedu', 'pickupcustomer': 'Arun Prakash', 'orderstatus': 'pending', }, ], ), ).routeGroups(); expect(groups, hasLength(1)); expect(groups.single.name, 'Vidhya Kitchen'); expect(groups.single.stops, hasLength(2)); }); // ── The run-level select-all survives a mixed route ── // // `_flatSelectIds` bailed on `groups.every((g) => g.flat)`, which was right // while a route was one shape or the other. Grouping the drops made a // logistics route *both*, so the guard removed the only way to take the // collections in one go — on exactly the routes that have the most of them. group('taking the collections in one go', () { Future pump( WidgetTester tester, List> stops, { Set selected = const {}, }) async { tester.view.physicalSize = const Size(390 * 3, 900 * 3); tester.view.devicePixelRatio = 3.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( ScreenUtilInit( designSize: const Size(390, 844), builder: (_, _) => MaterialApp( home: Scaffold( body: SingleChildScrollView( child: TripCard( trip: Trip.fromStops(id: 't1', stops: stops), selectedIds: selected, onToggleSelect: (_) {}, onSelectAll: (_) {}, ).buildStops(), ), ), ), ), ); await tester.pumpAndSettle(); } testWidgets('a logistics run has one, and it is the hub\'s', ( tester, ) async { ServiceProfile.setActive(ServiceProfile.parcel); await pump( tester, [ drop('1', 'Joe'), drop('2', 'Arun'), collect('8', 'Sri Balaji Stores'), collect('9', 'Kumar Traders'), ], // The head control only appears once something is ticked — it is a // selection-mode helper, not a permanent row. selected: const {'8'}, ); // ── One control, over the whole run ── // // The rider accepts the day's bookings at the hub before he leaves it, // and every booking is under the hub's node — so there is one select-all // and it covers the lot. The run-level control that sits outside the // groups is for stops with no place above them to hold one, and a // logistics run has none. expect(find.byType(SelectAllAction), findsOneWidget); expect(find.text('1 of 4 selected'), findsOneWidget); }); testWidgets('a milk round is unchanged — it has no flat groups', ( tester, ) async { ServiceProfile.setActive(ServiceProfile.milkMan); await pump( tester, [ { 'orderid': '1', 'pickupid': 'K1', 'kitchenid': 'K1', 'kitchenname': 'Vidhya Kitchen', 'pickupaddress': '12, SNS Colony, Peelamedu', 'pickupcustomer': 'Joe Mathew', 'orderstatus': 'pending', }, { 'orderid': '2', 'pickupid': 'K1', 'kitchenid': 'K1', 'kitchenname': 'Vidhya Kitchen', 'pickupaddress': '12, SNS Colony, Peelamedu', 'pickupcustomer': 'Arun Prakash', 'orderstatus': 'pending', }, ], selected: const {'1'}, ); // Every group is a counter, so there is nothing for a run-level control // to be over: the select-all belongs to the kitchen and there is exactly // **one** of it. Before the fix this was also one, and it must stay one — // the guard that was removed never applied to a route with no flat groups. expect(find.byType(SelectAllAction), findsOneWidget); expect(find.text('1 of 2 selected'), findsOneWidget); }); }); }