production
This commit is contained in:
@@ -121,6 +121,14 @@ class RouteGroup {
|
||||
/// The rung every choosable order in this group is on.
|
||||
final StopState state;
|
||||
|
||||
/// Each of [allStops]' own state, in the same order.
|
||||
///
|
||||
/// The visible half is [stopStates]; this is the whole bucket, and it is what
|
||||
/// lets [groupSize] tell an order that is still this counter's business from
|
||||
/// one that has finished and left it. Empty when a caller does not track it,
|
||||
/// in which case [groupSize] falls back to the raw count.
|
||||
final List<StopState> allStopStates;
|
||||
|
||||
/// Each of [stops]' own state, in the same order.
|
||||
///
|
||||
/// A group's rung is the rung it is *worked* on, and a group can hold orders
|
||||
@@ -150,7 +158,42 @@ class RouteGroup {
|
||||
/// number again.
|
||||
int get onRungCount => stopStates.where((s) => s == state).length;
|
||||
|
||||
int get groupSize => allStops.isNotEmpty ? allStops.length : stops.length;
|
||||
/// The denominator of `Accepted 1/4` — **this counter's load**.
|
||||
///
|
||||
/// ── Why it is not `allStops.length` ──
|
||||
///
|
||||
/// It was, and the reasoning was sound as far as it went: an order the rider
|
||||
/// has collected leaves the visible list but did not leave the counter, so
|
||||
/// counting only what is on screen would renumber the place under him.
|
||||
///
|
||||
/// What that missed is that [allStops] is every order the trip ever bucketed
|
||||
/// under this source — including the ones already **delivered**, which are
|
||||
/// Activity's business and not this card's, and the ones he **declined**,
|
||||
/// which he has finished with. On a full day off one kitchen that is most of
|
||||
/// them: a rider with ten orders in the dropdown and one accepted read
|
||||
/// `Accepted 1/63`, where 63 was the whole day and 10 was the question in
|
||||
/// front of him. A fraction whose denominator the rider cannot see anywhere
|
||||
/// on the screen is worse than no fraction.
|
||||
///
|
||||
/// So the load is stated the way he would state it: **what is still in the
|
||||
/// dropdown, plus what he has already taken off this counter**. Collected
|
||||
/// orders keep their place in the arithmetic, which was the original point;
|
||||
/// finished and declined ones drop out of it, which was the bug.
|
||||
///
|
||||
/// Skipped orders are out too. One is still owed at the counter, but it is
|
||||
/// parked for a return visit and it is not part of the accept decision this
|
||||
/// chip is a fraction of — see [onRungCount], which counts the accepted rung.
|
||||
int get groupSize {
|
||||
// A caller that does not supply the states gets the old arithmetic rather
|
||||
// than a wrong number: better to over-count than to invent a denominator.
|
||||
if (allStopStates.length != allStops.length) {
|
||||
return allStops.isNotEmpty ? allStops.length : stops.length;
|
||||
}
|
||||
final carried = allStopStates
|
||||
.where((s) => s == StopState.collected)
|
||||
.length;
|
||||
return stops.length + carried;
|
||||
}
|
||||
|
||||
/// True when the header **is** the stop: one order, at one address, with no
|
||||
/// place above it worth naming.
|
||||
@@ -169,6 +212,7 @@ class RouteGroup {
|
||||
required this.firstIndex,
|
||||
required this.state,
|
||||
required this.stopStates,
|
||||
this.allStopStates = const [],
|
||||
this.travel = Duration.zero,
|
||||
this.meters,
|
||||
this.flat = false,
|
||||
@@ -245,7 +289,7 @@ class RouteGroup {
|
||||
bool get isDone => state == StopState.collected || state == StopState.done;
|
||||
|
||||
/// The rider is on this stop right now.
|
||||
bool get isLive => state == StopState.active;
|
||||
bool get isLive => state.isAtSource;
|
||||
}
|
||||
|
||||
/// The whole run: a head, then one expandable group per pickup place.
|
||||
@@ -270,10 +314,24 @@ class RouteTimeline extends StatefulWidget {
|
||||
/// Toggles one order's tick.
|
||||
final ValueChanged<String>? onToggleSelect;
|
||||
|
||||
/// Opens one order's detail sheet. Progressive disclosure for the address,
|
||||
/// the phone and everything else a step deliberately does not carry.
|
||||
/// Opens a **place's** detail sheet — a flat group's header, where one stop
|
||||
/// is one location. Progressive disclosure for the address, the phone and
|
||||
/// everything else a step deliberately does not carry.
|
||||
final ValueChanged<Map<String, dynamic>>? onStopTap;
|
||||
|
||||
/// Opens **one order's** detail sheet, from a row inside an expanded place.
|
||||
///
|
||||
/// Separate from [onStopTap] because they are about different subjects, and
|
||||
/// the sheet has to know which. Every order under a kitchen shares that
|
||||
/// kitchen's address, so a sheet opened from an order row that pins the
|
||||
/// kitchen draws the same map once per order and answers nothing about the
|
||||
/// one the rider tapped — what distinguishes it is the door it is going to.
|
||||
/// The place's own map is on the card's preview, beside the call.
|
||||
///
|
||||
/// Falls back to [onStopTap] when unset, so a caller that does not care keeps
|
||||
/// the old single-callback behaviour.
|
||||
final ValueChanged<Map<String, dynamic>>? onOrderTap;
|
||||
|
||||
/// Opens navigation for a whole group. **One per group, never per order** —
|
||||
/// five Navigate buttons that go to the same counter was the single largest
|
||||
/// source of clutter on the old screen.
|
||||
@@ -311,6 +369,7 @@ class RouteTimeline extends StatefulWidget {
|
||||
this.onSelectAll,
|
||||
this.onToggleSelect,
|
||||
this.onStopTap,
|
||||
this.onOrderTap,
|
||||
this.onNavigateGroup,
|
||||
this.onCallGroup,
|
||||
this.onPreviewGroup,
|
||||
@@ -351,6 +410,7 @@ class _RouteTimelineState extends State<RouteTimeline> {
|
||||
/// would put him back on a screen he has to scroll.
|
||||
String? get _defaultOpenKey {
|
||||
for (final rung in const [
|
||||
StopState.arrived,
|
||||
StopState.active,
|
||||
StopState.accepted,
|
||||
StopState.pending,
|
||||
@@ -402,6 +462,7 @@ class _RouteTimelineState extends State<RouteTimeline> {
|
||||
onSelectAll: widget.onSelectAll,
|
||||
onToggleSelect: widget.onToggleSelect,
|
||||
onStopTap: widget.onStopTap,
|
||||
onOrderTap: widget.onOrderTap ?? widget.onStopTap,
|
||||
onNavigate: widget.onNavigateGroup,
|
||||
onCall: widget.onCallGroup,
|
||||
onPreview: widget.onPreviewGroup,
|
||||
@@ -641,7 +702,7 @@ class SelectAllAction extends StatelessWidget {
|
||||
// [PickupTimelineGroup._railWidth] — so matching the number instead
|
||||
// of the axis left a 2pt drift. The axis is the thing being matched;
|
||||
// `home_grid_test.dart` measures it so the two cannot part again.
|
||||
padding: EdgeInsets.only(left: scoped ? 10.w : 0),
|
||||
padding: EdgeInsets.only(right: scoped ? 2.w : 0),
|
||||
child: Row(
|
||||
// ── Inside a panel it closes the row; at the run head it opens it ──
|
||||
//
|
||||
@@ -653,20 +714,21 @@ class SelectAllAction extends StatelessWidget {
|
||||
//
|
||||
// Unscoped — the run head — it is the first thing on the screen
|
||||
// above the route and keeps the reading edge.
|
||||
// ── Both versions read from the left ──
|
||||
// ── The scoped one closes the row; the run head opens it ──
|
||||
//
|
||||
// The scoped one was pushed to the right edge, on the argument
|
||||
// that a control with nothing beside it should sit on an edge and
|
||||
// that a list's own controls live on the right. That was an
|
||||
// argument about a *panel* — it had a right edge to sit on, and
|
||||
// the orders were inside it.
|
||||
// Inside a group the select-all is a *control over a list*, and a
|
||||
// list's own controls sit on the right — the same edge the bag
|
||||
// labels and the chevrons below it already use. On the left it
|
||||
// headed the list like a label, which is what made it read as a
|
||||
// stray tick above the first name rather than as the thing that
|
||||
// ticks all five.
|
||||
//
|
||||
// On the open ground there is no panel, and the right edge now
|
||||
// belongs to the bag column. A select-all floating out there reads
|
||||
// as a fourth thing in the Bag axis. It heads the list it selects,
|
||||
// so it starts where the list starts: the same left axis the
|
||||
// customer names below it keep.
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
// The run-head version has no list beside it and no right edge to
|
||||
// close: it is the first thing above the route, so it keeps the
|
||||
// reading edge.
|
||||
mainAxisAlignment: scoped
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
// ── One rail, not two ──
|
||||
//
|
||||
@@ -680,16 +742,25 @@ class SelectAllAction extends StatelessWidget {
|
||||
// the same arrangement every other railed row on this screen
|
||||
// uses. `home_grid_test.dart` measures the axis so the pair
|
||||
// cannot drift apart again.
|
||||
// The mark is drawn by the rail on a scoped row — see
|
||||
// [PickupTimelineGroup._selectAllMark] — so only the run-head
|
||||
// version, which has no rail, carries it inline.
|
||||
if (!scoped) ...[_glyph(all, some, accent), SizedBox(width: 8.w)],
|
||||
// ── The mark goes where the words went ──
|
||||
//
|
||||
// The scoped row's tick was drawn by the rail, in the node
|
||||
// column on the far left, while the label it belongs to sits on
|
||||
// the right edge. A control split across the full width of a
|
||||
// card is two controls as far as the eye is concerned: the tick
|
||||
// read as a mark on the spine and the words as a caption, and
|
||||
// nothing said they were one thing you press.
|
||||
//
|
||||
// Both versions carry it inline now, immediately before the
|
||||
// words, and the rail draws nothing for this row.
|
||||
_glyph(all, some, accent),
|
||||
SizedBox(width: 8.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
textAlign: scoped ? TextAlign.right : TextAlign.left,
|
||||
style: MilerType.label.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: accent,
|
||||
@@ -733,6 +804,9 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
final ValueChanged<List<String>>? onSelectAll;
|
||||
final ValueChanged<String>? onToggleSelect;
|
||||
final ValueChanged<Map<String, dynamic>>? onStopTap;
|
||||
|
||||
/// One order's sheet — see [RouteTimeline.onOrderTap].
|
||||
final ValueChanged<Map<String, dynamic>>? onOrderTap;
|
||||
final ValueChanged<Map<String, dynamic>>? onNavigate;
|
||||
final ValueChanged<Map<String, dynamic>>? onCall;
|
||||
final ValueChanged<RouteGroup>? onPreview;
|
||||
@@ -753,6 +827,7 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
this.onSelectAll,
|
||||
this.onToggleSelect,
|
||||
this.onStopTap,
|
||||
this.onOrderTap,
|
||||
this.onNavigate,
|
||||
this.onCall,
|
||||
this.onPreview,
|
||||
@@ -777,7 +852,7 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
static bool _isChoosable(StopState state) =>
|
||||
state == StopState.pending ||
|
||||
(ServiceProfile.active.handsOffAtCollection &&
|
||||
(state == StopState.accepted || state == StopState.active));
|
||||
(state == StopState.accepted || state.isAtSource));
|
||||
|
||||
/// The one group the rider is working, given a ground of its own.
|
||||
///
|
||||
@@ -833,7 +908,7 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
// daylight ground. What used to be conveyed by an edge is conveyed by the
|
||||
// node the header hangs off and by the indent the orders keep to.
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 24.h),
|
||||
padding: EdgeInsets.only(bottom: 12.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1023,7 +1098,7 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
return Padding(
|
||||
// Zero on the right: the utilities finish flush with the card's own edge.
|
||||
// This is the limit — the next point out is outside the card.
|
||||
padding: EdgeInsets.fromLTRB(_railWidth, 0, 0, 12.h),
|
||||
padding: EdgeInsets.fromLTRB(_railWidth, 0, 0, 0),
|
||||
child: wide
|
||||
? Row(
|
||||
children: [
|
||||
@@ -1315,7 +1390,7 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
// flush instead: a disc's silhouette curves away from the
|
||||
// margin, so it needs to overshoot a text edge by a few points
|
||||
// to look level with it. Optical alignment, not metric.
|
||||
padding: EdgeInsets.fromLTRB(2.w, 14.h, 6.w, 12.h),
|
||||
padding: EdgeInsets.fromLTRB(2.w, 8.h, 6.w, 8.h),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -1448,6 +1523,14 @@ 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.
|
||||
if (group.flat) ...[
|
||||
SizedBox(width: 4.w),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 20.sp,
|
||||
color: ColorConstants.borderStrong,
|
||||
),
|
||||
],
|
||||
if (showJourney) ...[
|
||||
SizedBox(width: 6.w),
|
||||
_JourneyColumn(
|
||||
@@ -1460,43 +1543,26 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
prominent: isCurrent,
|
||||
),
|
||||
],
|
||||
// ── The disclosure says it is one ──
|
||||
// ── The chevron is gone; the journey takes the corner ──
|
||||
//
|
||||
// The chevron was removed when the header became the
|
||||
// toggle, and its *signal* went with it: a collapsed
|
||||
// It came back once as an *indicator* — a passive glyph
|
||||
// inside the header's own InkWell — because a collapsed
|
||||
// place showed nothing that said "there is a list under
|
||||
// me", so the orders were discoverable only by accident
|
||||
// — on the control the rider uses more than any other
|
||||
// on this screen.
|
||||
// me". That argument held while the header's right edge
|
||||
// was empty.
|
||||
//
|
||||
// It returns as an INDICATOR, not a target. The old bug
|
||||
// was two tap arenas over one pixel; this glyph is
|
||||
// passive inside the header's own InkWell, so there is
|
||||
// exactly one gesture and it owns the whole card. It
|
||||
// rotates with the state (the app's state duration, no
|
||||
// bounce), and a flat stop — whose tap pushes the stop
|
||||
// detail instead — wears the pointing variant, the same
|
||||
// vocabulary every row of Account uses for "this goes
|
||||
// somewhere".
|
||||
SizedBox(width: 4.w),
|
||||
group.flat
|
||||
? Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 20.sp,
|
||||
color: ColorConstants.borderStrong,
|
||||
)
|
||||
: AnimatedRotation(
|
||||
turns: expanded ? 0.5 : 0,
|
||||
duration: DesignConstants.motionState,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: Icon(
|
||||
LucideIcons.chevronDown,
|
||||
size: 20.sp,
|
||||
color: expanded
|
||||
? ColorConstants.secondaryText
|
||||
: ColorConstants.borderStrong,
|
||||
),
|
||||
),
|
||||
// It is not empty: the distance and the ride time live
|
||||
// there, and they are the reason a rider looks at this
|
||||
// corner in the first place. The chevron was standing
|
||||
// between them and the card's edge, so the one column he
|
||||
// scans down a multi-kitchen run — *which of these is
|
||||
// nearest* — was pushed 24 points off the right margin
|
||||
// and no longer lined up with anything.
|
||||
//
|
||||
// What the chevron was signalling is still signalled: an
|
||||
// open group is visibly open (its orders are on screen,
|
||||
// indented under it), and a flat stop is the one case
|
||||
// with nothing to open, so it keeps a pointing glyph.
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1604,6 +1670,9 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
// breathing chip among outlined ones — so it survives sunlight and a
|
||||
// grayscale screen. The stop under way used to be hidden from Home
|
||||
// entirely and announced by a red banner over the foot of the page.
|
||||
// Both rungs take the live mark — from this rail's distance the useful
|
||||
// fact is *this is the one he is on* — and each says which rung it is.
|
||||
StopState.arrived => const LiveMark(label: 'Arrived'),
|
||||
StopState.active => const LiveMark(),
|
||||
StopState.collected || StopState.done => const _StateWord(
|
||||
label: 'Collected',
|
||||
@@ -1648,91 +1717,34 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
|
||||
Widget _body(BuildContext context, List<BagLine> manifest, int chosen) {
|
||||
final ids = _choosableIds;
|
||||
|
||||
final selectable = _isChoosable(group.state) && onToggleSelect != null;
|
||||
|
||||
// ── The orders are a panel *inside* the card ──
|
||||
// ── The orders are the kitchen's contents, and sit inside it ──
|
||||
//
|
||||
// The ground ran the full width of the card and squared off against its
|
||||
// edges, so it read as a band stuck to the bottom of the header rather than
|
||||
// as the card's contents — the orders looked like they had spilled out of
|
||||
// the thing that owns them.
|
||||
//
|
||||
// Inset on all four sides with the app's inner radius, it is unambiguously
|
||||
// *in* the card: a panel with the place's name above it. That is one nested
|
||||
// surface, not "a collection of cards inside a card" — the orders themselves
|
||||
// stay rows on it, with no frame of their own.
|
||||
//
|
||||
// The inset moves the step rail right by [_panelInset], so the order nodes
|
||||
// no longer share an axis with the place node above them. That is correct
|
||||
// now: the spine stopped running between them when the card became the thing
|
||||
// that groups them (see the note at `lineBelow`), so there is no line to
|
||||
// bend — and a nested panel that ignored its own container's inset would
|
||||
// read as broken. `stop_row_alignment_test.dart` pins the new relationship.
|
||||
// They were flush with the group's own left edge, which put a stop's rail
|
||||
// on the same axis as the kitchen heading above it — so an open group read
|
||||
// as two lists that happen to be adjacent rather than as one thing
|
||||
// containing another. A small indent is the whole signal; the spine does
|
||||
// the grouping, so it does not need a fill or a box to go with it.
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
_panelInset,
|
||||
0,
|
||||
_panelInset,
|
||||
_grounded ? _panelInset : 0,
|
||||
),
|
||||
// ── The well is back, because the ground changed under it ──
|
||||
//
|
||||
// The tinted panel behind the orders was removed when Home's rows sat
|
||||
// directly on a white page — a second rectangle then had nothing to
|
||||
// separate itself from. The route now stands on a white [MilerPanel],
|
||||
// which put the open list back to white-on-white: an accordion whose
|
||||
// open state looked exactly like its closed neighbours, which is the
|
||||
// flatness being fixed on this screen.
|
||||
//
|
||||
// So the open orders sit in a canvas-toned well — the one grey that
|
||||
// measurably stands on this white (1.29:1) and the same tone the page
|
||||
// ground already is, so the well reads as the card opening *down to the
|
||||
// page* rather than as a new object. One nested surface, the ladder's
|
||||
// permitted inset; the rows on it still have no frames of their own.
|
||||
padding: EdgeInsets.zero,
|
||||
child: Container(
|
||||
decoration: _grounded
|
||||
? BoxDecoration(
|
||||
color: MilerSurface.canvas,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
||||
)
|
||||
: null,
|
||||
// 8 horizontal, not 10: at 320pt and 2.0× text the two extra points
|
||||
// per side were the difference between fitting and a 31px overflow —
|
||||
// the accessibility sweep in `home_structure_test` is the referee.
|
||||
padding: _grounded
|
||||
? EdgeInsets.fromLTRB(8.w, 8.h, 8.w, 10.h)
|
||||
: EdgeInsets.only(top: 4.h, bottom: 8.h),
|
||||
decoration: null,
|
||||
// 8 → 4. Enough that the rail is visibly inside the group rather than
|
||||
// on the same axis as its heading, and not so much that the stops read
|
||||
// as a second column. The indent is the whole signal; it does not need
|
||||
// to be large to be read.
|
||||
padding: EdgeInsets.only(left: 4.w, top: 4.h, bottom: 4.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── The journey action lives with the orders ──
|
||||
//
|
||||
// It sat on the collapsed card, which read well but put the
|
||||
// screen's loudest control on every place in the run at once —
|
||||
// three kitchens, three red buttons, none of them the one he is
|
||||
// working. Inside the dropdown there is exactly one on screen,
|
||||
// because exactly one place is open: opening a counter *is* the
|
||||
// rider saying "this is the one", and the ride to it is what he
|
||||
// does next.
|
||||
// ── One control row, not two ──
|
||||
//
|
||||
// The journey button sat on its own line *above* the select-all, so
|
||||
// opening a kitchen produced two stacked rows of chrome before the
|
||||
// first order — and they are the two things a rider does with an
|
||||
// open counter, which makes them one row. Select-all leads: it is
|
||||
// what he does first and the quieter of the two. Navigate closes the
|
||||
// row on the right, where the primary action belongs.
|
||||
// No spine on this row. It sits above the first order, so a line
|
||||
// through it is a length of route drawn above node 1 — the thing
|
||||
// the rail is supposed to start at.
|
||||
if (_railRow(context, ids, chosen) != null)
|
||||
_railed(
|
||||
_railRow(context, ids, chosen)!,
|
||||
line: false,
|
||||
// The tick sits where the order nodes sit, so the label lands
|
||||
// on the same axis as the customer names it gathers.
|
||||
node: _selectAllMark(ids, chosen),
|
||||
// No node: the tick travels with its own label now — see the
|
||||
// note in [SelectAllAction].
|
||||
node: null,
|
||||
nodeSize: _StepNode.size,
|
||||
),
|
||||
for (final (i, line) in manifest.indexed)
|
||||
@@ -1743,14 +1755,15 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
selected: selectedIds.contains(line.orderId),
|
||||
busy: busyIds.contains(line.orderId),
|
||||
selectable: selectable,
|
||||
// The spine closes inside the card now: the run's continuity is
|
||||
// carried by the cards themselves, so the last order's connector
|
||||
// has nothing below it to reach.
|
||||
first: i == 0,
|
||||
last: i == manifest.length - 1,
|
||||
lineColor: _lineColor,
|
||||
onTap: () => onToggleSelect?.call(line.orderId),
|
||||
onOpen: onStopTap == null ? null : () => onStopTap!(line.stop),
|
||||
// The order's own sheet, not the place's — see
|
||||
// [RouteTimeline.onOrderTap].
|
||||
onOpen: onOrderTap == null
|
||||
? null
|
||||
: () => onOrderTap!(line.stop),
|
||||
onUnreject: onUnreject == null
|
||||
? null
|
||||
: () => onUnreject!(line.stop),
|
||||
@@ -1783,24 +1796,6 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
///
|
||||
/// Null when there is nothing to select, so an empty row never reserves height
|
||||
/// above the first order.
|
||||
/// The select-all's tick, drawn in the rail's node column.
|
||||
Widget _selectAllMark(List<String> ids, int chosen) {
|
||||
final all = chosen == ids.length && ids.isNotEmpty;
|
||||
final some = chosen > 0 && !all;
|
||||
final accent = (all || some)
|
||||
? ColorConstants.primary
|
||||
: ColorConstants.secondaryText;
|
||||
return Icon(
|
||||
all
|
||||
? LucideIcons.circleCheck
|
||||
: some
|
||||
? LucideIcons.circleMinus
|
||||
: LucideIcons.circleCheck,
|
||||
size: 18.sp,
|
||||
color: accent,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _railRow(BuildContext context, List<String> ids, int chosen) {
|
||||
final selectAll = !group.flat && ids.isNotEmpty && onSelectAll != null
|
||||
? SelectAllAction(
|
||||
@@ -1820,7 +1815,7 @@ class PickupTimelineGroup extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// The inset that turns the expanded ground into a panel *inside* the card.
|
||||
static double get _panelInset => 10.w;
|
||||
static double get _panelInset => 0;
|
||||
|
||||
/// Anything that hangs under the header on the group's own spine, so the line
|
||||
/// runs unbroken past it rather than restarting at the next node.
|
||||
@@ -1997,159 +1992,91 @@ class PickupTimelineStep extends StatelessWidget {
|
||||
color: lineColor,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 6.h, right: 2.w),
|
||||
// ── No tile behind an order ──
|
||||
//
|
||||
// Three grounds used to run here — brand for selected, slate for
|
||||
// accepted, white for pending — each on its own rounded tile.
|
||||
// Every one of them was a rectangle inside the panel inside the
|
||||
// card, and with both of those gone the tiles are five loose
|
||||
// shapes on open ground: a list that reads as five objects
|
||||
// rather than as five steps of one visit.
|
||||
//
|
||||
// Both facts are still carried, and by the thing that is already
|
||||
// per-order: the **node**. Selected is a filled brand disc with
|
||||
// a tick; accepted is a filled slate disc; pending is an
|
||||
// outlined ring with its sequence number. Shape and fill, which
|
||||
// survive sunlight — and the name's weight follows, so the row
|
||||
// reads decided or undecided without a wash across it.
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
children: [
|
||||
// ── The body ticks ──
|
||||
//
|
||||
// The commonest action on a step, so it gets the largest
|
||||
// target: everything left of the bag block.
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
checked: selectable ? selected : null,
|
||||
label: selectable
|
||||
? 'Select $customer, ${line.bag}'
|
||||
: '$customer, ${line.bag}',
|
||||
excludeSemantics: true,
|
||||
// ── The body selects; the corner opens ──
|
||||
//
|
||||
// Selecting is what a rider does to an order row five
|
||||
// times per counter; reading its address is what he
|
||||
// does to one of them occasionally. The frequent action
|
||||
// gets the large target — the whole row — and the
|
||||
// occasional one gets the block that already looks like
|
||||
// a way in: the bag, the order id and a chevron in the
|
||||
// right corner ([_OrderOpenBlock]).
|
||||
//
|
||||
// The two are **siblings**, never nested: two tap
|
||||
// recognisers over the same pixels resolve to neither,
|
||||
// which is how the card's chevron came to be dead for a
|
||||
// build.
|
||||
//
|
||||
// It falls back to opening only where there is nothing
|
||||
// to select, so a settled row is never inert.
|
||||
child: InkWell(
|
||||
onTap: selectable && !busy
|
||||
? onTap
|
||||
: (canOpen ? onOpen : null),
|
||||
borderRadius: BorderRadius.circular(
|
||||
DesignConstants.radiusLg,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(12.w, 9.h, 8.w, 9.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
customer,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
// ── A child of the kitchen, not a sixth kitchen ──
|
||||
//
|
||||
// 15sp w500 in the page's mid-tone keeps a name
|
||||
// comfortably readable outdoors while letting the
|
||||
// header lead. Ticking promotes it to full slate,
|
||||
// because a selected row *is* the thing he is
|
||||
// acting on.
|
||||
// Weight is the third channel, after the node's
|
||||
// shape and the tile's ground: a taken order is
|
||||
// set at w700 in full slate, an undecided one at
|
||||
// w500 in the mid-tone. Three channels means the
|
||||
// distinction survives losing any one of them.
|
||||
style: MilerType.body.copyWith(
|
||||
fontSize: 15.sp,
|
||||
fontWeight: selected || accepted
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
color: rejected
|
||||
? ColorConstants.secondaryText
|
||||
: selected || accepted
|
||||
? ColorConstants.slateText
|
||||
: ColorConstants.onSurfaceVariant,
|
||||
decoration: rejected
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
),
|
||||
),
|
||||
// ── Where the bag is going ──
|
||||
//
|
||||
// The row carried the customer and the booking
|
||||
// reference. The reference is what the office reads
|
||||
// down a phone once, when something has gone wrong;
|
||||
// the *drop area* is what the rider is deciding on
|
||||
// every time he looks at this list — it is how he
|
||||
// knows whether Joe and Priya are the same trip out
|
||||
// of the kitchen or opposite ends of the city.
|
||||
//
|
||||
// So the id moves to the detail sheet, where the
|
||||
// rare question lives, and the area takes the line
|
||||
// under the name. Absent rather than blank when the
|
||||
// payload carries no drop.
|
||||
if (dropArea.isNotEmpty) ...[
|
||||
SizedBox(height: 2.h),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: last
|
||||
? null
|
||||
: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorConstants.borderSubtle,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: 4.h, top: 4.h, right: 2.w),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
checked: selectable ? selected : null,
|
||||
label: selectable
|
||||
? 'Select $customer, ${line.bag}'
|
||||
: '$customer, ${line.bag}',
|
||||
excludeSemantics: true,
|
||||
child: InkWell(
|
||||
onTap: selectable && !busy
|
||||
? onTap
|
||||
: (canOpen ? onOpen : null),
|
||||
borderRadius: BorderRadius.circular(
|
||||
DesignConstants.radiusLg,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(8.w, 6.h, 8.w, 6.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
dropArea,
|
||||
// ── The area wraps; it does not clip ──
|
||||
//
|
||||
// At 2× `Peelamedu` became `SNS…`, which
|
||||
// is not a shorter answer to "where is
|
||||
// this going" — it is no answer. Height is
|
||||
// the cheapest thing on this row to spend,
|
||||
// and the row is already allowed to grow.
|
||||
maxLines: 2,
|
||||
customer,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: MilerType.micro.copyWith(
|
||||
fontSize: 12.5.sp,
|
||||
style: MilerType.body.copyWith(
|
||||
fontSize: 15.sp,
|
||||
fontWeight: selected || accepted
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
color: rejected
|
||||
? ColorConstants.borderStrong
|
||||
: ColorConstants.secondaryText,
|
||||
? ColorConstants.secondaryText
|
||||
: selected || accepted
|
||||
? ColorConstants.slateText
|
||||
: ColorConstants.onSurfaceVariant,
|
||||
decoration: rejected
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
),
|
||||
),
|
||||
if (dropArea.isNotEmpty) ...[
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
dropArea,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: MilerType.micro.copyWith(
|
||||
fontSize: 12.5.sp,
|
||||
color: rejected
|
||||
? ColorConstants.borderStrong
|
||||
: ColorConstants.secondaryText,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// The UNDO branch is gone: a declined order leaves Home for
|
||||
// Activity the moment it is declined, so there is no row
|
||||
// here to un-decline from. See `TripCard._showsOnHome`.
|
||||
//
|
||||
// ── The bag, the id and the chevron are one target ──
|
||||
//
|
||||
// "Which bag" and "which order" are the two things a rider
|
||||
// reads off a step, and "tell me more about this order" is
|
||||
// the action they lead to — so they are one block and one
|
||||
// gesture, marked by the chevron that has always meant
|
||||
// *opens*.
|
||||
_OrderOpenBlock(
|
||||
bag: rejected ? 'Rejected' : line.bag,
|
||||
orderId: line.orderId,
|
||||
selected: selected,
|
||||
onTap: canOpen ? onOpen : null,
|
||||
),
|
||||
],
|
||||
_OrderOpenBlock(
|
||||
bag: rejected ? 'Rejected' : line.bag,
|
||||
orderId: line.orderId,
|
||||
selected: selected,
|
||||
onTap: canOpen ? onOpen : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -2537,7 +2464,7 @@ class _GroupNode extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final done = state == StopState.collected || state == StopState.done;
|
||||
final live = state == StopState.active;
|
||||
final live = state.isAtSource;
|
||||
final taken = state == StopState.accepted;
|
||||
|
||||
// ── Exactly one red node on the route, and it is the next one ──
|
||||
@@ -2664,7 +2591,7 @@ class _StepNode extends StatelessWidget {
|
||||
final done = state == StopState.collected || state == StopState.done;
|
||||
final rejected = state == StopState.rejected;
|
||||
final skipped = state == StopState.skipped;
|
||||
final live = state == StopState.active && !selected;
|
||||
final live = state.isAtSource && !selected;
|
||||
final accepted = state == StopState.accepted;
|
||||
|
||||
final Color accent = selected
|
||||
|
||||
Reference in New Issue
Block a user