/// Canonical lifecycle status for a booking/stop, parsed from the backend's /// free-form `orderstatus` string. /// /// Status used to be compared as raw string literals in ~90 places, with /// case-sensitivity landmines (`'active'` vs `'ACTIVE'`) and variant spellings /// (`'picked'` / `'picked up'` / `'pickuped'`). This normalizes all of that in /// one place so filters agree everywhere. /// /// NOTE: this is the *read* side. The status-change API still sends its own /// protocol verbs (`ACCEPTED`/`ARRIVED`/`PICKED`/`REJECTED`/`CANCELLED`) — see /// [OrderAction] for those canonical write values. enum StopStatus { newStop, assigned, accepted, active, arrived, picked, skipped, cancelled, rejected, unknown, } /// Normalizes any raw `orderstatus` value (case-insensitive, trimmed, variant /// spellings folded) into a [StopStatus]. StopStatus stopStatusFromRaw(dynamic raw) { final s = (raw?.toString() ?? '').trim().toLowerCase(); switch (s) { case 'new': return StopStatus.newStop; case 'assigned': case 'miler_assigned': case 'pending': return StopStatus.assigned; case 'accepted': return StopStatus.accepted; case 'active': return StopStatus.active; case 'arrived': return StopStatus.arrived; case 'picked': case 'picked up': case 'pickuped': case 'pickedup': return StopStatus.picked; case 'skipped': return StopStatus.skipped; case 'cancelled': case 'canceled': return StopStatus.cancelled; case 'rejected': return StopStatus.rejected; default: return StopStatus.unknown; } } /// Parse straight from a booking map's `orderstatus`. StopStatus stopStatusOf(Map booking) => stopStatusFromRaw(booking['orderstatus']); extension StopStatusX on StopStatus { bool get isPicked => this == StopStatus.picked; bool get isCancelled => this == StopStatus.cancelled; bool get isSkipped => this == StopStatus.skipped; bool get isActive => this == StopStatus.active; bool get isRejected => this == StopStatus.rejected; /// A completed pickup — picked up or cancelled. (Matches the long-standing /// `!= 'picked' && != 'picked up' && != 'cancelled'` filter used to build the /// "next stops" and remaining-pickups lists.) bool get isFinishedPickup => this == StopStatus.picked || this == StopStatus.cancelled; /// Still awaiting the rider's acceptance — belongs on Home, not Bookings. bool get isPending => this == StopStatus.newStop || this == StopStatus.assigned; /// What this state is *called*, in the rider's words. /// /// The status vocabulary had been re-invented per widget: the live banner had /// its own switch over raw `orderstatus` strings returning "At pickup" and /// "In progress", the trip card wrote 'Accepted' as a literal, and the two /// disagreed about the same backend state. A rider moving between Home, the /// banner and Bookings was reading three vocabularies for one lifecycle and /// re-learning the app at each stop. /// /// One list, here, next to the parser that produces the states — so a new /// status cannot be added without someone deciding what to call it. String get label => switch (this) { StopStatus.newStop => 'New', StopStatus.assigned => 'Assigned', StopStatus.accepted => 'Accepted', StopStatus.active => 'In progress', StopStatus.arrived => 'At the stop', StopStatus.picked => 'Completed', StopStatus.skipped => 'Skipped', StopStatus.cancelled => 'Cancelled', StopStatus.rejected => 'Rejected', StopStatus.unknown => 'Active', }; } /// Canonical protocol verbs sent to the status-change API (the *write* side). class OrderAction { static const String accept = 'ACCEPT'; static const String accepted = 'ACCEPTED'; static const String arrived = 'ARRIVED'; static const String picked = 'PICKED'; static const String rejected = 'REJECTED'; static const String cancelled = 'CANCELLED'; }