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

@@ -8,6 +8,7 @@ import 'package:miler/data/service_profile.dart';
import 'package:miler/data/stop_area.dart';
import 'package:miler/views/Dashboard/home/stop_card.dart' show LiveMark;
import 'package:miler/views/Dashboard/home/trip.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
import 'package:miler/views/Dashboard/pickups/route_metrics.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/views/helpers/constants/design_constants.dart';
@@ -209,9 +210,7 @@ class RouteGroup {
if (allStopStates.length != allStops.length) {
return allStops.isNotEmpty ? allStops.length : stops.length;
}
final carried = allStopStates
.where((s) => s == StopState.collected)
.length;
final carried = allStopStates.where((s) => s == StopState.collected).length;
return stops.length + carried;
}
@@ -495,14 +494,32 @@ class _RouteTimelineState extends State<RouteTimeline> {
}
/// Every order on the run the rider may tick right now, across the flat
/// groups — a parcel route, where each stop is its own place.
/// groups — the stops that are their own place.
///
/// One rung only, and only when there are at least two: with one stop its own
/// row already is the select-all.
///
/// ── Why this is not "every group is flat" ──
///
/// It was, and the guard was right while a route was one shape or the other:
/// a parcel run of single-stop nodes got a run-level select-all, a kitchen
/// run got one per counter, and neither had the other's.
///
/// A logistics route is now **both** — its drops gather under the hub they
/// were loaded at, its collections stay their own doors — and `every` made
/// the whole control vanish the moment one group was not flat. The rider lost
/// the only way to take his collections in one go, on exactly the routes that
/// have the most of them.
///
/// So it gathers from the flat groups and ignores the rest. A grouped place
/// already carries its own select-all *inside* it, over its own orders, which
/// is the control that belongs to it; this one is for the stops that have no
/// place above them to hold it. A run with no flat groups at all — a milk
/// round — still gets nothing here, exactly as before.
List<String> get _flatSelectIds {
if (!widget.groups.every((g) => g.flat)) return const [];
final ids = <String>[];
for (final g in widget.groups) {
if (!g.flat) continue;
if (g.state != StopState.pending) continue;
for (final (i, s) in g.stops.indexed) {
if (g.stopStates[i] != StopState.pending) continue;
@@ -783,7 +800,7 @@ class SelectAllAction extends StatelessWidget {
overflow: TextOverflow.ellipsis,
textAlign: scoped ? TextAlign.right : TextAlign.left,
style: MilerType.label.copyWith(
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
color: accent,
),
),
@@ -1006,7 +1023,7 @@ class PickupTimelineGroup extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: MilerType.label.copyWith(
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
color: ColorConstants.primary,
),
),
@@ -1488,7 +1505,7 @@ class PickupTimelineGroup extends StatelessWidget {
? MilerType.cardTitle
: MilerType.body)
.copyWith(
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w800,
color: group.isDone || rejected
? ColorConstants.secondaryText
: ColorConstants.slateText,
@@ -1571,7 +1588,7 @@ class PickupTimelineGroup extends StatelessWidget {
// pale grey reserved for things nobody has
// to act on.
style: MilerType.label.copyWith(
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
color: chosen > 0
? ColorConstants.primary
: ColorConstants.onSurfaceVariant,
@@ -1608,7 +1625,18 @@ class PickupTimelineGroup extends StatelessWidget {
// the distances line up on one right edge, so "which is
// nearest" is answered by looking down a column rather
// than reading five blocks.
// ── And on a flat stop, which has no rail under it ──
//
// A place with one order draws no list, so there is no
// rung for [_KindTag] to sit on — and a logistics route
// is mostly flat stops, which is exactly where "am I
// collecting or dropping here" is asked. The tag goes on
// the header instead, in the same corner and at the same
// size, so the answer is in one place whichever shape the
// route happens to take.
if (group.flat) ...[
if (group.stops.isNotEmpty)
_KindTag(kind: stopKindOf(group.stops.first)),
SizedBox(width: 4.w),
Icon(
LucideIcons.chevronRight,
@@ -2151,8 +2179,8 @@ class PickupTimelineStep extends StatelessWidget {
style: MilerType.body.copyWith(
fontSize: 15.sp,
fontWeight: selected || accepted
? FontWeight.w600
: FontWeight.w500,
? FontWeight.w700
: FontWeight.w600,
color: rejected
? ColorConstants.secondaryText
: selected || accepted
@@ -2190,6 +2218,11 @@ class PickupTimelineStep extends StatelessWidget {
// [OrderManifest.labelFor].
label: rejected ? 'Rejected' : line.label,
orderId: line.orderId,
// What the rider is doing at this door — see
// [_KindTag]. Read from the stop itself, so a route
// that mixes collections and drops says which is which
// on every rung.
kind: stopKindOf(line.stop),
selected: selected,
onTap: canOpen ? onOpen : null,
),
@@ -2228,12 +2261,17 @@ class _OrderOpenBlock extends StatelessWidget {
final String label;
final String orderId;
/// Collection, drop, or both — stamped beside the chevron.
final StopKind kind;
final bool selected;
final VoidCallback? onTap;
const _OrderOpenBlock({
required this.label,
required this.orderId,
required this.kind,
required this.selected,
required this.onTap,
});
@@ -2310,8 +2348,8 @@ class _OrderOpenBlock extends StatelessWidget {
style: MilerType.label.copyWith(
fontSize: 12.sp,
fontWeight: selected
? FontWeight.w700
: FontWeight.w600,
? FontWeight.w800
: FontWeight.w700,
color: selected
? ColorConstants.primary
: ColorConstants.secondaryText,
@@ -2327,12 +2365,22 @@ class _OrderOpenBlock extends StatelessWidget {
// the list. The id is on the detail sheet this block opens, and
// on the Activity record — both places the rare question is
// actually asked.
if (onTap != null)
// ── What he is doing here, in one or two characters ──
//
// A mixed route runs collections and drops down the same rail,
// and until now the only way to tell one rung from another was
// to open it. The tag sits inside the block the chevron closes,
// so the row reads `name · area · P ›` — identity, place, job,
// and the way in.
_KindTag(kind: kind),
if (onTap != null) ...[
SizedBox(width: 4.w),
Icon(
LucideIcons.chevronRight,
size: 18.sp,
color: ColorConstants.borderStrong,
),
],
],
),
),
@@ -2342,6 +2390,60 @@ class _OrderOpenBlock extends StatelessWidget {
}
}
/// `P`, `D` or `P&D` on a rail rung.
///
/// ── A mark, not a word ──
///
/// The full names are [StopKindUi.badgeLabel], and they are what the stop's own
/// card and sheet carry — there is room there, and the rider is reading one
/// stop. The rail is a *column*: he runs down it deciding which doors are
/// collections and which are drops, and `PICKUP & DELIVERY` at a size that fits
/// beside a chevron is a word he has to read on every rung. One or two
/// characters is a mark he recognises. See [StopKindUi.shortLabel].
///
/// ── And it is grey ──
///
/// The obvious version tints it with the kind's own accent — collections one
/// colour, drops another. `stop_card_neutral_test.dart` forbids exactly that,
/// and the reason survives this tag: the route carries **one** accent, the
/// brand, on the live node and the journey action, and a per-stop hue down the
/// rail competes with the one mark that says *where you are*. Three hues on
/// every rung is a second colour system on the screen that most needs one.
///
/// What was asked for here is the *information* — which of these is a
/// collection and which is a drop — and a letter carries that on its own. So
/// the tag is the same quiet grey as the metric tiles, and the accents stay
/// where that test put them: on the stop's own sheet, one tap away, where the
/// rider is reading one stop instead of scanning forty.
class _KindTag extends StatelessWidget {
final StopKind kind;
const _KindTag({required this.kind});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(left: 6.w),
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: ColorConstants.tint(ColorConstants.slateText, 0.055),
borderRadius: BorderRadius.circular(DesignConstants.radiusFull),
),
child: Text(
kind.shortLabel,
maxLines: 1,
style: MilerType.label.copyWith(
fontSize: 10.5.sp,
fontWeight: FontWeight.w900,
letterSpacing: 0.2,
height: 1.1,
color: ColorConstants.secondaryText,
),
),
);
}
}
/// The order id, as every screen reads it.
String stopIdOf(Map<String, dynamic> stop) =>
(stop['orderid'] ?? '').toString();
@@ -2893,7 +2995,7 @@ class _JourneyColumn extends StatelessWidget {
// it: same column, one step down in size and weight.
style: MilerType.label.copyWith(
fontSize: prominent ? 13.sp : 12.5.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
color: ColorConstants.secondaryText,
),
),
@@ -2981,7 +3083,7 @@ class _StateWord extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: MilerType.label.copyWith(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
color: color,
),
),
@@ -3182,7 +3284,7 @@ class _TextAction extends StatelessWidget {
Text(
label,
style: MilerType.label.copyWith(
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
color: ColorConstants.primary,
),
),