miler map

This commit is contained in:
2026-09-09 12:55:23 +05:30
parent 074cc0eccf
commit 127fa062ed
143 changed files with 4315 additions and 2291 deletions

View File

@@ -0,0 +1,269 @@
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<String, dynamic> 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<String, dynamic> 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<void> pump(
WidgetTester tester,
List<Map<String, dynamic>> stops, {
Set<String> 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);
});
});
}