diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index 841dcd5..04c84f0 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -2113,6 +2113,194 @@ border: 1px solid var(--border); } +/* ── Active-delivery card (Active view sidebar) ───────────────────────────── + Clean monitoring card: rider avatar + customer/rider header + status pill, a + truncated drop-address line, and a footer with the pickup kitchen on the left + and distance + ETA-to-drop on the right. `--ad-accent` = owning rider color. */ +.dispatch-container .adcard-list { + display: flex; + flex-direction: column; + gap: 12px; + padding: 4px 2px 12px; +} + +.dispatch-container .adcard { + position: relative; + background: #fff; + border: 1px solid var(--border); + border-radius: 14px; + padding: 13px 14px 13px 16px; + cursor: pointer; + overflow: hidden; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05); + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.dispatch-container .adcard::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: var(--ad-accent, var(--accent)); + opacity: 0.9; + transition: width 0.18s ease; +} + +.dispatch-container .adcard:hover { + transform: translateY(-2px); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1); + border-color: var(--ad-accent, var(--accent)); +} + +.dispatch-container .adcard:hover::before { + width: 5px; +} + +.dispatch-container .adcard.is-active { + border-color: var(--ad-accent, var(--accent)); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12); +} + +.dispatch-container .adcard.is-active::before { + width: 5px; +} + +.dispatch-container .adcard-top { + display: flex; + align-items: center; + gap: 11px; +} + +.dispatch-container .adcard-avatar { + width: 38px; + height: 38px; + border-radius: 11px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 800; + color: #fff; + letter-spacing: 0.02em; +} + +.dispatch-container .adcard-titles { + flex: 1; + min-width: 0; +} + +.dispatch-container .adcard-customer { + font-size: 14.5px; + font-weight: 700; + color: var(--text); + letter-spacing: -0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.dispatch-container .adcard-rider { + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + margin-top: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.dispatch-container .adcard-status { + flex-shrink: 0; + align-self: flex-start; + font-size: 9.5px; + font-weight: 800; + padding: 4px 9px; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.dispatch-container .adcard-addr { + display: flex; + align-items: center; + gap: 7px; + margin-top: 11px; + font-size: 12px; + color: var(--text-muted); + min-width: 0; +} + +.dispatch-container .adcard-addr .adcard-ic { + color: #94a3b8; +} + +.dispatch-container .adcard-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 11px; + padding-top: 10px; + border-top: 1px solid var(--border); +} + +.dispatch-container .adcard-pickup { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: 12px; + font-weight: 600; + color: var(--text); +} + +.dispatch-container .adcard-pickup .adcard-ic { + color: var(--kitchen); +} + +.dispatch-container .adcard-metrics { + display: inline-flex; + align-items: center; + gap: 12px; + flex-shrink: 0; + font-size: 12px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.dispatch-container .adcard-m { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.dispatch-container .adcard-m-km { + color: var(--text-muted); +} + +.dispatch-container .adcard-m-eta { + color: var(--ad-accent, var(--accent)); +} + +.dispatch-container .adcard-ic { + flex-shrink: 0; + display: inline-flex; + align-items: center; + font-size: 14px; +} + +.dispatch-container .adcard-tx { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + + .dispatch-container .zone-order-change-rider { flex-shrink: 0; width: 26px; diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index 3499f9f..21f0b14 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -56,6 +56,8 @@ import { getStatusStyle, FINAL_STATUSES, SKIPPED_STATUSES, + isActiveDelivery, + getActiveOrder, STEP_PALETTE, stepColor } from './dispatchShared'; @@ -1706,6 +1708,15 @@ const Dispatch = ({ }; }, [focusedRider, focusedKitchen, isAllActiveView, allViewOrders, visibleRiders, stats]); + // Count of in-progress deliveries shown in the Active view list. Drives the + // sidebar header visibility — when the active fleet has nothing in progress, + // the header (RIDER DISPATCH title + Active Fleet badge + order/rider tiles) + // is hidden so the "No active deliveries" empty state stands on its own. + const activeDeliveryCount = useMemo( + () => (isAllActiveView ? allViewOrders.filter(isActiveDelivery).length : 0), + [isAllActiveView, allViewOrders] + ); + // List of deliveryids we want GPS logs for. Drives two pipelines: // • renderRoutes() — actual-route polylines on the main map for // every visible rider/trip @@ -2499,6 +2510,58 @@ const Dispatch = ({ ); }; + // Delivery-centric card for the "Active" view sidebar. Instead of one card + // per rider, the Active view lists the in-progress deliveries themselves so + // operators monitor the work, not the people. Clicking focuses the owning + // rider and centers the map on the drop (which, per the Active-view rules, + // collapses to that rider's single active leg + drop pin). + const renderActiveDeliveryCard = (o, i) => { + const rid = o.rider_id; + const rider = riders.find((r) => String(r.id) === String(rid)); + const color = getRiderColor(rid); + const statusStyle = getStatusStyle(o.orderstatus); + const lat = parseFloat(o.droplat || o.deliverylat); + const lon = parseFloat(o.droplon || o.deliverylong); + const canFocus = Number.isFinite(lat) && Number.isFinite(lon); + const estMeters = calculateEstMeters(rid, o); + const customer = o.deliverycustomer || o.customername || `Order #${o.orderid}`; + const dropArea = o.deliverysuburb || o.deliveryaddress || o.zone_name || ''; + + return ( +