/* global window, React */ // ============================================================ // All-trips overview — shown when no trip is selected. // Displays trips ACROSS WAVES (the system runs a wave every hour // and trips accumulate). Filters by utilization / route / fleet / order id. // Clicking a "review needed" trip drills into detail; thin trips // (already dispatched / pre-cleared / completed) are read-only. // ============================================================ const { useState: useOvState, useMemo: useOvMemo } = React; function Kpi({ label, value, sub, kind }) { return (
{label}
{value}
{sub &&
{sub}
}
); } // Status pill — colour-coded function StatusPill({ status }) { const map = { 'PLANNED': { cls: 'pill--red', label: 'Needs review' }, 'HELD': { cls: 'pill--amber', label: 'HELD' }, 'PRE-CLEARED': { cls: 'pill--green', label: 'Pre-cleared' }, 'IN-TRANSIT': { cls: 'pill--blue', label: 'In-transit' }, 'DELIVERED': { cls: 'pill--grey', label: 'Delivered' } }; const m = map[status] || { cls: 'pill--grey', label: status }; return {m.label}; } function WaveBadge({ wave }) { // wave string like "Wave 7" — extract number const n = (wave || '').replace('Wave ', 'W'); return {n}; } // Compact filter dropdown function FilterSelect({ label, value, options, onChange }) { return ( ); } function OverviewBoard({ trips: detailedTrips, checked, onCheck, onClearChecks, onOpen, onEdit, onBulkEdit, onBatch, onToast, dcScope = 'all', dcs = [] }) { const baseTrips = window.ALL_TRIPS || detailedTrips; const allTrips = dcScope === 'all' ? baseTrips : baseTrips.filter((t) => (t.dc || 'chi') === dcScope); const showDcCol = dcScope === 'all'; const numChecked = checked.size; // ---------- filter state ---------- const [fUtil, setFUtil] = useOvState('all'); // all | low | balanced | tight | over const [fRoute, setFRoute] = useOvState('all'); const [fFleet, setFFleet] = useOvState('all'); const [fOrder, setFOrder] = useOvState(''); // text search // build route options (unique destinations) const routeOptions = useOvMemo(() => { const dests = Array.from(new Set(allTrips.map((t) => t.route.to.split(' ')[0]))); dests.sort(); return [{ value: 'all', label: 'All routes' }, ...dests.map((d) => ({ value: d, label: 'Chi → ' + d }))]; }, [allTrips]); const fleetOptions = [ { value: 'all', label: 'All fleets' }, { value: 'private', label: 'Private' }, { value: 'dedicated', label: 'Dedicated' }, { value: 'market', label: 'Market' }, { value: 'spot', label: 'Spot' }]; const utilOptions = [ { value: 'all', label: 'All util' }, { value: 'low', label: '< 80% low' }, { value: 'balanced', label: '80–95%' }, { value: 'tight', label: '95–100%' }, { value: 'over', label: '> 100% over' }]; // ---------- filter logic ---------- const filtered = useOvMemo(() => { return allTrips.filter((t) => { if (fFleet !== 'all' && t.fleet !== fFleet) return false; if (fRoute !== 'all' && !t.route.to.startsWith(fRoute)) return false; if (fUtil !== 'all') { const u = t.util; if (fUtil === 'low' && !(u < 80)) return false; if (fUtil === 'balanced' && !(u >= 80 && u < 95)) return false; if (fUtil === 'tight' && !(u >= 95 && u <= 100)) return false; if (fUtil === 'over' && !(u > 100)) return false; } if (fOrder.trim()) { const q = fOrder.trim().toLowerCase(); const inOrders = (t.orders || []).some((o) => (o.id || '').toLowerCase().includes(q)); const inTrip = (t.id || '').toLowerCase().includes(q); if (!inOrders && !inTrip) return false; } return true; }); }, [allTrips, fFleet, fRoute, fUtil, fOrder]); const hasFilter = fUtil !== 'all' || fRoute !== 'all' || fFleet !== 'all' || fOrder.trim(); // KPIs reflect filtered view (or overall when no filter) const summary = useOvMemo(() => { const review = filtered.filter((t) => t.status === 'PLANNED').length; const held = filtered.filter((t) => t.status === 'HELD').length; const preC = filtered.filter((t) => t.status === 'PRE-CLEARED').length; const flight = filtered.filter((t) => t.status === 'IN-TRANSIT').length; const done = filtered.filter((t) => t.status === 'DELIVERED').length; const otif = filtered.filter((t) => t.chips && t.chips.some((c) => /OTIF/i.test(c.label))).length; const over = filtered.filter((t) => t.util > 100).length; const lowU = filtered.filter((t) => t.util < 80).length; return { review, held, preC, flight, done, otif, over, lowU }; }, [filtered]); const fleetPillKind = { private: 'green', dedicated: 'amber', market: 'blue', spot: 'red' }; const clickRow = (trip) => { if (trip.isThin) { onToast && onToast(`${trip.id} · ${trip.status.toLowerCase()} — read-only (${trip.wave}). No actions available.`); return; } onOpen(trip.id); }; return (
All active trips{dcScope !== 'all' && ` · ${(dcs.find((d) => d.id === dcScope)?.name || dcScope).toUpperCase()}`}
Live across waves · system runs every hour · {summary.review + summary.held} need TP review · {summary.preC} pre-cleared · {summary.flight} in-flight · {summary.done} delivered (wave 5) {dcScope !== 'all' && scoped to {dcScope.toUpperCase()}}
{/* Filter bar */}
{hasFilter && }
{filtered.length} of {allTrips.length} trips
Trip detail · {filtered.length} match{filtered.length === 1 ? '' : 'es'} Click any row to open · ✎ to edit · ☐ to multi-select · pre-cleared / in-flight are read-only
Trip ID {showDcCol && DC} Wave Status Route / Customer Fleet Util Weight Orders Window Agent suggestion
{filtered.map((t) => { const totalLbs = t.orders.reduce((a, o) => a + (o.lbs || 0), 0) || parseInt(t.weight.replace(/[^\d]/g, ''), 10) || 0; const isReadOnly = t.isThin; return (
clickRow(t)}> {!isReadOnly ?
{e.stopPropagation();onCheck(t.id);}}>
: }
{t.id} {t.changedInLastRun && upd {t.updatedMinsAgo < 60 ? `${t.updatedMinsAgo}m` : `${(t.updatedMinsAgo / 60).toFixed(1)}h`} ago }
{showDcCol &&
{(t.dc || 'chi').toUpperCase()}
}
{t.route.from}{t.route.to} {t.customer}
{t.fleetLabel}
100 ? 'var(--red-2)' : t.util < 80 ? 'var(--amber-2)' : 'var(--ink)', fontWeight: 600 }}>{t.util}%
{totalLbs.toLocaleString()} lbs
{t.orders.length}
{t.window.icon} {t.window.value}
TP {t.shortSuggestion}
{!isReadOnly ? <> : READ-ONLY }
); })} {filtered.length === 0 &&
No trips match the current filters.
}
); } Object.assign(window, { OverviewBoard });