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

@@ -19,6 +19,7 @@ import 'package:miler/Models/stop_status.dart';
import 'package:miler/data/order_manifest.dart';
import 'package:miler/data/stop_area.dart';
import 'package:miler/data/milk_run.dart';
import 'package:miler/data/pickup_locations.dart';
import 'package:miler/data/service_profile.dart';
/// ─────────────────────────────────────────────────────────────────────────
@@ -251,7 +252,11 @@ class TripCard extends StatelessWidget {
/// The collapsed trip brief: progress, time left, cash — and everything else
/// one tap away. See [TripBriefStrip] for what this used to be and why the
/// four surfaces it replaced were the wrong price for the information.
Widget buildSummary() => TripBriefStrip(
/// [onBrand] hands the brief over for Home's trip tab panel: no white panel
/// of its own, and its ink inverted for the brand red it lands on. See
/// [TripSelector].
Widget buildSummary({bool onBrand = false}) => TripBriefStrip(
onBrand: onBrand,
trip: trip,
// What the four figures describe: the collections still owed, measured
// from where the rider is standing — the same origin the page used to
@@ -423,11 +428,42 @@ class TripCard extends StatelessWidget {
///
/// A group whose orders have all left Home — collected, and now on the work
/// tab — emits nothing at all. Its heading would be a title over a gap.
/// Every stop is drawn as the **place** it is worked at, with its orders in a
/// list under it.
///
/// ── One shape for both lines ──
///
/// This was a flag, then a per-stop question, and it is now neither. The
/// history is worth keeping because each step was right about something:
///
/// 1. `ServiceProfile.sourceIsKitchen` — a milk round grouped its orders
/// under the counter they are loaded at; a logistics route grouped
/// nothing. The argument for the second half was that a parcel rider
/// "goes to one door for one consignment", so a header above a single
/// order would print the same name twice.
/// 2. Then deliveries were added to it: a booking the rider *hands over* was
/// loaded at the depot, and six drops drawing six nodes repeated one base
/// address six times.
///
/// The premise under step 1 was wrong, and the milk run is the proof. A place
/// holding one order does not print the same name twice: the header names
/// **where** — `VBK Complex, 11C-18, Ramasamy Rd` — and the row under it
/// names **who** — `Jai Hari Shankar P R`. Those are two different facts, and
/// on a collection the rider needs both: the address is what he rides to, the
/// customer is who he asks for when he gets there. Flattening them threw the
/// address away and titled the card with the person.
///
/// So every stop groups by its source now, and the shape of Home is one shape
/// on both lines. Two collections off one address gather, which they always
/// should have; a lone one is a place holding one order. The only stop still
/// drawn flat is one with nothing to name a place by at all — no counter and
/// no address — where there is genuinely no header to write. See the `flat`
/// rule below, which is unchanged.
@visibleForTesting
List<RouteGroup> routeGroups() {
final order = <String>[];
final buckets = <String, List<int>>{};
final grouped = ServiceProfile.active.sourceIsKitchen;
// Every stop is bucketed, not only the ones Home still draws: a group's bag
// numbers are counted against the whole group the rider was handed, so the
@@ -449,9 +485,23 @@ class TripCard extends StatelessWidget {
// is the same two-readers-one-question failure `sourceNameOf` documents,
// repeated on the id, and it splits a kitchen's orders across two groups
// on the screen while the collect still treats them as one.
final key = grouped
// ── One key for a milk round's counter, one for the whole depot ──
//
// A kitchen line has several counters in a day and the rider loads at
// each in turn, so its stops key on the counter they came off.
//
// A logistics day has one building. It assigns the day's bookings —
// collections, drops and both — hands over the route, and the rider
// accepts the lot before he leaves. So the run belongs to that building,
// and every stop keys to it however the individual booking is addressed.
// See [RiderHub].
//
// `base`, not the internal word for it: `no_hub_in_rider_copy_test`
// forbids that string in this layer and it is right to — a bucket key is
// one rename away from being a label.
final key = ServiceProfile.active.sourceIsKitchen
? MilkRun.sourceKeyOf(stop, within: trip.stops)
: 'stop:${_idOf(stop)}';
: 'base';
if (!buckets.containsKey(key)) {
buckets[key] = <int>[];
order.add(key);
@@ -522,22 +572,35 @@ class TripCard extends StatelessWidget {
// to name its place with — no counter name and no address. With an
// address it is a place holding one order, headed by where it is, with
// the order in the list under it.
// Every bucket is a place now, so the only stop still drawn flat is one
// with nothing to head it with — no counter name and no address. There
// is genuinely no header to write for that one.
final flat =
all.length == 1 &&
(!grouped || (stopSourceName(first).isEmpty && place.isEmpty));
all.length == 1 && stopSourceName(first).isEmpty && place.isEmpty;
// A grouped place is named by its counter, or failing that by where it
// is. A flat one *is* the stop, so it is named by the customer —
// `MilkRun.navigationLabel` answers "pickup" for a parcel that has not
// been collected yet, which is a description of the leg rather than a
// name the rider can read off a gate.
// ── The head card is named after the place, and the depot is a place ──
//
// On a logistics run that place is the rider's **hub**, not any one
// booking's address. Reading it off the stops titled the card with a
// customer's street — right for the drops, which are loaded at the
// depot, and wrong for the collections, which are not.
//
// It falls back through what the payload names and then the address, so
// a build with no locations table still draws the card it drew before
// rather than an untitled one.
final hub = ServiceProfile.active.sourceIsKitchen ? '' : RiderHub.name;
final name = flat
? OrderManifest.customerOf(first)
: (grouped
? (stopSourceName(first).trim().isNotEmpty
? stopSourceName(first)
: place)
: MilkRun.navigationLabel(first));
: (hub.isNotEmpty
? hub
: stopSourceName(first).trim().isNotEmpty
? stopSourceName(first)
: place);
return RouteGroup(
key: key,
// A route with no source names still groups — into one unnamed
@@ -559,7 +622,16 @@ class TripCard extends StatelessWidget {
// components that are identical on every stop of every route and
// would push the part that names the street off the end of the line.
// Not repeated when it is already the heading — see [name] above.
location: flat || name.trim() == place ? '' : place,
//
// ── And never a booking's street under the hub's name ──
//
// `place` is the first stop's address, which is the right subtitle
// for a counter the stops were collected from and quite wrong under
// a depot: it printed a customer's road under `H1`, which reads as
// the hub's address and is not. The locations table gives the hub a
// name and no street, so when the head is the hub the line is left
// empty rather than filled with something that looks like an answer.
location: flat || hub.isNotEmpty || name.trim() == place ? '' : place,
stops: stops,
allStops: all,
firstIndex: indices.first,
@@ -857,7 +929,7 @@ class TripCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.2,
color: ColorConstants.slateText,
fontFamily: FontConstants.fontFamily,
@@ -879,7 +951,7 @@ class TripCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.1,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
@@ -1008,7 +1080,7 @@ class TripCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: 0.7,
color: _hubColor,
fontFamily: FontConstants.fontFamily,
@@ -1101,7 +1173,7 @@ class TripCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: -0.2,
color: picked.isEmpty
? ColorConstants.secondaryText
@@ -1117,7 +1189,7 @@ class TripCard extends StatelessWidget {
all ? 'CLEAR' : 'ALL',
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: 0.6,
color: ColorConstants.primary,
fontFamily: FontConstants.fontFamily,
@@ -1379,7 +1451,7 @@ class TripCard extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
color: ColorConstants.acceptGreen,
fontFamily: FontConstants.fontFamily,
),
@@ -1429,7 +1501,7 @@ class TripCard extends StatelessWidget {
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11.5.sp,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
@@ -1477,7 +1549,7 @@ class _ViewRouteAction extends StatelessWidget {
'MAP',
style: TextStyle(
fontSize: 11.5.sp,
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
letterSpacing: 0.6,
color: ColorConstants.primary,
fontFamily: FontConstants.fontFamily,