import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip, useMap, useMapEvents, ZoomControl } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Side-effect import: patches L.Polyline so pathOptions.offset (in screen px)
// renders the line perpendicular-shifted from its actual geometry. Used by
// Compare → Combined mode to render planned + actual as parallel rails when
// they share the same road geometry (otherwise they'd stack and read as one).
import '../../../utils/leafletPolylineOffset';
import dayjs from 'dayjs';
import { useInfiniteQuery, useQueries, useQuery, useMutation } from '@tanstack/react-query';
import axios from 'axios';
import {
  MdMap,
  MdDirectionsBike,
  MdRestaurant,
  MdPublic,
  MdInventory2,
  MdTrendingUp,
  MdTrendingDown,
  MdAccountBalanceWallet,
  MdStraighten,
  MdLocationOn,
  MdMarkunreadMailbox,
  MdMoveToInbox,
  MdPlace,
  MdTwoWheeler,
  MdNotes,
  MdSwapHoriz,
  MdExpandMore,
  MdInfoOutline,
  MdBatteryFull,
  MdSignalCellularAlt,
  MdMyLocation,
  MdSpeed,
  MdExplore,
  MdAccessTime,
  MdOutlineInventory2,
  MdOutlineTwoWheeler,
  MdOutlineAccessTime,
  MdGpsFixed,
  MdPower,
  MdSearch,
  MdChevronLeft,
  MdChevronRight,
  MdLocalMall,
  MdCheckCircle,
  MdErrorOutline,
  MdWarning,
  MdClose,
  MdFormatListBulleted,
  MdTimer,
  MdCalendarToday,
  MdInsights,
  MdRefresh,
  MdAttachMoney
} from 'react-icons/md';
import ProfitabilitySection from './ProfitabilitySection';
import ActiveSection from './ActiveSection';
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../../api/api';
import {
  STATUS_STYLES,
  getStatusStyle,
  FINAL_STATUSES,
  SKIPPED_STATUSES,
  STEP_PALETTE,
  stepColor,
  isActiveDelivery,
  getActiveOrder
} from './dispatchShared';
import CompareDataPanel from './CompareDataPanel';
import './Dispatch.css';
import logger from '../../../utils/logger';

// Combined-mode rail colors. The per-step palette (STEP_PALETTE) is great for
// "which step is this" but useless for "is this line planned or actual" when
// both layers overlay. In Combined view only we drop the step palette on
// polylines and use fixed, high-contrast colors: indigo for the dispatched
// plan (matches the "Compare" button + the prior "Planned Route" label) and
// emerald for the actual GPS trail (signals "live / real" data). Per-step
// distinction in Combined view is carried by the numbered drop pins, which
// keep STEP_PALETTE so the timeline link to a specific delivery survives.
const COMBINED_PLANNED_COLOR = '#662582';
const COMBINED_ACTUAL_COLOR = '#10b981';

const toNum = (v) => {
  const n = parseFloat(v);
  return Number.isFinite(n) ? n : NaN;
};

// Long delivery addresses come in two shapes:
//   1. Comma-separated: "Room No:C-4, Second Floor, ..., Vetrilaikara St,
//      Peelamedu" — we keep the last two segments (typically street + area).
//   2. Free-form / space-separated: "Vistara Homes 71 & 72 ... Uppilipalayam
//      post Coimbatore - 641 015 Opposite ..." — Indian addresses often run
//      everything into one comma-less string. There's no reliable way to
//      pick the locality token, so we hard-cap to the last 6 words and trim
//      to ~40 chars; the full address still lives in the row's title tooltip.
const extractArea = (addr) => {
  if (!addr) return '';
  const str = String(addr).trim();
  if (!str) return '';
  if (str.includes(',')) {
    const parts = str.split(',').map((s) => s.trim()).filter(Boolean);
    if (parts.length === 0) return str;
    if (parts.length <= 2) return parts.join(', ');
    return parts.slice(-2).join(', ');
  }
  const words = str.split(/\s+/).filter(Boolean);
  const tail = words.length > 6 ? words.slice(-6).join(' ') : str;
  return tail.length > 40 ? `${tail.slice(0, 40).trim()}…` : tail;
};

const hasValidDrop = (o) => Number.isFinite(toNum(o.droplat || o.deliverylat)) && Number.isFinite(toNum(o.droplon || o.deliverylong));
// Try multiple field-name variants — the live delivery API may return pickuplatitude/picklongitude
// or pickuplongitude instead of the shorter pickuplat/pickuplong used in the static data.
const pickupLat = (o) => o.pickuplat || o.pickuplatitude || o.pickup_lat;
const pickupLon = (o) => o.pickuplong || o.pickuplongitude || o.picklongitude || o.pickup_lon;
const hasValidPickup = (o) => Number.isFinite(toNum(pickupLat(o))) && Number.isFinite(toNum(pickupLon(o)));

// Named delivery batches — operator's mental model of the day's waves.
// Each entry covers a half-open range [startHour, endHour) measured in
// FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported.
// Three named batches, bucketed by assigntime per spec:
//   • Morning Batch:   before 8 AM (00:00 → 08:00)
//   • Afternoon Batch: 9 AM → 12:30 PM (09:00 → 12:30)
//   • Evening Batch:   4 PM → 7 PM (16:00 → 19:00)
// Gaps (8–9 AM, 12:30 PM–4 PM, 7 PM+) intentionally fall outside every batch.
const BATCHES_DEFAULT_RAW = [
  { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
  { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 },
  { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
];

// v9: afternoon batch extended to 12:30 PM. Bumping from v8 wipes the
// cached layouts that still hold the old endHour: 12 value.
const SLOTS_STORAGE_KEY = 'dispatch.slots.v9';

// Every prior storage key. Wiped once on mount so stale layouts
// from earlier code versions can't reappear on the next page load.
const LEGACY_SLOTS_STORAGE_KEYS = [
  'dispatch.slots.v1',
  'dispatch.slots.v2',
  'dispatch.slots.v3',
  'dispatch.slots.v4',
  'dispatch.slots.v5',
  'dispatch.slots.v6',
  'dispatch.slots.v7',
  'dispatch.slots.v8'
];

// Build a label like "Slot 1 · 8 AM" (or "Slot 2 · 12:30 PM") from a
// fractional startHour (24h, half-hour steps). Mirrors the human-readable
// form the defaults use, so user-edited slots still look consistent.
const formatSlotLabel = (idx, startHour) => {
  return `Slot ${idx + 1} · ${formatHourLabel(startHour)}`;
};

// Render a fractional hour as a human-readable clock label. Whole hours
// render as "8 AM"; half-hour values render as "12:30 PM". Other fractions
// round to the nearest minute (covers any future extension to quarter-hours
// without losing precision in the label).
const formatHourLabel = (h) => {
  const wholeHour = Math.floor(h);
  const minutes = Math.round((h - wholeHour) * 60);
  const hr = ((wholeHour + 11) % 12) + 1;
  const ampm = wholeHour >= 12 && wholeHour < 24 ? 'PM' : 'AM';
  if (minutes === 0) return `${hr} ${ampm}`;
  const mm = String(minutes).padStart(2, '0');
  return `${hr}:${mm} ${ampm}`;
};

const formatSlotRange = (startHour, endHour) => {
  if (endHour >= 24) return `After ${formatHourLabel(startHour)}`;
  return `${formatHourLabel(startHour)}–${formatHourLabel(endHour)}`;
};

// Derive the operator-facing label/range strings from BATCHES_DEFAULT_RAW.
// Doing it through the formatters (instead of hardcoding "Slot 2 · 12:30 PM"
// etc.) guarantees user-edited slots and default slots render the same way
// — no chance of drift between the two paths.
// Prefer the explicit `name` (e.g. "Morning Batch") when provided; fall back
// to the auto-generated "Slot N · 8 AM" label for any user-added slot that
// doesn't carry a name.
const BATCHES_DEFAULT = BATCHES_DEFAULT_RAW.map((s, i) => ({
  ...s,
  label: s.name || formatSlotLabel(i, s.startHour),
  range: formatSlotRange(s.startHour, s.endHour)
}));

const getBatchForHour = (h, batches) => {
  for (const b of batches) {
    if (h >= b.startHour && h < b.endHour) return b.id;
  }
  return null;
};

// Time fields the operator can pick from to drive slot bucketing. Each
// option maps to a column on the delivery row; the chosen one becomes the
// timestamp `getRowBatch` reads. "Delivery" defaults to actual deliverytime
// with a fallback to expecteddeliverytime so undelivered orders still bucket.
const TIME_FIELDS = [
  { id: 'delivered', label: 'Delivered', keys: ['deliverytime'] },
  { id: 'pending', label: 'Pending', keys: ['expecteddeliverytime'] },
  { id: 'assigned', label: 'Assigned', keys: ['assigntime'] },
  { id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] },
  { id: 'started', label: 'Started', keys: ['starttime'] },
  { id: 'arrived', label: 'Arrived', keys: ['arrivaltime'] },
  { id: 'pickup', label: 'Pickup', keys: ['pickuptime'] },
  { id: 'all', label: 'All', keys: ['deliverytime', 'expecteddeliverytime', 'assigntime', 'acceptedtime', 'arrivaltime', 'pickuptime', 'starttime'] }
];

const getTimeFieldValue = (r, fieldId) => {
  const field = TIME_FIELDS.find((f) => f.id === fieldId) || TIME_FIELDS[0];
  for (const k of field.keys) {
    if (r?.[k]) return r[k];
  }
  return null;
};

const getRowBatch = (r, fieldId = 'all', batches = BATCHES_DEFAULT) => {
  const t = getTimeFieldValue(r, fieldId);
  if (!t) return null;
  const str = String(t).trim();
  // Skip bare date strings — no time component, would always parse to midnight.
  if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null;
  const d = dayjs(t);
  if (!d.isValid()) return null;
  // Pass FRACTIONAL hour so a delivery at 12:45 falls into slot 2 (which
  // starts at 12:30 = 12.5) rather than slot 1 — d.hour() alone would
  // truncate to 12 and mis-bucket the back half of every hour.
  return getBatchForHour(d.hour() + d.minute() / 60, batches);
};

// Sits inside the Compare MapContainer and unpins any pinned popup whenever
// the operator clicks empty map space. Markers' click events do NOT bubble
// to the map, so this only fires on background clicks (which is what we
// want — clicking elsewhere should release the pin).
function CompareMapClickUnpin({ onUnpin }) {
  useMapEvents({ click: () => onUnpin() });
  return null;
}

// Captures the Leaflet map instance for the parent component via a ref. Kept
// available even after the two-map Compare layout was unified into one map,
// since future per-step imperative zoom logic still needs a handle on the
// planned map instance.
function CaptureMap({ targetRef }) {
  const map = useMap();
  useEffect(() => {
    targetRef.current = map;
    return () => { targetRef.current = null; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [map]);
  return null;
}

// Keeps Leaflet's internal canvas in sync with its container when the layout
// changes (sidebar collapse/expand, Compare open/close). A flex/width change
// doesn't fire a window resize, so without this the map's drawn area stays at
// the old size and leaves a grey gutter at the right edge until the next
// pan/zoom. We pulse invalidateSize across the sidebar's 0.32s CSS transition
// so the map grows/shrinks smoothly with the container instead of snapping.
function MapAutoResize({ trigger }) {
  const map = useMap();
  useEffect(() => {
    const timers = [0, 60, 120, 200, 300, 380].map((t) =>
      setTimeout(() => map.invalidateSize({ animate: false }), t)
    );
    return () => timers.forEach(clearTimeout);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [trigger, map]);
  return null;
}

// Haversine distance between two [lat, lng] points in kilometers. Good to
// ~0.1% across city scales; we use it to sum the length of an OSRM-snapped
// polyline so the Compare delta panel can show "actual km" without depending
// on the backend's actualkms field (which can be stale or missing).
function haversineKm(a, b) {
  const R = 6371; // km
  const toRad = (d) => (d * Math.PI) / 180;
  const lat1 = toRad(a[0]);
  const lat2 = toRad(b[0]);
  const dLat = toRad(b[0] - a[0]);
  const dLon = toRad(b[1] - a[1]);
  const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
  return 2 * R * Math.asin(Math.min(1, Math.sqrt(s)));
}

function polylineLengthKm(points) {
  if (!Array.isArray(points) || points.length < 2) return 0;
  let total = 0;
  for (let i = 1; i < points.length; i++) {
    total += haversineKm(points[i - 1], points[i]);
  }
  return total;
}

// ─── Kalman filter + RTS smoother for GPS pings ──────────────────────────
//
// Two independent 1D Kalman filters (one for lat, one for lng) applied to a
// chronologically sorted list of GPS pings, followed by a Rauch-Tung-
// Striebel backward pass. Per-axis state: [position, velocity]. Constant-
// velocity dynamics with random acceleration as process noise; measurement
// model H = [1, 0] (we measure position only).
//
// Pipeline:
//   1. Pre-filter teleport pings (>maxSpeedKmh between consecutive pings,
//      e.g. cold-start fix, GPS multipath). These would otherwise tug the
//      forward filter even with the in-loop Mahalanobis gate enabled.
//   2. Forward Kalman pass with Mahalanobis 3σ outlier gating — pings whose
//      innovation exceeds the gate are not used to update; the prediction
//      is kept as the posterior. Stores prior + posterior moments at each
//      step so the backward pass can run.
//   3. Backward RTS smoother — refines every step using ALL future
//      observations. Logs are fetched in one shot (not streamed) so we
//      can afford the second pass; the accuracy lift is biggest near the
//      start of the trail and through turns the forward pass under-corrects.
//
// Tuning (all in degrees² since pings are in lat/lng):
//   processNoise (q)     — random-acceleration variance (deg²/s²). Default
//                          tuned for urban two-wheelers (~1 m/s² accel).
//                          Lower = smoother but slower to follow sharp turns.
//   measurementNoise (r) — GPS-fix variance (deg²). Default = ~5 m std dev,
//                          which matches consumer GPS in open urban areas.
//                          Bump for dense canyons.
//   outlierGate          — Mahalanobis² threshold for in-loop rejection.
//                          9.0 = 3σ (≈ 99.7% of inliers pass).
//   maxSpeedKmh          — pre-filter for impossible inter-ping speed.
//                          120 km/h covers any legal two-wheeler movement
//                          plus margin; anything above is GPS error.
function kalmanSmoothGps(pings, options = {}) {
  if (!Array.isArray(pings) || pings.length === 0) return [];

  // 1. Filter out obviously invalid coordinate pings (e.g. 0,0 or NaN)
  const cleanedPings = pings.filter(p =>
    Number.isFinite(p.lat) &&
    Number.isFinite(p.lng) &&
    (Math.abs(p.lat) > 0.1 || Math.abs(p.lng) > 0.1)
  );

  if (cleanedPings.length === 0) return [];
  if (cleanedPings.length === 1) {
    return [{ lat: cleanedPings[0].lat, lng: cleanedPings[0].lng, logdate: cleanedPings[0].logdate, _ts: cleanedPings[0]._ts }];
  }

  const processNoise =
    options.processNoise != null ? options.processNoise : 1e-10;
  const measurementNoise =
    options.measurementNoise != null ? options.measurementNoise : 2e-9;
  const outlierGate =
    options.outlierGate != null ? options.outlierGate : 9.0;
  const maxSpeedKmh =
    options.maxSpeedKmh != null ? options.maxSpeedKmh : 120;

  const tsOf = (p) =>
    p._ts || (p.logdate ? new Date(p.logdate).getTime() : 0);

  // 2. Scan forward to find the first valid starting anchor
  let startIdx = 0;
  while (startIdx < cleanedPings.length - 1) {
    const p0 = cleanedPings[startIdx];
    const p1 = cleanedPings[startIdx + 1];
    const ts0 = tsOf(p0);
    const ts1 = tsOf(p1) || ts0 + 1000;
    const dtSec = Math.max(0.001, (ts1 - ts0) / 1000);
    const km = haversineKm([p0.lat, p0.lng], [p1.lat, p1.lng]);
    const speedKmh = (km / dtSec) * 3600;

    if (speedKmh <= maxSpeedKmh) {
      break;
    } else {
      // Speed is too high. Check if p1->p2 is normal (meaning p0 is the outlier)
      if (startIdx + 2 < cleanedPings.length) {
        const p2 = cleanedPings[startIdx + 2];
        const ts2 = tsOf(p2) || ts1 + 1000;
        const dtSec12 = Math.max(0.001, (ts2 - ts1) / 1000);
        const km12 = haversineKm([p1.lat, p1.lng], [p2.lat, p2.lng]);
        const speedKmh12 = (km12 / dtSec12) * 3600;

        if (speedKmh12 <= maxSpeedKmh) {
          startIdx = startIdx + 1;
          continue;
        }
      }
      startIdx++;
    }
  }

  // 3. Teleport filter starting from the valid anchor
  const accepted = [cleanedPings[startIdx]];
  let lastTs = tsOf(cleanedPings[startIdx]);
  for (let i = startIdx + 1; i < cleanedPings.length; i++) {
    const p = cleanedPings[i];
    const ts = tsOf(p) || lastTs + 1000;
    const dtSec = Math.max(0.001, (ts - lastTs) / 1000);
    const prev = accepted[accepted.length - 1];
    const km = haversineKm([prev.lat, prev.lng], [p.lat, p.lng]);
    const speedKmh = (km / dtSec) * 3600;
    if (speedKmh > maxSpeedKmh) continue;
    accepted.push(p);
    lastTs = ts;
  }

  if (accepted.length < 2) {
    return accepted.map((p) => ({ lat: p.lat, lng: p.lng, logdate: p.logdate, _ts: p._ts }));
  }

  // Run a 1D Kalman + RTS smoother over one axis. Returns smoothed
  // positions parallel to `accepted`.
  const smoothAxis = (axisKey) => {
    const N = accepted.length;
    // Per-step storage for the backward RTS pass.
    const xPost = new Array(N);   // [pos, vel] posterior after update
    const pPost = new Array(N);   // 2x2 cov posterior, flattened [p00,p01,p10,p11]
    const xPrior = new Array(N);  // predicted mean before update
    const pPrior = new Array(N);  // predicted cov before update
    const dtArr = new Array(N);   // dt from i-1 → i, for RTS transition

    // Initial state: position = first measurement, velocity from the first
    // two pings (better than 0 — keeps the start of the trail from lagging
    // behind the rider's actual motion). Initial position covariance = r
    // (we just measured it); initial velocity covariance is loose so it
    // can be refined quickly.
    const ts0 = tsOf(accepted[0]);
    const ts1 = tsOf(accepted[1]);
    const dt01 = Math.max(0.1, (ts1 - ts0) / 1000);
    const v0 = (accepted[1][axisKey] - accepted[0][axisKey]) / dt01;
    xPost[0] = [accepted[0][axisKey], v0];
    pPost[0] = [measurementNoise, 0, 0, 1];
    xPrior[0] = xPost[0].slice();
    pPrior[0] = pPost[0].slice();
    dtArr[0] = 0;

    let prevTs = ts0;
    for (let i = 1; i < N; i++) {
      const ts = tsOf(accepted[i]) || prevTs + 1000;
      const dt = Math.max(0.1, (ts - prevTs) / 1000);
      prevTs = ts;
      dtArr[i] = dt;

      // ─── Predict ───
      // x' = F x          where F = [[1, dt], [0, 1]]
      const [xPrev, vPrev] = xPost[i - 1];
      const xPredPos = xPrev + vPrev * dt;
      const xPredVel = vPrev;
      // P' = F P F^T + Q  where Q = q · [[dt⁴/4, dt³/2], [dt³/2, dt²]]
      const [pp00, pp01, pp10, pp11] = pPost[i - 1];
      const dt2 = dt * dt;
      const dt3 = dt2 * dt;
      const dt4 = dt3 * dt;
      const np00 = pp00 + dt * (pp01 + pp10) + dt2 * pp11 + (dt4 / 4) * processNoise;
      const np01 = pp01 + dt * pp11 + (dt3 / 2) * processNoise;
      const np10 = pp10 + dt * pp11 + (dt3 / 2) * processNoise;
      const np11 = pp11 + dt2 * processNoise;
      xPrior[i] = [xPredPos, xPredVel];
      pPrior[i] = [np00, np01, np10, np11];

      // ─── Update (with Mahalanobis gating) ───
      // y = z − Hx'       (innovation)
      // S = H P' H^T + R  (innovation covariance)
      // Reject the measurement if mahal² = y²/S exceeds the gate. The
      // prediction then carries forward as the posterior — the trail stays
      // continuous instead of being yanked toward a bad fix.
      const z = accepted[i][axisKey];
      const y = z - xPredPos;
      const S = np00 + measurementNoise;
      const mahal2 = (y * y) / S;
      if (mahal2 > outlierGate) {
        xPost[i] = [xPredPos, xPredVel];
        pPost[i] = [np00, np01, np10, np11];
        continue;
      }
      // K = P' H^T / S
      const K0 = np00 / S;
      const K1 = np10 / S;
      // x = x' + K y
      const newPos = xPredPos + K0 * y;
      const newVel = xPredVel + K1 * y;
      // P = (I − K H) P'
      xPost[i] = [newPos, newVel];
      pPost[i] = [
        (1 - K0) * np00,
        (1 - K0) * np01,
        np10 - K1 * np00,
        np11 - K1 * np01
      ];
    }

    // ─── Backward RTS smoother ─────────────────────────────────────────
    // x_smooth[N-1] = x_post[N-1]
    // For i = N-2 … 0:
    //   C       = P_post[i] · F^T · inv(P_prior[i+1])
    //   x_smooth[i] = x_post[i] + C · (x_smooth[i+1] − x_prior[i+1])
    // F^T for a constant-velocity model is [[1,0],[dt,1]], so
    //   P_post · F^T = [[p00 + dt·p01,  p01],
    //                   [p10 + dt·p11,  p11]]
    const xSmooth = new Array(N);
    xSmooth[N - 1] = xPost[N - 1].slice();
    for (let i = N - 2; i >= 0; i--) {
      const dt = dtArr[i + 1];
      const [pp00, pp01, pp10, pp11] = pPost[i];
      const a = pp00 + dt * pp01;
      const b = pp01;
      const c = pp10 + dt * pp11;
      const d = pp11;
      // Invert P_prior[i+1] (2x2):  inv = (1/det) · [[q11,-q01],[-q10,q00]]
      const [q00, q01, q10, q11] = pPrior[i + 1];
      const det = q00 * q11 - q01 * q10;
      if (!Number.isFinite(det) || Math.abs(det) < 1e-30) {
        xSmooth[i] = xPost[i].slice();
        continue;
      }
      const inv00 = q11 / det;
      const inv01 = -q01 / det;
      const inv10 = -q10 / det;
      const inv11 = q00 / det;
      // Smoother gain C = (P_post · F^T) · inv(P_prior_next)
      const c00 = a * inv00 + b * inv10;
      const c01 = a * inv01 + b * inv11;
      const c10 = c * inv00 + d * inv10;
      const c11 = c * inv01 + d * inv11;
      const dxPos = xSmooth[i + 1][0] - xPrior[i + 1][0];
      const dxVel = xSmooth[i + 1][1] - xPrior[i + 1][1];
      xSmooth[i] = [
        xPost[i][0] + c00 * dxPos + c01 * dxVel,
        xPost[i][1] + c10 * dxPos + c11 * dxVel
      ];
    }

    return xSmooth.map((s) => s[0]);
  };

  const lats = smoothAxis('lat');
  const lngs = smoothAxis('lng');
  return accepted.map((p, i) => ({
    lat: lats[i],
    lng: lngs[i],
    logdate: p.logdate,
    _ts: p._ts
  }));
}

// Splits a routed OSRM polyline into per-step segments by finding the
// polyline index closest to each drop waypoint. Returns an array of
// segments where segments[i] runs from the previous waypoint (or polyline
// start when i===0) to drops[i]. Adjacent segments share their endpoint
// so the rendered polylines visually touch with no gap.
//
// Used by renderRoutes in Compare mode to recolor the planned-route
// polyline per step, so the left map's step colors match the right map's
// per-step palette + the timeline strip. Without per-segment splitting
// the planned polyline would be a single rider-colored line that doesn't
// visually link to step N's actual GPS polyline.
function splitPolylineByDrops(polyline, drops) {
  if (!Array.isArray(polyline) || polyline.length < 2 || !drops || !drops.length) {
    return [];
  }
  const findClosestIndex = (target) => {
    let best = 0;
    let bestD = Infinity;
    for (let i = 0; i < polyline.length; i++) {
      const dy = polyline[i][0] - target[0];
      const dx = polyline[i][1] - target[1];
      const d = dy * dy + dx * dx;
      if (d < bestD) { bestD = d; best = i; }
    }
    return best;
  };
  const cutIndices = drops.map(findClosestIndex);
  // Force monotonically non-decreasing — in pathological route shapes the
  // raw "closest" indices can briefly regress (OSRM doubles back near a
  // waypoint), which would produce empty/negative slices below.
  for (let i = 1; i < cutIndices.length; i++) {
    if (cutIndices[i] < cutIndices[i - 1]) cutIndices[i] = cutIndices[i - 1];
  }
  const segments = [];
  let prev = 0;
  cutIndices.forEach((idx) => {
    const end = Math.max(idx, prev);
    segments.push(polyline.slice(prev, end + 1));
    prev = end;
  });
  return segments;
}


// --- Marker popup helpers ---
// Strip the date portion from an API timestamp — operators viewing today's
// orders only care about the wall-clock time. Falls back to the raw string
// if dayjs can't parse the value.
const formatTimeOnly = (t) => {
  if (!t) return null;
  const d = dayjs(t);
  if (!d.isValid()) return String(t);
  return d.format('HH:mm:ss');
};

// Stages the popup walks through, top → bottom, in real-world delivery order.
// Each row only renders when the corresponding API field is populated, so the
// timeline visually shrinks for orders that haven't reached later stages yet.
const POPUP_TIMELINE = [
  { key: 'assigntime', label: 'Assigned' },
  { key: 'acceptedtime', label: 'Accepted' },
  { key: 'arrivaltime', label: 'Arrived' },
  { key: 'pickuptime', label: 'Pickup' },
  { key: 'starttime', label: 'Started' },
  { key: 'deliverytime', label: 'Delivered', final: true }
];

// Build a polyline-ready point list for a sorted trip:
//   - drop NaN drops
//   - prepend the first valid pickup we can find (so the line starts at the kitchen)
const buildTripPoints = (sorted) => {
  const valid = sorted.filter(hasValidDrop);
  if (!valid.length) return [];
  const pickupSrc = sorted.find(hasValidPickup);
  const pts = [];
  if (pickupSrc) pts.push([toNum(pickupLat(pickupSrc)), toNum(pickupLon(pickupSrc))]);
  valid.forEach((o) => pts.push([toNum(o.droplat || o.deliverylat), toNum(o.droplon || o.deliverylong)]));
  return pts;
};

const getTripCacheKey = (riderId, tripKey, points) => {
  if (!points || points.length === 0) return `${riderId}-${tripKey}`;
  const pointsSig = points.map((p) => `${p[0].toFixed(5)},${p[1].toFixed(5)}`).join('|');
  return `${riderId}-${tripKey}-${pointsSig}`;
};

// Fix for default leaflet marker icons
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
  iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
  iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
  shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});

const RIDER_COLORS = ['#0055FF', '#00D82C', '#FF6B00', '#9D00FF', '#FF00A8', '#00C2B2', '#FF9900', '#FF0000'];

// Deterministic rider-id → palette slot mapping. The main `riders` array
// assigns colors by iteration order (see useMemo around line 1308), which
// means the same rider can shuffle to a different color across live-data
// refetches. The Rider Info list needs a fixed color per rider so the dot
// next to "Rajan A" doesn't flip from blue to green on the next poll, so
// it uses this hash-based lookup instead of `getRiderColor(id)`.
const getStableRiderColor = (id) => {
  const s = String(id ?? '');
  if (!s) return RIDER_COLORS[0];
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = (h * 31 + s.charCodeAt(i)) >>> 0;
  }
  return RIDER_COLORS[h % RIDER_COLORS.length];
};

// STATUS_STYLES, getStatusStyle, FINAL_STATUSES, SKIPPED_STATUSES,
// STEP_PALETTE, stepColor — moved to ./dispatchShared.js so the
// extracted CompareDataPanel component can import them without forcing
// a circular dependency on Dispatch.js.

const MapController = ({ focusedItem, viewMode, orders, kitchens, locationKey }) => {
  const map = useMap();
  // Last fit signature. We only call fitBounds when this changes — otherwise
  // every parent render (data refetch, sidebar tick, etc.) would refit the
  // map and snap it back mid-drag, which felt like the map was un-draggable.
  const lastFitKeyRef = useRef('');

  // Stable identifier for the current focus target. Uses ids where possible
  // so it doesn't churn when the underlying objects get rebuilt by useMemo.
  // `locationKey` is included everywhere so switching hubs (Coimbatore →
  // Nagercoil etc.) always triggers a refit onto the new region, even when
  // the new hub happens to have the same kitchen/order count.
  //
  // We also append a coarse centroid (lat/lon rounded to 1 decimal) of the
  // current dataset. This is what fixes the Nagercoil case: when only the
  // hub id changes but the kitchen/order array is briefly the previous
  // hub's stale data (or empty during refetch), the fit only happens once
  // truly-new data arrives — at which point the centroid signature flips
  // (e.g. 11.0,77.0 → 8.2,77.4) and we refit to the new region.
  const fitKey = useMemo(() => {
    const loc = locationKey != null ? `loc:${locationKey}|` : '';
    const centroidSig = (pairs) => {
      let lat = 0;
      let lon = 0;
      let n = 0;
      for (const p of pairs) {
        if (Number.isFinite(p[0]) && Number.isFinite(p[1])) {
          lat += p[0];
          lon += p[1];
          n += 1;
        }
      }
      return n === 0 ? '0' : `${(lat / n).toFixed(1)},${(lon / n).toFixed(1)}`;
    };
    if (focusedItem) {
      const id =
        focusedItem.id ??
        focusedItem.kitchenName ??
        focusedItem.name ??
        (focusedItem.lat != null ? `${focusedItem.lat},${focusedItem.lon}` : 'item');
      const n = focusedItem.orders ? focusedItem.orders.length : 0;
      return `${loc}f|${id}|${n}`;
    }
    const kPairs = (kitchens || []).map((k) => [k.lat, k.lon]);
    const kSig = centroidSig(kPairs);
    if (viewMode === 'kitchens') {
      const n = kPairs.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1])).length;
      return `${loc}k|${n}|${kSig}`;
    }
    if (viewMode === 'all') {
      const oPairs = (orders || []).map((o) => [parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]);
      return `${loc}a|${oPairs.length}|${centroidSig(oPairs)}`;
    }
    return `${loc}m|${viewMode || ''}|${kPairs.length}|${kSig}`;
  }, [focusedItem, viewMode, orders, kitchens, locationKey]);

  useEffect(() => {
    if (lastFitKeyRef.current === fitKey) return;

    let pts = [];
    if (focusedItem) {
      if (focusedItem.orders) {
        pts = focusedItem.orders.map((o) => [parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]);
        focusedItem.orders.forEach((o) => pts.push([toNum(pickupLat(o)), toNum(pickupLon(o))]));
      } else {
        pts = [[focusedItem.lat, focusedItem.lon]];
      }
    } else if (viewMode === 'kitchens') {
      pts = (kitchens || [])
        .filter((k) => Number.isFinite(k.lat) && Number.isFinite(k.lon))
        .map((k) => [k.lat, k.lon]);
      if (pts.length === 0) {
        pts = (orders || []).map((o) => [parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]);
      }
    } else if (viewMode === 'all') {
      pts = (orders || []).map((o) => [parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]);
    } else {
      // No focus, viewMode is 'riders' / 'zones' / etc. — still fit to the
      // current hub's footprint so switching from Coimbatore → Nagercoil
      // (or any hub change) recenters the map onto the new region instead
      // of leaving it on the previous city.
      pts = (kitchens || [])
        .filter((k) => Number.isFinite(k.lat) && Number.isFinite(k.lon))
        .map((k) => [k.lat, k.lon]);
      if (pts.length === 0) {
        pts = (orders || []).map((o) => [parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]);
      }
    }

    const filtered = pts.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
    if (filtered.length > 0) {
      const bounds = L.latLngBounds(filtered);
      if (bounds.isValid()) {
        // Zoom into a single-point bounds (focused rider with one drop, or
        // a single kitchen) instead of using maxZoom defaults which would
        // leave the map zoomed out.
        const isSinglePoint = filtered.length === 1 || bounds.getNorthEast().equals(bounds.getSouthWest());
        if (isSinglePoint) {
          map.setView(filtered[0], 15, { animate: true, duration: 0.6 });
        } else {
          map.flyToBounds(bounds, { padding: [60, 60], duration: 0.6, maxZoom: 16 });
        }
        lastFitKeyRef.current = fitKey;
      }
      return;
    }
    // No data to fit to yet — hub switch is mid-flight, or this hub has no
    // orders today. Do NOT lock the fit key: when real hub data arrives the
    // fitKey will change (its centroid signature flips, e.g. 11.0,77.0 →
    // 8.2,77.4) and this effect will re-run with proper coordinates.
    //
    // Also do NOT call setView([11.022, 76.982], 12) here — that was the
    // bug that left Nagercoil (and every non-Coimbatore hub) stuck on the
    // Coimbatore default during the brief window between picking the hub
    // and its data arriving.
  }, [fitKey, focusedItem, viewMode, orders, kitchens, map]);

  return null;
};

// Smoothly-moving rider marker — the Swiggy/Zomato/Rapido style "bike gliding
// down the road" effect. Instead of letting react-leaflet snap the marker to
// each new GPS fix, we keep the <Marker>'s `position` prop frozen at its mount
// coordinate (a stable ref, so react-leaflet never repositions it) and drive
// every subsequent move imperatively with marker.setLatLng() inside a
// requestAnimationFrame loop. Each time `target` changes we ease from the
// marker's current on-screen latlng to the new fix, so the rider visibly
// travels between points rather than teleporting. A large jump (GPS glitch /
// first real fix after a placeholder) snaps instead of crawling across the map.
//
// ADAPTIVE GLIDE — the critical bit for "live" feel:
// We poll the GPS feed every 1s, but the backend only emits a fresh coordinate
// every ~30s, so the same fix repeats for ~30 polls and then jumps. With a
// fixed glide we'd animate for ~1s and then sit frozen for ~29s — the bike
// would look like it teleports every 30s. Instead we MEASURE the real wall-time
// between distinct fixes and stretch the glide across that whole interval
// (clamped). So when fixes are 30s apart the bike eases continuously for the
// full 30s; if the backend ever speeds up to 1s the glide tightens to 1s
// automatically. Either way motion is smooth and never freezes mid-trip.
// `duration` is only the seed used for the very first segment (no prior fix to
// measure against yet).
const MIN_GLIDE_MS = 200; // floor: don't animate faster than this even on rapid fixes
const MAX_GLIDE_MS = 2500; // ceiling: tighter ceiling for 1s update cadence
const interpolatePath = (path, t) => {
  if (!path || path.length === 0) return null;
  if (path.length === 1) return path[0];
  if (t <= 0) return path[0];
  if (t >= 1) return path[path.length - 1];

  const segmentDistances = [];
  let totalDistance = 0;
  for (let i = 0; i < path.length - 1; i++) {
    const p1 = path[i];
    const p2 = path[i + 1];
    const dLat = p2[0] - p1[0];
    const dLng = p2[1] - p1[1];
    const dist = Math.sqrt(dLat * dLat + dLng * dLng);
    segmentDistances.push(dist);
    totalDistance += dist;
  }

  if (totalDistance === 0) return path[0];

  const targetDistance = t * totalDistance;
  let accumulatedDistance = 0;
  for (let i = 0; i < segmentDistances.length; i++) {
    const nextDistance = accumulatedDistance + segmentDistances[i];
    if (nextDistance >= targetDistance) {
      const segT = (targetDistance - accumulatedDistance) / segmentDistances[i];
      const p1 = path[i];
      const p2 = path[i + 1];
      return [
        p1[0] + (p2[0] - p1[0]) * segT,
        p1[1] + (p2[1] - p1[1]) * segT
      ];
    }
    accumulatedDistance = nextDistance;
  }
  return path[path.length - 1];
};

const AnimatedRiderMarker = ({ target, icon, duration = 950, zIndexOffset, eventHandlers, children, markerRef: externalRef }) => {
  const markerRef = useRef(null);
  const rafRef = useRef(null);
  // Frozen mount position — never handed back to react-leaflet again, so it
  // can't fight the imperative animation below.
  const mountPosRef = useRef(target);
  // performance.now() timestamp of the last DISTINCT fix we glided to. Lets us
  // measure the true inter-fix interval and size the next glide to match it.
  const prevFixTsRef = useRef(null);

  // CRITICAL: depend on the primitive lat/lon, NOT the `target` array. The
  // parent re-renders every second (clock tick + 1s GPS poll) and hands us a
  // brand-new `[lat, lon]` array each time. If the effect keyed off that array
  // it would cancel + restart the glide on every render — the ease keeps
  // resetting to zero velocity and the bike visibly stutters/lags. Keying off
  // the numbers means the glide only (re)starts when the rider's coordinate
  // genuinely changes, so each 1s segment plays out uninterrupted.
  const lat = Array.isArray(target) ? Number(target[0]) : NaN;
  const lon = Array.isArray(target) ? Number(target[1]) : NaN;
  useEffect(() => {
    const marker = markerRef.current;
    if (!marker || !Number.isFinite(lat) || !Number.isFinite(lon)) return undefined;

    const to = L.latLng(lat, lon);
    const from = marker.getLatLng();
    if (!from) {
      marker.setLatLng(to);
      return undefined;
    }
    const dLat = to.lat - from.lat;
    const dLng = to.lng - from.lng;
    // No meaningful move (~<0.1m) — snap and skip the rAF.
    if (Math.abs(dLat) < 1e-6 && Math.abs(dLng) < 1e-6) {
      marker.setLatLng(to);
      return undefined;
    }
    // Teleport on big jumps (>2km) so a bad fix doesn't drag the icon across town.
    let bigJump = false;
    try {
      bigJump = from.distanceTo(to) > 2000;
    } catch {
      bigJump = false;
    }
    if (bigJump) {
      marker.setLatLng(to);
      prevFixTsRef.current = performance.now();
      return undefined;
    }

    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    const startTs = performance.now();
    // Size this glide to the real gap since the previous fix so the bike keeps
    // moving for the whole interval instead of darting then freezing. First
    // segment has no prior timestamp, so fall back to the `duration` seed.
    const gap = prevFixTsRef.current == null ? duration : startTs - prevFixTsRef.current;
    const segMs = Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, gap));
    prevFixTsRef.current = startTs;
    const startLat = from.lat;
    const startLng = from.lng;

    let active = true;
    let osrmPath = null;

    // Fetch OSRM route for this step so the bike follows the roads
    const fetchOsrmPath = async () => {
      const url = `https://router.project-osrm.org/route/v1/driving/${from.lng},${from.lat};${to.lng},${to.lat}?overview=full&geometries=geojson`;
      try {
        const res = await fetch(url);
        const json = await res.json();
        if (active && json.routes && json.routes[0]) {
          osrmPath = json.routes[0].geometry.coordinates.map(c => [c[1], c[0]]);
        }
      } catch (e) {
        console.error('OSRM animated rider marker fetch error:', e);
      }
    };
    fetchOsrmPath();

    const step = (now) => {
      if (!active) return;
      const t = Math.min(1, (now - startTs) / segMs);
      
      if (osrmPath && osrmPath.length >= 2) {
        const pos = interpolatePath(osrmPath, t);
        if (pos) {
          marker.setLatLng(pos);
        } else {
          marker.setLatLng([startLat + dLat * t, startLng + dLng * t]);
        }
      } else {
        // Fallback to linear interpolation if OSRM route isn't available
        marker.setLatLng([startLat + dLat * t, startLng + dLng * t]);
      }
      
      if (t < 1) rafRef.current = requestAnimationFrame(step);
    };
    rafRef.current = requestAnimationFrame(step);
    return () => {
      active = false;
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
    };
  }, [lat, lon, duration]);

  // Cleanup any in-flight animation on unmount.
  useEffect(() => () => {
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
  }, []);

  return (
    <Marker
      ref={(inst) => {
        markerRef.current = inst;
        if (typeof externalRef === 'function') externalRef(inst);
        else if (externalRef) externalRef.current = inst;
      }}
      position={mountPosRef.current}
      icon={icon}
      zIndexOffset={zIndexOffset}
      eventHandlers={eventHandlers}
    >
      {children}
    </Marker>
  );
};

// Inline-icon wrapper used wherever a Material icon precedes some text — keeps the
// SVG vertically centered with the adjacent text and inherits the parent color.
const Ico = ({ children }) => (
  <span className="ico-inline" style={{ display: 'inline-flex', alignItems: 'center', verticalAlign: '-2px', marginRight: 4 }}>
    {children}
  </span>
);

// Batch windows for the standalone Dispatch Analysis view. Maps the UI label
// to the X-Batch-Window header value the backend expects.
const ANALYSIS_BATCH_WINDOWS = [
  { key: 'morning', label: 'Morning', timeRange: '12:00 AM – 8:00 AM', sub: 'Early shift orders', color: '#f59e0b', bg: '#fffbeb', border: '#fde68a' },
  { key: 'afternoon', label: 'Noon', timeRange: '9:00 AM – 12:30 PM', sub: 'Lunch rush window', color: '#10b981', bg: '#ecfdf5', border: '#a7f3d0' },
  { key: 'evening', label: 'Evening', timeRange: '4:00 PM – 7:00 PM', sub: 'Dinner & end-of-day', color: '#6366f1', bg: '#eef2ff', border: '#c7d2fe' }
];

// Tolerant field-name lookup so the Analysis card still renders cleanly even
// if the API response uses slightly different keys than expected.
const analysisPick = (obj, keys) => {
  for (const k of keys) {
    if (obj && obj[k] != null && obj[k] !== '') return obj[k];
  }
  return null;
};
const analysisFormatNum = (v) => {
  if (v == null) return '—';
  if (typeof v === 'number') return v.toLocaleString('en-IN');
  const n = parseFloat(v);
  if (Number.isFinite(n)) return n.toLocaleString('en-IN');
  return String(v);
};
const analysisFormatKm = (v) => (v == null ? '—' : `${parseFloat(v).toFixed(1)} km`);
const analysisFormatRupees = (v) => (v == null ? '—' : `₹${parseFloat(v).toFixed(0)}`);
const analysisFormatPct = (v) => {
  if (v == null) return '—';
  const n = parseFloat(v);
  if (!Number.isFinite(n)) return '—';
  return `${n > 1 ? n.toFixed(1) : (n * 100).toFixed(1)}%`;
};
// Parse "HH:mm:ss" or "HH:mm" → seconds since midnight. Returns null when the
// string is missing or malformed. Used to compute gantt percentages for the
// rider timelines on the Analysis page — the API ships those fields as bare
// clock strings, not full ISO datetimes, so we can't lean on dayjs.
const parseHMS = (s) => {
  if (!s || typeof s !== 'string') return null;
  const parts = s.split(':').map(Number);
  if (parts.length < 2 || parts.some((n) => !Number.isFinite(n))) return null;
  const [h, m, sec = 0] = parts;
  return h * 3600 + m * 60 + sec;
};
// Derive an "imbalance severity" bucket from the assessment text + stdev.
// Used to color the recommendation banner correctly when `action == 'none'` —
// the current backend returns no action but still flags real imbalance in
// `fleet_balance_assessment` (e.g. "High imbalance — 57 min spread"), and
// muting that behind a green "balanced" empty state would be misleading.
const analysisSeverity = ({ assessment, spreadMin, stdev }) => {
  const text = String(assessment || '').toLowerCase();
  if (/high imbalance|severe|critical/.test(text)) return 'warn';
  if (/imbalance|recommend/.test(text)) return 'warn';
  // Numeric fallback when the assessment string is missing / unrecognised.
  if (spreadMin != null && spreadMin > 30) return 'warn';
  if (stdev != null && stdev > 1.0) return 'warn';
  if (/balanced|excellent|good/.test(text)) return 'success';
  return 'info';
};

const Dispatch = ({
  data,
  embedded = false,
  // Controlled focus: when selectedRiderId is defined, the focused rider is derived from prop
  // and clicks inside Dispatch only fire onRiderSelect (parent owns the state). When undefined,
  // Dispatch falls back to its internal focusedRider state (standalone /dispatch behavior).
  selectedRiderId,
  onRiderSelect,
  // Highlight a single marker (e.g. on table-row hover). Adds a `.pulse` class to that cmark.
  pulseOrderId,
  // Optional. When provided, focused-rider order cards render a small "change rider"
  // icon in their header. Receiving callsite owns the rider-picker dialog. Standalone
  // /dispatch usage leaves this undefined so the icon never appears there.
  onChangeRider
}) => {
  // Default to "By Zone" when the caller passes pre-zoned data (AI preview); fall back to
  // "By Rider" for the standalone live page where zones are synthesized but riders are primary.
  const initialViewMode = data?.zones && data.zones.length > 0 ? 'zones' : 'riders';
  const [viewMode, setViewMode] = useState(initialViewMode);

  // Top-level page mode for the standalone Dispatch screen: 'live' (existing
  // map + sidebar UI) or 'analysis' (batch efficiency panel). Embedded use
  // (Preview) never reaches the Analysis switcher, so default doesn't matter
  // there.
  const [topView, setTopView] = useState('live');
  const [analysisResults, setAnalysisResults] = useState({});
  const [analysisLoadingWindow, setAnalysisLoadingWindow] = useState(null);
  const [activeBatchKey, setActiveBatchKey] = useState(null);
  // Position modal for rider's `last_position` in the analysis section.
  // Shape: { lat, lon, name, userid, kitchen, status } | null.
  // Held outside the analysis IIFE so a single MapContainer mounts at the
  // top level (Leaflet is expensive to remount mid-list).
  const [riderPositionModal, setRiderPositionModal] = useState(null);
  useEffect(() => {
    if (!riderPositionModal) return undefined;
    const onKey = (e) => { if (e.key === 'Escape') setRiderPositionModal(null); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [riderPositionModal]);

  // TODO: wire to real tenant context once the standalone Dispatch screen
  // surfaces it. 916 matches the example tenant in the API spec.
  const ANALYSIS_TENANT_ID = 916;

  const batchEfficiencyMutation = useMutation({
    mutationFn: fetchBatchEfficiency,
    onMutate: (vars) => setAnalysisLoadingWindow(vars.batch),
    onSuccess: (resp, vars) => {
      setAnalysisResults((prev) => ({
        ...prev,
        [vars.batch]: { data: resp, fetchedAt: dayjs().format('HH:mm:ss') }
      }));
    },
    onSettled: () => setAnalysisLoadingWindow(null)
  });

  const handleFetchAnalysisBatch = (windowKey) => {
    setActiveBatchKey(windowKey);
    // Re-use cached non-error response if already loaded
    const cached = analysisResults[windowKey];
    if (cached && cached.data && cached.data.success !== false) return;
    batchEfficiencyMutation.mutate({
      batch: windowKey,
      tenantId: ANALYSIS_TENANT_ID
    });
  };

  // ─── Auto-refresh the active analysis batch every 15s while the
  //   Analysis view is open. Skips firing when:
  //     • the user isn't on the Analysis tab,
  //     • no batch is selected,
  //     • the previous request is still in flight (prevents queue
  //       stacking on slow networks),
  //     • the browser tab is hidden (saves API quota on routes.workolik).
  //   Loading state is tracked through a ref so the interval doesn't
  //   reset on every in-flight flip. ─────────────────────────────────
  const ANALYSIS_POLL_MS = 15000;
  const analysisLoadingRef = useRef(null);
  useEffect(() => {
    analysisLoadingRef.current = analysisLoadingWindow;
  }, [analysisLoadingWindow]);
  useEffect(() => {
    if (topView !== 'analysis') return undefined;
    if (!activeBatchKey) return undefined;
    const tick = () => {
      if (analysisLoadingRef.current) return;
      if (typeof document !== 'undefined' && document.hidden) return;
      batchEfficiencyMutation.mutate({
        batch: activeBatchKey,
        tenantId: ANALYSIS_TENANT_ID
      });
    };
    const id = setInterval(tick, ANALYSIS_POLL_MS);
    return () => clearInterval(id);
    // batchEfficiencyMutation is stable for the component lifetime.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [topView, activeBatchKey]);
  const [activeRiders, setActiveRiders] = useState(new Set());
  const [internalFocusedRider, setInternalFocusedRider] = useState(null);
  const [focusedKitchen, setFocusedKitchen] = useState(null);
  const [focusedZone, setFocusedZone] = useState(null);
  // Single delivery stop pinned by clicking its sidebar row — overrides the rider's full-route bounds on the map.
  const [focusedStop, setFocusedStop] = useState(null);
  // Sort mode for the focused-rider trip cards in the left sidebar.
  //   'planned' (default) — orders displayed in dispatched sequence (step asc).
  //   'time'              — orders displayed in actual completion sequence
  //                         (deliverytime asc, expecteddeliverytime fallback,
  //                         step tiebreaker). Mirrors how the Compare timeline
  //                         numbers its sequenceStep so an operator can switch
  //                         between sidebar + Compare without re-mapping
  //                         which delivery is "1st", "2nd", etc.
  // Global per the focused rider — every trip in the rider detail shares the
  // mode so the toggle in any trip-header reflects (and controls) all trips.
  const [tripSortMode, setTripSortMode] = useState('planned');
  // Holds leaflet marker instances keyed by orderid so we can imperatively open
  // their popups when the user clicks a step in the focused-rider sidebar.
  const orderMarkerRefs = useRef({});
  // Orderids whose popup was explicitly pinned open by a click. mouseout skips
  // the close for these so the popup stays visible after the cursor leaves the
  // marker; clicking the marker again unpins it. Stored in a ref because this
  // only drives imperative leaflet calls — no re-render needed.
  const pinnedPopupsRef = useRef(new Set());
  // Rider ids whose LIVE GPS marker popup was pinned open by a click. Same
  // semantics as pinnedPopupsRef but tracked separately because live-GPS
  // popups use leaflet's marker-attached <Popup> (openPopup/closePopup) rather
  // than the centered overlay used for order popups.
  const pinnedLivePopupsRef = useRef(new Set());
  // Cache for live GPS rider Leaflet divIcon instances.
  const liveIconCacheRef = useRef(new Map());
  // Short-lived close timer for the general map order/marker popups.
  // Gives the cursor a ~200ms window to travel from the marker onto the popup
  // or vice versa without immediately triggering a close.
  const activePopupMarkerRef = useRef(null);
  const popupHoverTimerRef = useRef(null);
  // Order shown in the centered popup overlay. Rendered outside the leaflet
  // map (see `dispatch-popup-center` overlay near the bottom of the JSX) so
  // `position: fixed` actually centers on the viewport. Drives behavior that
  // used to flow through leaflet's marker-attached <Popup>: hover opens it,
  // mouseout closes it after a ~200ms grace window (unless pinned), click
  // toggles a pinned state stored in pinnedPopupsRef.
  const [centerPopupOrder, setCenterPopupOrder] = useState(null);
  const isControlled = selectedRiderId !== undefined;
  const [clock, setClock] = useState('');

  // Fetch all hubs/locations the logged-in user has access to. The list is
  // rendered as a dropdown next to the Dispatch title so the operator can
  // switch the active hub without going back to login.
  const { data: appLocations } = useQuery({
    queryKey: ['appLocations'],
    queryFn: fetchAppLocations,
    staleTime: 5 * 60 * 1000
  });

  // The user's last-picked location is mirrored into localStorage so it
  // survives reloads and stays in sync with other pages that read it.
  const initialAppLocationId = typeof window !== 'undefined' ? localStorage.getItem('applocationid') : null;
  const [selectedAppLocationId, setSelectedAppLocationId] = useState(
    initialAppLocationId != null ? Number(initialAppLocationId) : 0
  );
  const [locationMenuOpen, setLocationMenuOpen] = useState(false);
  const locationMenuRef = useRef(null);

  // Which timestamp column drives slot bucketing. Default = assigntime so
  // orders bucket into Morning/Afternoon/Evening by when they were assigned,
  // per current spec. The status-wise time-field dropdown is hidden for now
  // (see commented-out block in JSX), so this stays fixed at 'assigned'.
  const [selectedTimeField, setSelectedTimeField] = useState('assigned');
  const [timeFieldMenuOpen, setTimeFieldMenuOpen] = useState(false);
  const timeFieldMenuRef = useRef(null);

  // Operator-editable slot configuration. Seeded from localStorage so edits
  // survive reloads; falls back to BATCHES_DEFAULT otherwise. Each entry has
  // id/label/range/startHour/endHour just like the default list — the rest
  // of the file reads BATCHES (derived below) without caring whether the
  // values came from defaults or from operator edits.
  const [slotsConfig, setSlotsConfig] = useState(() => {
    if (typeof window === 'undefined') return BATCHES_DEFAULT;
    try {
      const raw = window.localStorage.getItem(SLOTS_STORAGE_KEY);
      if (!raw) return BATCHES_DEFAULT;
      const parsed = JSON.parse(raw);
      // If the parsed slots length does not match BATCHES_DEFAULT_RAW, the data
      // is stale (e.g. written from the older 5-slot layout). Discard it and
      // load the default 3-batch layout.
      if (!Array.isArray(parsed) || parsed.length !== BATCHES_DEFAULT_RAW.length) return BATCHES_DEFAULT;
      // Re-derive label + range from the saved hours so any UI tweaks to the
      // formatter (e.g. AM/PM style) flow through to old persisted slots. If
      // the saved id matches a default batch, prefer that batch's friendly
      // name ("Morning Batch", etc.) over the generated "Slot N · time" label.
      return parsed.map((s, i) => {
        const id = s.id || `slot-${i + 1}`;
        const startHour = Number(s.startHour) || 0;
        const endHour = Number(s.endHour) || 24;
        const defaultMatch = BATCHES_DEFAULT.find((b) => b.id === id);
        return {
          id,
          startHour,
          endHour,
          label: defaultMatch?.name || formatSlotLabel(i, startHour),
          range: formatSlotRange(startHour, endHour)
        };
      });
    } catch (e) {
      return BATCHES_DEFAULT;
    }
  });
  const BATCHES = slotsConfig;
  const [slotEditOpen, setSlotEditOpen] = useState(false);
  const slotEditRef = useRef(null);

  // One-shot housekeeping: drop every prior slot-storage key so a stale
  // layout written under an older schema version (e.g. v1/v2/v3 with five
  // slots) can never resurface. Runs once per mount because deps are [].
  useEffect(() => {
    if (typeof window === 'undefined') return;
    try {
      LEGACY_SLOTS_STORAGE_KEYS.forEach((k) => window.localStorage.removeItem(k));
    } catch (e) { /* private-mode / quota — ignore */ }
  }, []);

  // Persist edits whenever slotsConfig changes (skip the first render — the
  // initializer already loaded from storage).
  const slotsInitMountedRef = useRef(false);
  useEffect(() => {
    if (!slotsInitMountedRef.current) { slotsInitMountedRef.current = true; return; }
    if (typeof window === 'undefined') return;
    try {
      window.localStorage.setItem(SLOTS_STORAGE_KEY, JSON.stringify(
        slotsConfig.map(({ id, startHour, endHour }) => ({ id, startHour, endHour }))
      ));
    } catch (e) { /* quota / private-mode — ignore */ }
  }, [slotsConfig]);

  // Close the location dropdown on any click outside its wrapper.
  useEffect(() => {
    if (!locationMenuOpen) return;
    const onDocClick = (e) => {
      if (locationMenuRef.current && !locationMenuRef.current.contains(e.target)) {
        setLocationMenuOpen(false);
      }
    };
    document.addEventListener('mousedown', onDocClick);
    return () => document.removeEventListener('mousedown', onDocClick);
  }, [locationMenuOpen]);

  // Same click-outside behavior for the slot-time-field dropdown.
  useEffect(() => {
    if (!timeFieldMenuOpen) return;
    const onDocClick = (e) => {
      if (timeFieldMenuRef.current && !timeFieldMenuRef.current.contains(e.target)) {
        setTimeFieldMenuOpen(false);
      }
    };
    document.addEventListener('mousedown', onDocClick);
    return () => document.removeEventListener('mousedown', onDocClick);
  }, [timeFieldMenuOpen]);

  // Click-outside dismissal for the slot-edit popover.
  useEffect(() => {
    if (!slotEditOpen) return;
    const onDocClick = (e) => {
      if (slotEditRef.current && !slotEditRef.current.contains(e.target)) {
        setSlotEditOpen(false);
      }
    };
    document.addEventListener('mousedown', onDocClick);
    return () => document.removeEventListener('mousedown', onDocClick);
  }, [slotEditOpen]);

  // Rider Info view — operator picks a rider in the sidebar, the main panel
  // shows that rider's getriderperiodiclogs snapshot. Lives behind a viewMode
  // ('rider-info') so it follows the same toggle pattern as the other modes.
  // Importantly, the rider list here is built from the FULL day's rows
  // (`liveRows`) — NOT the slot-filtered `filteredLiveRows`. Operators using
  // Rider Info want to look up any rider regardless of which slot they were
  // active in.
  const [riderInfoUserid, setRiderInfoUserid] = useState(null);
  const [riderInfoSearch, setRiderInfoSearch] = useState('');

  const { data: riderInfoData, isFetching: riderInfoFetching, isError: riderInfoIsError, error: riderInfoError } = useQuery({
    queryKey: ['riderPeriodicLog', riderInfoUserid],
    queryFn: () => getRiderPeriodicLogs(riderInfoUserid),
    enabled: viewMode === 'rider-info' && riderInfoUserid != null,
    // Auto-refresh the rider snapshot every 15s while the view is open and a
    // rider is selected. Don't poll while the tab is hidden so we don't burn
    // requests on background tabs.
    refetchInterval: viewMode === 'rider-info' && riderInfoUserid != null ? 15_000 : false,
    refetchIntervalInBackground: false,
    staleTime: 5 * 1000,
    refetchOnWindowFocus: false
  });

  // Reverse-geocode the selected rider's GPS so we can show a small banner
  // above the map pin telling the operator which suburb/area the rider is in.
  // Nominatim is rate-limited (1 req/sec public), so we round coords to 4
  // decimals (~11 m) — that turns the query key into a stable cache slot and
  // stops jittery GPS fixes from re-firing the request every poll cycle.
  const riderInfoCoordsKey = useMemo(() => {
    const lat = parseFloat(riderInfoData?.latitude);
    const lon = parseFloat(riderInfoData?.longitude);
    if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
    return { lat: lat.toFixed(4), lon: lon.toFixed(4) };
  }, [riderInfoData?.latitude, riderInfoData?.longitude]);

  const { data: riderInfoArea } = useQuery({
    queryKey: ['reverseGeocode', riderInfoCoordsKey?.lat, riderInfoCoordsKey?.lon],
    queryFn: async () => {
      const res = await fetch(
        `https://nominatim.openstreetmap.org/reverse?lat=${riderInfoCoordsKey.lat}&lon=${riderInfoCoordsKey.lon}&format=json&zoom=16&addressdetails=1`,
        { headers: { Accept: 'application/json' } }
      );
      if (!res.ok) return null;
      const j = await res.json();
      const a = j?.address || {};
      // Prefer the most specific locality name available; the bigger admin
      // levels (city/county/state) are kept only as last-resort fallbacks.
      const area =
        a.suburb || a.neighbourhood || a.village || a.hamlet ||
        a.city_district || a.town || a.city || a.county || a.state || '';
      return { area, display: j?.display_name || '' };
    },
    enabled: viewMode === 'rider-info' && !!riderInfoCoordsKey,
    staleTime: 5 * 60 * 1000,
    refetchOnWindowFocus: false,
    retry: 1
  });

  const locationName = useMemo(() => {
    if (!appLocations) return null;
    const match = appLocations.find((l) => String(l.applocationid) === String(selectedAppLocationId));
    return match?.locationname || null;
  }, [appLocations, selectedAppLocationId]);

  const handleLocationPick = (id) => {
    logger.info('Switching hub/location ID:', id);
    setSelectedAppLocationId(Number(id));
    setLocationMenuOpen(false);
    if (typeof window !== 'undefined') {
      try { localStorage.setItem('applocationid', String(id)); } catch (e) { /* ignore quota errors */ }
    }
    // Switching hubs invalidates the current focused rider/kitchen/zone since
    // they belong to the previous hub's data. handleRiderFocus is declared
    // further down the component but the closure is only called after click,
    // so the reference resolves cleanly.
    handleRiderFocus(null);
    setFocusedKitchen(null);
    setFocusedZone(null);
  };

  const [osrmRoutes, setOsrmRoutes] = useState({});
  // Mirror of osrmRoutes held in a ref so fetchRoute can check the cache without
  // being listed in useCallback deps (which caused a render-loop: fetch → state
  // update → new fetchRoute → effect re-runs → repeat).
  const osrmRoutesRef = useRef({});
  // Road-snapped polylines for each delivery's actual GPS trace (Compare map).
  // Keyed by deliveryid. Same null/false/array semantics as osrmRoutes.
  const [osrmTrackRoutes, setOsrmTrackRoutes] = useState({});
  const osrmTrackRoutesRef = useRef({});
  const [isAnimating, setIsAnimating] = useState(false);
  const [animatedSegments, setAnimatedSegments] = useState([]);
  // Per-step polyline progress for the Compare actual-GPS map. Keyed by
  // sequenceStep, value is the index up to which the step's polyline should
  // be rendered (positions.slice(0, value)). Lets each step's polyline DRAW
  // progressively across the map — matches the planned animation's
  // pair-by-pair drawing style instead of the older "whole step pops in"
  // behavior. Cleared when Animate stops.
  const [animatedActualProgress, setAnimatedActualProgress] = useState({});
  // Mirror of isAnimating accessible inside the rAF loop. The loop needs to
  // bail mid-animation if the user clicks Stop, but it can't read isAnimating
  // directly (closure captures the value at scheduling time).
  const isAnimatingRef = useRef(false);
  const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD'));
  // Custom date-picker popover state. The visible month can drift from the
  // selected date (operator browses months without committing), so we keep
  // the "viewing" month separately. Resets to the selected date's month
  // every time the popover opens so the user never has to navigate back
  // from wherever they last browsed.
  const [datePickerOpen, setDatePickerOpen] = useState(false);
  const [calViewMonth, setCalViewMonth] = useState(() =>
    dayjs(selectedDate).isValid() ? dayjs(selectedDate).startOf('month') : dayjs().startOf('month')
  );
  const datePickerRef = useRef(null);

  // Close the date-picker on outside click or Escape. Mounted only while the
  // popover is open so we're not leaving global listeners around when it's
  // not needed.
  useEffect(() => {
    if (!datePickerOpen) return undefined;
    const onDocClick = (e) => {
      if (!datePickerRef.current) return;
      if (!datePickerRef.current.contains(e.target)) setDatePickerOpen(false);
    };
    const onKey = (e) => {
      if (e.key === 'Escape') setDatePickerOpen(false);
    };
    document.addEventListener('mousedown', onDocClick);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDocClick);
      document.removeEventListener('keydown', onKey);
    };
  }, [datePickerOpen]);

  // When the popover opens, snap the visible month back to whichever month
  // contains the currently-selected date. Otherwise the user could open it
  // expecting to see "May 2026" and find themselves still parked in January
  // from their last browsing session.
  useEffect(() => {
    if (datePickerOpen) {
      const d = dayjs(selectedDate);
      if (d.isValid()) setCalViewMonth(d.startOf('month'));
    }
  }, [datePickerOpen, selectedDate]);

  // Compare mode — when ON, the body splits 50/50: left half is the regular
  // dispatch map (planned routes), right half is a second map that overlays
  // the focused rider's ACTUAL GPS tracks per delivery, pulled from the same
  // /deliveries/getdeliverylogs/?deliveryid=X endpoint the Order Details map
  // uses. Lets the operator visually verify whether the rider stuck to the
  // dispatched plan. Disabled when no rider is focused (the comparison is
  // per-rider). Kept as a single state flag so we don't entangle it with
  // viewMode/focused* logic.
  const [compareOpen, setCompareOpen] = useState(false);
  // Which layer the unified compare map renders.
  //   'combined' = planned (dashed) + actual GPS (solid) overlaid on one map
  //   'planned'  = planned route only
  //   'actual'   = actual GPS tracks only
  // Driven by the segmented control overlaid on the compare map's top-left.
  const [compareViewMode, setCompareViewMode] = useState('combined');
  // Default-open toggle for the "Route sequence" section in the compare data
  // panel — shows planned vs actual visit order with out-of-order steps flagged.
  const [sequenceOpen, setSequenceOpen] = useState(true);
  // Default-open toggle for the planned/actual timeline strip above the
  // compare actual-map. Lets the operator collapse the dual step rows when
  // they want maximum map real estate.
  const [compareTimelineOpen, setCompareTimelineOpen] = useState(true);
  // Set of route-sequence "diff group" indices that are currently expanded.
  // Cascade-aware: when N consecutive steps share the same shift amount,
  // they're collapsed into one summary row by default; clicking it adds the
  // run's index to this set so the individual steps reveal.
  const [expandedSeqGroups, setExpandedSeqGroups] = useState(() => new Set());
  // In controlled mode the parent owns selectedRiderId, so handleRiderFocus only
  // fires onRiderSelect — focusedRider won't update until the parent re-renders.
  // This ref lets us defer setCompareOpen(true) until focusedRider is confirmed.
  const pendingCompareRef = useRef(false);
  // (Compare popup-pin state removed — the marker tooltips now open on hover
  // automatically and click drives focusedCompareStep, so there's nothing left
  // to "pin". See the timeline + delta panel for the persistent detail surface.)

  // Sidebar collapse — hides the 400px sidebar (slides to width 0) so the
  // two maps in Compare mode can use the full body width. Auto-collapses when
  // Compare opens and restores the prior state when Compare closes; a peek
  // tab on the left edge of the body lets the operator override at any time.
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
  const preCompareCollapsedRef = useRef(false);
  const prevCompareOpenRef = useRef(false);

  // Compare-data-panel collapse — mirrors the left sidebar peek tab on the
  // opposite edge so the operator can hide the right rail and let the map
  // claim the full width during compare. Resets to expanded each time
  // Compare opens fresh.
  const [compareDataCollapsed, setCompareDataCollapsed] = useState(false);

  // Compare UI — step focus on the unified compare map.
  //   focusedCompareStep: null = "overall" (whole day); 1..N = drill into
  //     that single delivery. The unified map zooms to that step's bounds
  //     and the data panel switches from day-summary to per-step delta.
  //   leftMapRef: Leaflet map instance captured via CaptureMap — kept so
  //     future per-step zoom logic can drive the map imperatively.
  const [focusedCompareStep, setFocusedCompareStep] = useState(null);
  const leftMapRef = useRef(null);

  // Stable Canvas renderer for the planned (left) map. Leaflet's default
  // canvas (used when `preferCanvas` is true) only draws ~10% beyond the
  // viewport — the moment a drag carries a polyline edge outside that
  // padding, the off-area pixels are blank until `moveend` triggers a
  // redraw, which makes polylines look "broken" mid-drag. Bumping padding
  // to 1.5 makes the canvas ~4× viewport area, so dragging within a screen
  // of travel keeps every polyline drawn without re-raster gaps. The
  // actual (right) map's renderer is created later, after focusedRider is
  // in scope, since it remounts per rider.
  const plannedMapRendererRef = useRef(null);
  if (!plannedMapRendererRef.current) {
    plannedMapRendererRef.current = L.canvas({ padding: 1.5, tolerance: 5 });
  }

  // Pull the partners/getriderlogs feed for the currently selected hub + date.
  // This endpoint returns the exact live GPS position for every rider at the
  // hub (latitude/longitude/logdate/status). We render those positions as
  // markers on the main dispatch map so the operator sees where each rider
  // actually is — matching the Reports → Riders Logs page.
  const RIDER_LOG_POLL_MS = 1000;
  const { data: ridersLocationLogs } = useQuery({
    queryKey: [selectedAppLocationId, selectedDate, ''],
    queryFn: fetchRidersLogs,
    refetchInterval: RIDER_LOG_POLL_MS,
    refetchIntervalInBackground: false,
    staleTime: 0,
    refetchOnWindowFocus: false
  });

  // Normalize the feed into map-ready rider points. Drop entries without a
  // usable lat/lon — those would crash the Leaflet Marker.
  const liveRiderLocations = useMemo(() => {
    return (ridersLocationLogs || [])
      .map((r) => {
        const lat = parseFloat(r?.latitude);
        const lon = parseFloat(r?.longitude);
        if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
        return {
          id: String(r.userid ?? ''),
          userid: r.userid,
          username: r.username || `Rider #${r.userid}`,
          status: String(r.status || '').toLowerCase(),
          contactno: r.contactno,
          orderid: r.orderid,
          logdate: r.logdate,
          lat,
          lon
        };
      })
      .filter(Boolean);
  }, [ridersLocationLogs]);

  // Set of rider ids whose latest GPS log row is `active` (i.e. on the road
  // right now). The "All Active Routes" view (viewMode === 'all') uses this to
  // show ONLY currently-active riders — their cards, routes, drop markers and
  // live bike markers — and hide everyone who is offline/idle for the slot.
  const activeRiderIdSet = useMemo(
    () => new Set(liveRiderLocations.filter((r) => r.status === 'active' || r.status === 'pending').map((r) => String(r.id))),
    [liveRiderLocations]
  );
  // Default to the slot containing the current wall-clock time. Use a
  // fractional hour so 12:45 lands in the 12:30+ slot 2 (not slot 1). If
  // the current time falls outside every slot window (e.g. before 8 AM)
  // fall back to the first slot.
  const [selectedBatch, setSelectedBatch] = useState(() => {
    const now = dayjs();
    return getBatchForHour(now.hour() + now.minute() / 60, BATCHES_DEFAULT) || BATCHES_DEFAULT[0].id;
  });

  // If the operator deletes the slot currently selected, fall back to the
  // first remaining slot so the page doesn't show an empty bucket.
  useEffect(() => {
    if (selectedBatch === 'all') return;
    if (!BATCHES.some((b) => b.id === selectedBatch)) {
      setSelectedBatch(BATCHES[0]?.id || 'all');
    }
  }, [BATCHES, selectedBatch]);
  const activeBatchRef = useRef(null);

  // Live deliveries query — runs only when no `data` prop is passed (i.e., standalone page).
  const shouldFetchLive = !data;
  const liveUserid = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;

  const {
    data: livePagesData,
    isFetching: liveIsFetching,
    isError: liveIsError,
    fetchNextPage: liveFetchNextPage,
    hasNextPage: liveHasNextPage,
    isFetchingNextPage: liveIsFetchingNextPage
  } = useInfiniteQuery({
    queryKey: ['dispatchDeliveries', selectedAppLocationId, liveUserid, 'all', selectedDate, selectedDate, 50, '', 0, 0, 0],
    queryFn: fetchDeliveries,
    getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
    enabled: shouldFetchLive,
    // Order status (pending → delivered) only lives in this feed. Poll it in the
    // Active view so a completed delivery drops out and the rider's NEXT active
    // leg automatically becomes the one shown (card + route + flag) without a
    // manual refresh. Other views don't need second-by-second order churn, so
    // they refetch only on the usual triggers (date/slot/hub change, refocus).
    refetchInterval: viewMode === 'all' ? 15_000 : false,
    refetchIntervalInBackground: false
  });

  // Auto-page through all results for the selected date.
  useEffect(() => {
    if (!shouldFetchLive) return;
    if (liveHasNextPage && !liveIsFetchingNextPage) liveFetchNextPage();
  }, [shouldFetchLive, liveHasNextPage, liveIsFetchingNextPage, liveFetchNextPage]);

  // Holds the last fully-loaded row set (all pages present). Used below to
  // prevent the "orders vanish then reappear" flash during background refetches
  // where TanStack Query fetches pages sequentially and livePagesData temporarily
  // contains only page 1 while the rest are still loading.
  const prevCompleteRowsRef = useRef([]);

  // Sticky cache for active orders in the Active Section sidebar. Keys are orderid strings.
  // Orders are added/refreshed when orderstatus === 'active' and evicted only when the
  // API explicitly returns a terminal status. This prevents cards from flashing away
  // during the 15-second poll cycle when the API temporarily omits a row.
  const stickyActiveRef = useRef({});

  const liveRows = useMemo(() => {
    // Flatten infinite-query pages, then dedupe by orderid. The deliveries API
    // can return the same orderid more than once (e.g. when page bookkeeping
    // overlaps), which would otherwise inflate counts like the header's
    // "N orders today" pill. First occurrence wins.
    const all = (livePagesData?.pages || []).flatMap((p) => p.rows || []);
    const seen = new Set();
    const out = [];
    for (const r of all) {
      const key = r.orderid != null ? String(r.orderid) : null;
      if (key && seen.has(key)) continue;
      if (key) seen.add(key);
      out.push(r);
    }

    // Background refetch loads pages one-by-one: page 1 arrives first, then the
    // auto-pager fetches page 2, 3… While pages are still loading (liveHasNextPage
    // is true and a fetch is in-flight), "out" is a partial snapshot that is
    // smaller than the complete dataset we had before. Returning prevCompleteRowsRef
    // keeps the Active section stable — no cards disappear mid-refetch.
    // On the very first load prevCompleteRowsRef is empty, so we fall through and
    // show whatever partial data is already available (correct for initial render).
    if (liveIsFetching && liveHasNextPage && prevCompleteRowsRef.current.length > 0) {
      return prevCompleteRowsRef.current;
    }

    // All pages have arrived — this is a complete snapshot. Persist it so the
    // next refetch cycle can return it during the partial-pages window above.
    if (!liveHasNextPage) {
      prevCompleteRowsRef.current = out;
    }

    return out;
  }, [livePagesData, liveIsFetching, liveHasNextPage]);

  // Distinct riders across the WHOLE day's rows — NOT slot-filtered. Used by
  // the Rider Info view so the operator can pick any rider regardless of
  // whether they had orders in the currently selected slot.
  const ridersAllDay = useMemo(() => {
    const map = new Map();
    liveRows.forEach((r) => {
      const id = String(r.userid || r.rider_id || '');
      if (!id || id === 'unassigned' || id === '0') return;
      if (!map.has(id)) {
        map.set(id, {
          id,
          riderName: r.ridername || r.rider_name || r.username || `Rider ${id}`
        });
      }
    });
    return Array.from(map.values()).sort((a, b) =>
      String(a.riderName).localeCompare(String(b.riderName))
    );
  }, [liveRows]);

  // Per-batch counts shown on the batch selector pills (uses unfiltered rows so counts stay
  // visible even when a single batch is active). Recomputes whenever the operator
  // picks a different timestamp column to bucket on, or edits the slot ranges.
  const batchCounts = useMemo(() => {
    const counts = { all: liveRows.length };
    BATCHES.forEach((b) => { counts[b.id] = 0; });
    liveRows.forEach((r) => {
      const b = getRowBatch(r, selectedTimeField, BATCHES);
      if (b) counts[b] = (counts[b] || 0) + 1;
    });
    return counts;
  }, [liveRows, selectedTimeField, BATCHES]);

  // Apply the batch filter before grouping so zones/riders/bikes all reflect the chosen wave.
  const filteredLiveRows = useMemo(() => {
    if (selectedBatch === 'all') return liveRows;
    return liveRows.filter((r) => getRowBatch(r, selectedTimeField, BATCHES) === selectedBatch);
  }, [liveRows, selectedBatch, selectedTimeField, BATCHES]);

  const totalDailyProfit = useMemo(() => {
    let profit = 0;
    const riderDailyData = {};

    liveRows.forEach(r => {
      // Filter by selectedDate to align with daily slot aggregations
      const dateStr = r.assigntime
        ? dayjs(r.assigntime).format('YYYY-MM-DD')
        : r.deliverydate
        ? dayjs(r.deliverydate).format('YYYY-MM-DD')
        : 'unknown';
      if (selectedDate && dateStr !== selectedDate) return;

      const batch = getRowBatch(r, selectedTimeField, BATCHES);
      if (!batch || batch === 'all') return;

      const riderKey = String(r.userid || r.rider_id || 'unassigned');
      if (riderKey === 'unassigned' || riderKey === '0') return;

      if (!riderDailyData[riderKey]) {
        riderDailyData[riderKey] = { revenue: 0, kms: 0, batches: new Set() };
      }

      const kms = parseFloat(r.kms || r.actualkms || 0);
      riderDailyData[riderKey].kms += kms;
      riderDailyData[riderKey].revenue += (kms <= 8 ? 30 : 30 + (kms - 8) * 6);
      riderDailyData[riderKey].batches.add(batch);
    });

    Object.values(riderDailyData).forEach(stats => {
      const variableCost = stats.kms * 2.5;
      const slotCount = Math.min(stats.batches.size, 3);
      const fixedCost = slotCount * (500 / 3);
      const totalCost = variableCost + fixedCost;
      profit += (stats.revenue - totalCost);
    });

    return profit;
  }, [liveRows, selectedTimeField, BATCHES, selectedDate]);

  // Reshape flat delivery rows into the zones/riders/orders structure Dispatch consumes.
  const liveData = useMemo(() => {
    if (!shouldFetchLive) return null;
    if (!filteredLiveRows.length) return { code: 200, zone_summary: [], zones: [] };

    // Bucket by delivery area (suburb) so each zone card represents a real place like
    // "Uppilipalayam" — the operator-facing concept of a zone. Suburb strings from the
    // API have inconsistent whitespace and casing, so we key on the normalized form
    // (trimmed + lowercased) and keep the first-seen original casing as the display label.
    const normSuburb = (v) => String(v || '').trim();
    const zoneBuckets = {};
    filteredLiveRows.forEach((r) => {
      const rawZone = normSuburb(r.deliverysuburb) || normSuburb(r.locationsuburb) || normSuburb(r.zone_name);
      const displayName = rawZone || 'Unzoned';
      const zoneKey = displayName.toLowerCase();
      const riderKey = String(r.userid || r.rider_id || 'unassigned');
      const riderName = r.ridername || r.rider_name || r.username || (riderKey === 'unassigned' ? 'Unassigned' : `Rider ${riderKey}`);

      if (!zoneBuckets[zoneKey]) zoneBuckets[zoneKey] = { zone_name: displayName, riders: {} };
      if (!zoneBuckets[zoneKey].riders[riderKey]) {
        zoneBuckets[zoneKey].riders[riderKey] = { rider_id: riderKey, rider_name: riderName, orders: [] };
      }
      zoneBuckets[zoneKey].riders[riderKey].orders.push(r);
    });

    const zones = Object.values(zoneBuckets).map((z) => {
      const riders = Object.values(z.riders).map((rd) => {
        const sorted = [...rd.orders].sort((a, b) =>
          dayjs(a.deliverydate || a.assigntime || 0).valueOf() - dayjs(b.deliverydate || b.assigntime || 0).valueOf()
        );
        return {
          ...rd,
          orders: sorted.map((o, idx) => ({
            ...o,
            trip_number: o.trip_number || 1,
            step: o.step || idx + 1
          }))
        };
      });
      return { zone_name: z.zone_name, riders };
    });

    const zone_summary = zones.map((z) => {
      const allOrds = z.riders.flatMap((r) => r.orders);
      const assigned = allOrds.filter((o) => o.userid || o.rider_id).length;
      return {
        zone_name: z.zone_name,
        total_orders: allOrds.length,
        assigned_orders: assigned,
        unassigned_orders_count: allOrds.length - assigned,
        active_riders_count: z.riders.filter((r) => r.rider_id !== 'unassigned').length,
        total_delivery_kms: allOrds.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
        total_profit: allOrds.reduce((s, o) => s + parseFloat(o.profit || 0), 0)
      };
    });

    return { code: 200, zone_summary, zones };
  }, [shouldFetchLive, filteredLiveRows]);

  // Merge each zone's per-rider data with its summary metrics for sidebar rendering.
  // Also derive aggregates the AI response doesn't pre-compute: which suburbs the zone
  // delivers to, which kitchens it picks up from, and the order-status breakdown.
  const zoneCards = useMemo(() => {
    const source = data || liveData || { zones: [], zone_summary: [] };
    const zonesArr = source.zones || [];
    const summaryByName = {};
    (source.zone_summary || []).forEach((s) => { summaryByName[s.zone_name] = s; });

    const tally = (arr, keyFn) => {
      const m = {};
      arr.forEach((o) => {
        const k = keyFn(o);
        if (!k) return;
        m[k] = (m[k] || 0) + 1;
      });
      return Object.entries(m)
        .map(([name, count]) => ({ name, count }))
        .sort((a, b) => b.count - a.count);
    };

    return zonesArr.map((z) => {
      const summary = summaryByName[z.zone_name] || {};
      const allOrders = (z.riders || []).flatMap((r) => r.orders || []);
      const activeRiderCount = (z.riders || []).filter((r) => r.rider_id && r.rider_id !== 'unassigned').length;

      const suburbs = tally(allOrders, (o) => o.deliverysuburb || o.locationsuburb);
      const kitchens = tally(allOrders, (o) => o.pickupcustomer || o.kitchen_key);
      const statusCounts = {};
      allOrders.forEach((o) => {
        const s = String(o.orderstatus || 'unknown').toLowerCase();
        statusCounts[s] = (statusCounts[s] || 0) + 1;
      });

      return {
        id: z.zone_name,
        name: z.zone_name,
        riders: z.riders || [],
        orders: allOrders,
        totalOrders: summary.total_orders ?? allOrders.length,
        activeRidersCount: summary.active_riders_count ?? activeRiderCount,
        totalKms: summary.total_delivery_kms ?? allOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
        totalProfit: summary.total_profit ?? allOrders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
        suburbs,
        kitchens,
        statusCounts
      };
    });
  }, [data, liveData]);

  // Data processing logic
  const { riders, kitchens, allOrders, stats } = useMemo(() => {
    const source = data || liveData || { zones: [], zone_summary: [] };
    const orders = [];
    (source.zones || []).forEach(z => {
      (z.riders || []).forEach(r => {
        r.orders.forEach(o => {
          orders.push({ ...o, zone_name: z.zone_name, rider_name: r.rider_name, rider_id: r.rider_id });
        });
      });
    });

    const riderMap = {};
    
    // First, populate riderMap with all riders present in the zones to capture those with 0 orders
    (source.zones || []).forEach(z => {
      (z.riders || []).forEach(r => {
        const key = r.rider_id;
        if (!key || key === 'unassigned') return;
        if (!riderMap[key]) {
          riderMap[key] = {
            id: key,
            riderName: r.rider_name || r.username || key,
            orders: [],
            color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length]
          };
        }
      });
    });

    // Next, map the flat orders list to the riders
    orders.forEach(o => {
      const key = o.rider_id || o.userid || 'unknown';
      if (key === 'unassigned') return;
      if (!riderMap[key]) {
        riderMap[key] = {
          id: key,
          riderName: o.rider_name || o.username || o.rider || key,
          orders: [],
          color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length]
        };
      }
      if (!riderMap[key].orders.some(existing => existing.orderid === o.orderid)) {
        riderMap[key].orders.push(o);
      }
    });

    const kitchenMap = {};
    orders.forEach(o => {
      const name = o.pickupcustomer || o.kitchen_key || 'Unknown';
      const key = name.toLowerCase().trim();
      if (!kitchenMap[key]) {
        kitchenMap[key] = {
          id: key,
          kitchenName: name,
          lat: toNum(pickupLat(o)),
          lon: toNum(pickupLon(o)),
          orders: [],
          riders: new Set()
        };
      } else if (!Number.isFinite(kitchenMap[key].lat) && hasValidPickup(o)) {
        // Upgrade to first valid pickup coords we see for this kitchen
        kitchenMap[key].lat = toNum(pickupLat(o));
        kitchenMap[key].lon = toNum(pickupLon(o));
      }
      kitchenMap[key].orders.push(o);
      if (o.rider_id) kitchenMap[key].riders.add(o.rider_id);
    });

    const totalKms = orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
    const totalProfit = orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0);

    // Sort each rider's orders by (trip_number, step) so every downstream view —
    // sidebar step list, step-id badges on the card, OSRM trip points, animation —
    // sees them in delivery order 1→N.
    const sortedRiders = Object.values(riderMap).map((r) => ({
      ...r,
      orders: [...r.orders].sort((a, b) => {
        const tA = a.trip_number || 1;
        const tB = b.trip_number || 1;
        if (tA !== tB) return tA - tB;
        return (a.step || 0) - (b.step || 0);
      })
    }));

    return {
      riders: sortedRiders.sort((a, b) => b.orders.length - a.orders.length),
      kitchens: Object.values(kitchenMap).sort((a, b) => b.orders.length - a.orders.length),
      allOrders: orders,
      stats: {
        totalOrders: orders.length,
        totalKms,
        totalProfit,
        totalRiders: Object.keys(riderMap).length
      }
    };
  }, [data, liveData]);

  // Resolve focusedRider: prop-derived when controlled, local state otherwise.
  const focusedRider = isControlled
    ? (selectedRiderId ? (riders.find((r) => r.id === selectedRiderId) || null) : null)
    : internalFocusedRider;

  // "All Active Routes" view scoping. This view is ORDER-CENTRIC: it shows ONLY
  // riders who are live on GPS right now AND still have an in-progress order —
  // their card in the sidebar, their single active-leg route line, and their
  // live bike marker. Riders who are on GPS but have nothing left to deliver
  // (everything delivered, or GPS-only with no orders) are intentionally
  // excluded here. Every other view (By Location / By Zone / By Rider) is
  // unchanged.
  const isAllActiveView = viewMode === 'all';
  // Orders belonging to GPS-active riders — the candidate pool for this view's
  // list, drop set and auto-fit. Narrowed to in-progress orders below.
  const allViewOrders = useMemo(
    () => (isAllActiveView ? allOrders.filter((o) => activeRiderIdSet.has(String(o.rider_id))) : allOrders),
    [isAllActiveView, allOrders, activeRiderIdSet]
  );
  // The single gate for the whole Active view: rider ids that are GPS-active AND
  // currently have an in-progress order. Driving the sidebar list, routes,
  // markers and map-fit off this one set guarantees they can never disagree
  // about which riders are shown.
  const activeOrderRiderIdSet = useMemo(
    () =>
      new Set(
        (isAllActiveView ? allViewOrders : [])
          .filter(isActiveDelivery)
          .map((o) => String(o.rider_id))
      ),
    [isAllActiveView, allViewOrders]
  );
  // The riders we render in "All Active Routes": GPS-active riders that still
  // have an active order. Every other view is unchanged.
  const visibleRiders = useMemo(
    () =>
      isAllActiveView
        ? riders.filter((r) => activeOrderRiderIdSet.has(String(r.id)))
        : riders,
    [isAllActiveView, riders, activeOrderRiderIdSet]
  );
  // Active section sidebar riders — built from the FULL day's unfiltered rows
  // (liveRows, all batches, no GPS-status gate). Uses a sticky cache so cards
  // only disappear when the API returns an explicit terminal status (delivered /
  // cancelled / skipped). Temporary row omissions during the 15-second poll
  // cycle (single-page refetch, brief API inconsistency) no longer cause cards
  // to flash away and reappear.
  const activeViewRiders = useMemo(() => {
    if (!isAllActiveView) {
      stickyActiveRef.current = {};
      return [];
    }

    const TERMINAL = new Set(['delivered', 'cancelled', 'skipped', 'complete', 'completed']);

    liveRows.forEach((o) => {
      const id = String(o.orderid);
      const s = String(o.orderstatus || '').toLowerCase();
      if (s === 'active') {
        stickyActiveRef.current[id] = o;
      } else if (TERMINAL.has(s)) {
        delete stickyActiveRef.current[id];
      }
      // Non-terminal, non-active statuses for a known order are intentionally
      // ignored — a brief status regression in the poll data must not evict a card.
    });

    const stableOrders = Object.values(stickyActiveRef.current);
    if (!stableOrders.length) return [];

    const riderMeta = Object.fromEntries(riders.map((r) => [r.id, r]));
    const byRider = {};
    stableOrders.forEach((o) => {
      const key = String(o.rider_id || o.userid || '');
      if (!key || key === 'unassigned' || key === '0') return;
      if (!byRider[key]) {
        const meta = riderMeta[key];
        byRider[key] = {
          id: key,
          riderName: meta?.riderName || o.rider_name || o.ridername || o.username || `Rider ${key}`,
          orders: [],
          color: meta?.color || getStableRiderColor(key)
        };
      }
      if (!byRider[key].orders.some((ex) => String(ex.orderid) === String(o.orderid))) {
        byRider[key].orders.push(o);
      }
    });
    return Object.values(byRider);
  }, [isAllActiveView, liveRows, riders]);

  // Live GPS coordinates of exactly the riders this view shows (active + has an
  // order), fed to MapController's auto-fit so the map frames what's rendered.
  const allViewLivePoints = useMemo(
    () =>
      isAllActiveView
        ? liveRiderLocations
          .filter((r) => (r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id)))
          .map((r) => [r.lat, r.lon])
        : [],
    [isAllActiveView, liveRiderLocations, activeOrderRiderIdSet]
  );

  // Clear the sticky active-order cache whenever the operator switches to a
  // different date or zone — stale cards from the previous filter must not leak
  // into the new view.
  useEffect(() => {
    stickyActiveRef.current = {};
  }, [selectedDate, selectedAppLocationId]);

  // Count of in-progress deliveries shown in the Active view list. Derives from
  // the sticky cache (via activeViewRiders) so header and card list always agree.
  const activeDeliveryCount = useMemo(
    () => (isAllActiveView ? activeViewRiders.reduce((sum, r) => sum + r.orders.length, 0) : 0),
    [isAllActiveView, activeViewRiders]
  );

  // Per-rider canvas renderer for the actual (right) map in Compare mode.
  // Single setter used by every interactive site in the UI. In uncontrolled mode it
  // updates local state; in controlled mode it only notifies the parent.
  const handleRiderFocus = useCallback(
    (r) => {
      if (onRiderSelect) onRiderSelect(r ? r.id : null);
      if (!isControlled) setInternalFocusedRider(r);
      setFocusedStop(null);
    },
    [isControlled, onRiderSelect]
  );

  // ─── Logger Effects for Rider & Order Updates ───────────────────────

  // Log when the active focused rider changes (uncontrolled click or controlled prop change)
  const prevFocusedRiderIdRef = useRef(null);
  useEffect(() => {
    const activeId = focusedRider ? focusedRider.id : null;
    if (activeId !== prevFocusedRiderIdRef.current) {
      if (focusedRider) {
        logger.info(`Focused rider changed to: ${focusedRider.riderName} (${focusedRider.orders.length} orders)`);
      } else {
        logger.info('Focused rider reset to: None');
      }
      prevFocusedRiderIdRef.current = activeId;
    }
  }, [focusedRider]);

  // Log when selected focused stop (individual order) changes
  useEffect(() => {
    if (focusedStop) {
      logger.info(`Focused order updated: ID ${focusedStop.orderid}`);
    } else {
      logger.debug('Focused order selection cleared');
    }
  }, [focusedStop]);

  // Log when periodic query refetch completes and updates the overall orders list
  const prevOrdersCountRef = useRef(0);
  useEffect(() => {
    if (allOrders) {
      if (allOrders.length !== prevOrdersCountRef.current) {
        logger.info(`Orders database updated: ${allOrders.length} orders actively tracked`);
        prevOrdersCountRef.current = allOrders.length;
      }
    }
  }, [allOrders]);

  // Log when periodic query refetch completes and updates the live rider locations
  const prevRidersCountRef = useRef(0);
  useEffect(() => {
    if (liveRiderLocations) {
      if (liveRiderLocations.length !== prevRidersCountRef.current) {
        logger.info(`Live riders list updated: ${liveRiderLocations.length} active riders mapped`);
        prevRidersCountRef.current = liveRiderLocations.length;
      }
    }
  }, [liveRiderLocations]);

  const activeStats = useMemo(() => {
    if (focusedRider) {
      return {
        orders: focusedRider.orders.length,
        riders: 1,
        km: focusedRider.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
        profit: focusedRider.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
        label: 'Focused Rider'
      };
    }
    if (focusedKitchen) {
      return {
        orders: focusedKitchen.orders.length,
        riders: focusedKitchen.riders.size,
        km: focusedKitchen.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
        profit: focusedKitchen.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
        label: 'Focused Kitchen'
      };
    }
    // "All Active Routes": the header must reflect exactly what the list/map
    // shows — the in-progress orders and the riders working them — NOT the whole
    // day's totals. We count only active deliveries (and `visibleRiders`, which
    // is already gated to active-order riders) so the tiles can't disagree with
    // the list/map below.
    if (isAllActiveView) {
      const activeOrders = allOrders.filter(isActiveDelivery);
      return {
        orders: activeOrders.length,
        riders: visibleRiders.length,
        km: activeOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0),
        profit: activeOrders.reduce((s, o) => s + parseFloat(o.profit || 0), 0),
        label: 'Active Fleet'
      };
    }
    return {
      orders: stats.totalOrders,
      riders: stats.totalRiders,
      km: stats.totalKms,
      profit: stats.totalProfit,
      label: 'Total Fleet'
    };
  }, [focusedRider, focusedKitchen, isAllActiveView, allViewOrders, visibleRiders, stats]);

  // List of deliveryids tied to the focused rider's orders — used to drive the
  // batched per-delivery GPS log fetch for Compare mode. Deduped; ignores rows
  // without a deliveryid (e.g. unaccepted orders) since /getdeliverylogs needs
  // a real id.
  const focusedRiderDeliveryIds = useMemo(() => {
    if (!focusedRider) return [];
    const set = new Set();
    focusedRider.orders.forEach((o) => {
      if (o.deliveryid != null && o.deliveryid !== '' && o.deliveryid !== 0) set.add(String(o.deliveryid));
    });
    return Array.from(set);
  }, [focusedRider]);

  // Fan-out per-delivery GPS log queries in parallel. Each result is cached by
  // deliveryid, so toggling Compare off/on for the same rider is instant.
  // We only enable the queries while Compare is open AND a rider is focused —
  // otherwise the fetches would fire on idle dispatch viewing and waste
  // bandwidth.
  const deliveryLogQueries = useQueries({
    queries: focusedRiderDeliveryIds.map((deliveryid) => ({
      queryKey: ['deliveryLogs', deliveryid],
      queryFn: async () => {
        const res = await axios.get(
          `${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${deliveryid}`
        );
        // Accept several possible response shapes — the live API has shipped
        // {details:[…]}, plain arrays, and {data:[…]} variants over time, and
        // any of those should produce a polyline. Pick the first non-empty
        // array-like found.
        const candidates = [res?.data?.details, res?.data?.data, res?.data, res];
        const rows = candidates.find((c) => Array.isArray(c)) || [];
        // Also accept lat/lng (alternate naming) in case the endpoint ever
        // returns the same shape the front-end uses internally.
        // Sort by logdate ascending so the polyline follows the rider's
        // chronological path. The endpoint isn't guaranteed to return rows
        // in order — without this, consecutive points can be out of sequence
        // and the rendered track zig-zags between them.
        const sorted = rows
          .map((r) => {
            const ts = r?.logdate ? dayjs(r.logdate) : null;
            return {
              lat: parseFloat(r?.latitude ?? r?.lat),
              lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
              logdate: r?.logdate,
              _ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER
            };
          })
          .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
          .sort((a, b) => a._ts - b._ts);
        // Kalman pass: smooths raw GPS jitter (multi-path noise, stationary
        // wobble at the drop, occasional cold-start outliers) before the
        // pings ever reach the renderer or OSRM. The filter needs the
        // _ts on each ping to compute per-step dt, so we run it BEFORE
        // stripping _ts on the way out.
        const smoothed = kalmanSmoothGps(sorted);
        return smoothed.map(({ _ts, ...p }) => p);
      },
      enabled: compareOpen && focusedRider != null,
      staleTime: 5 * 60 * 1000,
      refetchOnWindowFocus: false,
      retry: 1
    }))
  });

  // Normalize the parallel query results into a flat array of per-delivery
  // tracks for the Compare map. Tracks are sorted by `deliverytime` (with
  // `expecteddeliverytime` and original step number as fallbacks) so the
  // sequence on the map mirrors the order they were actually completed.
  // A 1-indexed `sequenceStep` is attached to drive the numbered markers.
  const riderActualTracks = useMemo(() => {
    if (!focusedRider) return [];
    const tsOf = (o) => {
      const t = o.deliverytime || o.expecteddeliverytime;
      if (!t) return Number.MAX_SAFE_INTEGER;
      const d = dayjs(t);
      return d.isValid() ? d.valueOf() : Number.MAX_SAFE_INTEGER;
    };
    const sorted = focusedRider.orders
      .filter((o) => o.deliveryid != null && o.deliveryid !== '' && o.deliveryid !== 0)
      .sort((a, b) => {
        const diff = tsOf(a) - tsOf(b);
        if (diff !== 0) return diff;
        return (a.step || 0) - (b.step || 0);
      });
    return sorted.map((o, i) => {
      const idx = focusedRiderDeliveryIds.indexOf(String(o.deliveryid));
      const q = idx >= 0 ? deliveryLogQueries[idx] : null;
      return {
        sequenceStep: i + 1,
        orderid: o.orderid,
        deliveryid: o.deliveryid,
        deliverycustomer: o.deliverycustomer,
        pickupcustomer: o.pickupcustomer,
        step: o.step,
        tripNumber: o.trip_number || 1,
        deliverytime: o.deliverytime || o.expecteddeliverytime,
        kms: parseFloat(o.actualkms || o.kms || 0) || 0,
        profit: parseFloat(o.profit || 0) || 0,
        orderstatus: o.orderstatus,
        isLoading: q?.isLoading || q?.isFetching,
        isError: q?.isError,
        coords: q?.data || []
      };
    });
  }, [focusedRider, focusedRiderDeliveryIds, deliveryLogQueries]);


  // Per-step planned-vs-actual deltas for the Compare delta panel. Pure
  // computation off the data we already have:
  //   plannedKm   — order.kms field (set when dispatch planned this trip)
  //   actualKm    — prefer the OSRM-snapped polyline length (most accurate
  //                 once snapping completes); fall back to order.actualkms
  //                 (backend-computed) and finally to the raw GPS polyline
  //                 length so the panel never shows blank while OSRM is
  //                 still in flight.
  //   timeDeltaMin — actual deliverytime - expecteddeliverytime, in minutes.
  //                 Negative = ahead of plan, positive = late.
  //   anomaly      — true when actualKm > 1.25 * plannedKm OR
  //                  timeDeltaMin > 15 (configurable thresholds; chosen so
  //                  small noise doesn't trip the flag but a wrong-turn does).
  const compareDeltas = useMemo(() => {
    if (!focusedRider) return [];
    return riderActualTracks.map((t) => {
      const order = focusedRider.orders.find(
        (o) => String(o.deliveryid) === String(t.deliveryid)
      );
      const plannedKm = parseFloat(order?.kms || 0) || 0;
      const snapped = osrmTrackRoutes[t.deliveryid];
      let actualKm = 0;
      if (Array.isArray(snapped) && snapped.length >= 2) {
        actualKm = polylineLengthKm(snapped);
      } else if (order?.actualkms != null && order.actualkms !== '') {
        actualKm = parseFloat(order.actualkms) || 0;
      } else if (t.coords.length >= 2) {
        actualKm = polylineLengthKm(t.coords.map((p) => [p.lat, p.lng]));
      }
      const kmDelta = actualKm - plannedKm;
      const kmDeltaPct = plannedKm > 0 ? (kmDelta / plannedKm) * 100 : null;
      const expectedTs = order?.expecteddeliverytime
        ? dayjs(order.expecteddeliverytime)
        : null;
      const actualTs = order?.deliverytime ? dayjs(order.deliverytime) : null;
      const timeDeltaMin =
        expectedTs?.isValid() && actualTs?.isValid()
          ? actualTs.diff(expectedTs, 'minute')
          : null;
      const anomaly =
        (plannedKm > 0 && actualKm > plannedKm * 1.25) ||
        (timeDeltaMin != null && timeDeltaMin > 15);
      return {
        sequenceStep: t.sequenceStep,
        deliveryid: t.deliveryid,
        orderid: t.orderid,
        order,
        plannedKm,
        actualKm,
        kmDelta,
        kmDeltaPct,
        expectedTs: expectedTs?.isValid() ? expectedTs : null,
        actualTs: actualTs?.isValid() ? actualTs : null,
        timeDeltaMin,
        anomaly,
        orderstatus: t.orderstatus,
        deliverycustomer: t.deliverycustomer,
        pickupcustomer: order?.pickupcustomer,
        isLoading: t.isLoading,
        coordsCount: t.coords.length
      };
    });
  }, [riderActualTracks, focusedRider, osrmTrackRoutes]);

  // Roll-up of the per-step deltas — used by the overall summary card shown
  // when no specific step is focused. Excluding loading rows from the
  // anomaly count prevents the "47% deviation" headline from churning while
  // OSRM is still snapping.
  const compareSummary = useMemo(() => {
    if (compareDeltas.length === 0) {
      return { plannedKm: 0, actualKm: 0, kmDeltaPct: null, anomalies: 0, late: 0, onTime: 0 };
    }
    const ready = compareDeltas.filter((d) => !d.isLoading && d.coordsCount > 0);
    const plannedKm = ready.reduce((s, d) => s + d.plannedKm, 0);
    const actualKm = ready.reduce((s, d) => s + d.actualKm, 0);
    const kmDeltaPct = plannedKm > 0 ? ((actualKm - plannedKm) / plannedKm) * 100 : null;
    const anomalies = ready.filter((d) => d.anomaly).length;
    const late = ready.filter((d) => d.timeDeltaMin != null && d.timeDeltaMin > 5).length;
    const onTime = ready.filter((d) => d.timeDeltaMin != null && d.timeDeltaMin <= 5).length;
    return { plannedKm, actualKm, kmDeltaPct, anomalies, late, onTime };
  }, [compareDeltas]);

  const plannedOrdered = useMemo(() => {
    return [...compareDeltas].sort(
      (a, b) => (a.order?.step || a.sequenceStep) - (b.order?.step || b.sequenceStep)
    );
  }, [compareDeltas]);

  const actualOrdered = useMemo(() => {
    return [...compareDeltas].sort(
      (a, b) => a.sequenceStep - b.sequenceStep
    );
  }, [compareDeltas]);

  // When Compare is open and a step is focused, override MapController's
  // focusedItem with a synthetic single-order item so the planned (left)
  // map zooms onto the same delivery the operator is scrutinizing. Without
  // this, only the actual (right) map would zoom and the two views would
  // disagree.
  const compareFocusItem = useMemo(() => {
    if (!compareOpen || !focusedCompareStep || !focusedRider) return null;
    const track = riderActualTracks.find((t) => t.sequenceStep === focusedCompareStep);
    if (!track) return null;
    const order = focusedRider.orders.find(
      (o) => String(o.deliveryid) === String(track.deliveryid)
    );
    if (!order) return null;
    return { orders: [order], id: `cmp-step-${focusedCompareStep}-${order.orderid}` };
  }, [compareOpen, focusedCompareStep, focusedRider, riderActualTracks]);

  // Reset focused step when leaving Compare, switching riders, or re-entering
  // Compare. Otherwise "step 5" leaks across riders and the next rider opens
  // stuck on a step that may not exist in their day.
  useEffect(() => {
    setFocusedCompareStep(null);
    setExpandedSeqGroups(new Set());
    setCompareViewMode('combined');
  }, [compareOpen, focusedRider?.id]);

  // When the focused rider changes, close any open comparison so the operator
  // doesn't see stale tracks from a previous rider.
  useEffect(() => {
    if (!focusedRider && compareOpen) setCompareOpen(false);
  }, [focusedRider, compareOpen]);

  // Controlled-mode deferred compare open: once the parent has updated
  // selectedRiderId and focusedRider resolves, honour the pending open request.
  useEffect(() => {
    if (pendingCompareRef.current && focusedRider) {
      pendingCompareRef.current = false;
      setCompareOpen(true);
    }
  }, [focusedRider]);

  // Auto-collapse the sidebar when Compare turns on (two maps need the room),
  // and restore the operator's prior sidebar state when Compare turns off.
  // Manual toggle is still available via the peek tab while compare is open.
  useEffect(() => {
    if (compareOpen && !prevCompareOpenRef.current) {
      preCompareCollapsedRef.current = sidebarCollapsed;
      setSidebarCollapsed(true);
      // Fresh compare-open always reveals the data panel — last-collapsed state
      // shouldn't carry across compare sessions.
      setCompareDataCollapsed(false);
    } else if (!compareOpen && prevCompareOpenRef.current) {
      setSidebarCollapsed(preCompareCollapsedRef.current);
    }
    prevCompareOpenRef.current = compareOpen;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [compareOpen]);

  const fetchRoute = useCallback(async (riderId, tripKey, points) => {
    if (points.length < 2) return;
    const cacheKey = getTripCacheKey(riderId, tripKey, points);
    // Use the ref (not state) for the in-flight / already-cached check so this
    // callback doesn't need osrmRoutes in its deps — that old pattern caused a
    // render loop: each resolved route updated state → recreated fetchRoute →
    // re-ran all route-fetching effects for every rider.
    if (osrmRoutesRef.current[cacheKey] !== undefined) return;

    // Mark in-flight in both ref (immediate) and state (triggers re-render).
    osrmRoutesRef.current[cacheKey] = null;
    setOsrmRoutes(prev => ({ ...prev, [cacheKey]: null }));

    const coords = points.map(p => `${p[1]},${p[0]}`).join(';');
    const url = `https://router.project-osrm.org/route/v1/driving/${coords}?overview=full&geometries=geojson`;

    try {
      const res = await fetch(url);
      const json = await res.json();
      if (json.routes && json.routes[0]) {
        const poly = json.routes[0].geometry.coordinates.map(c => [c[1], c[0]]);
        osrmRoutesRef.current[cacheKey] = poly;
        setOsrmRoutes(prev => ({ ...prev, [cacheKey]: poly }));
      } else {
        // OSRM responded but couldn't route — record as failed so renderRoutes
        // shows the aerial fallback instead of an empty gap.
        osrmRoutesRef.current[cacheKey] = false;
        setOsrmRoutes(prev => ({ ...prev, [cacheKey]: false }));
      }
    } catch (e) {
      console.error('OSRM Fetch error:', e);
      osrmRoutesRef.current[cacheKey] = false;
      setOsrmRoutes(prev => ({ ...prev, [cacheKey]: false }));
    }
  }, []); // stable — cache reads go through osrmRoutesRef, not state

  // Clear the OSRM route cache whenever the date or batch changes. Without this,
  // routes fetched for the previous day/slot linger and are shown against the new
  // data — especially visible when the same rider ID appears across different batches
  // and the cached polyline from the earlier slot is drawn over the new orders.
  useEffect(() => {
    osrmRoutesRef.current = {};
    setOsrmRoutes({});
    osrmTrackRoutesRef.current = {};
    setOsrmTrackRoutes({});
  }, [selectedDate, selectedBatch]);

  // Snap a raw GPS trace (lat/lng pings from /getdeliverylogs) to the road
  // network so the Compare map always shows a smooth road-following polyline,
  // never a zig-zag of aerial segments between sparse pings.
  //
  // Strategy: try OSRM's match service first (purpose-built for noisy GPS
  // traces). If it returns nothing useful (sparse data, off-road pings, etc.)
  // fall back to OSRM's route service over a subsampled set of the same
  // points — that still produces a smooth road polyline, just with less
  // fidelity. Cached per-deliveryid.
  const fetchTrackRoute = useCallback(async (deliveryid, points) => {
    if (osrmTrackRoutesRef.current[deliveryid] !== undefined) return;
    if (!Array.isArray(points) || points.length < 2) return;

    osrmTrackRoutesRef.current[deliveryid] = null;
    setOsrmTrackRoutes((prev) => ({ ...prev, [deliveryid]: null }));

    const storeSuccess = (poly) => {
      osrmTrackRoutesRef.current[deliveryid] = poly;
      setOsrmTrackRoutes((prev) => ({ ...prev, [deliveryid]: poly }));
    };
    const storeFailure = () => {
      osrmTrackRoutesRef.current[deliveryid] = false;
      setOsrmTrackRoutes((prev) => ({ ...prev, [deliveryid]: false }));
    };

    // Even subsample to <=N coordinates while keeping first + last. Public
    // OSRM caps match at 100 and route is slower with many waypoints, so
    // we pick a different N for each service.
    const subsample = (arr, max) => {
      if (arr.length <= max) return arr;
      const step = Math.ceil(arr.length / max);
      const out = arr.filter((_, i) => i % step === 0);
      const last = arr[arr.length - 1];
      if (out[out.length - 1] !== last) out.push(last);
      return out;
    };

    // Attempt 1 — map-matching (best fidelity for dense traces).
    try {
      const ptsM = subsample(points, 90);
      const coordsM = ptsM.map((p) => `${p[1]},${p[0]}`).join(';');
      const matchUrl = `https://router.project-osrm.org/match/v1/driving/${coordsM}?overview=full&geometries=geojson&gaps=ignore&tidy=true`;
      const resM = await fetch(matchUrl);
      const jsonM = await resM.json();
      if (jsonM.matchings && jsonM.matchings.length > 0) {
        const poly = jsonM.matchings.flatMap((m) =>
          (m.geometry?.coordinates || []).map((c) => [c[1], c[0]])
        );
        if (poly.length >= 2) {
          storeSuccess(poly);
          return;
        }
      }
    } catch (e) {
      console.warn('OSRM Match error, trying route fallback:', e);
    }

    // Attempt 2 — waypoint routing through a coarser subsample. Always
    // returns a road polyline as long as the points are routable.
    try {
      const ptsR = subsample(points, 25);
      const coordsR = ptsR.map((p) => `${p[1]},${p[0]}`).join(';');
      const routeUrl = `https://router.project-osrm.org/route/v1/driving/${coordsR}?overview=full&geometries=geojson`;
      const resR = await fetch(routeUrl);
      const jsonR = await resR.json();
      if (jsonR.routes && jsonR.routes[0]) {
        const poly = jsonR.routes[0].geometry.coordinates.map((c) => [c[1], c[0]]);
        if (poly.length >= 2) {
          storeSuccess(poly);
          return;
        }
      }
      storeFailure();
    } catch (e) {
      console.error('OSRM Route fallback error:', e);
      storeFailure();
    }
  }, []);

  // Kick a snap-to-road request for every delivery whose GPS log just landed.
  // Only runs while Compare is open so idle dispatch viewing doesn't hit OSRM.
  useEffect(() => {
    if (!compareOpen || !focusedRider) return;
    riderActualTracks.forEach((t) => {
      if (!t.deliveryid || t.coords.length < 2) return;
      const pts = t.coords.map((c) => [c.lat, c.lng]);
      fetchTrackRoute(t.deliveryid, pts);
    });
  }, [riderActualTracks, compareOpen, focusedRider, fetchTrackRoute]);

  // Mirror isAnimating into a ref so the rAF tick loop in startAnimation can
  // bail mid-animation when the user clicks Stop (the rAF closure captures
  // state at scheduling time and would otherwise keep ticking).
  useEffect(() => {
    isAnimatingRef.current = isAnimating;
  }, [isAnimating]);

  useEffect(() => {
    if (embedded) return undefined;
    const tick = () => {
      const n = new Date();
      setClock([n.getHours(), n.getMinutes(), n.getSeconds()].map(v => String(v).padStart(2, '0')).join(':'));
    };
    const timer = setInterval(tick, 1000);
    tick();
    return () => clearInterval(timer);
  }, [embedded]);

  useEffect(() => {
    setActiveRiders(new Set(riders.map(r => r.id)));
  }, [riders]);

  useEffect(() => {
    riders.forEach(r => {
      const isActive = activeRiders.has(r.id);
      if (!isActive) return;
      if (focusedRider && focusedRider.id !== r.id) return;

      const trips = {};
      r.orders.forEach(o => {
        const t = o.trip_number || 1;
        if (!trips[t]) trips[t] = [];
        trips[t].push(o);
      });

      Object.entries(trips).forEach(([tNum, tOrders]) => {
        const sorted = [...tOrders].sort((a, b) => (a.step || 0) - (b.step || 0));
        const pts = buildTripPoints(sorted);
        if (pts.length >= 2) fetchRoute(r.id, tNum, pts);
      });

      // Active view: fetch a road-following route scoped to the rider's single
      // in-progress leg — current live GPS position → drop (pickup as fallback
      // when no live fix). Cached under a per-order key so renderRoutes() can
      // draw the road polyline for that leg instead of a straight line, and so
      // it never collides with the per-trip route fetched above.
      if (isAllActiveView) {
        const activeOrder = getActiveOrder(r.orders);
        if (activeOrder) {
          const lp = liveRiderLocations.find((l) => String(l.id) === String(r.id));
          const start = (lp && Number.isFinite(lp.lat) && Number.isFinite(lp.lon))
            ? [lp.lat, lp.lon]
            : (hasValidPickup(activeOrder)
              ? [toNum(pickupLat(activeOrder)), toNum(pickupLon(activeOrder))]
              : null);
          const dLat = toNum(activeOrder.droplat || activeOrder.deliverylat);
          const dLon = toNum(activeOrder.droplon || activeOrder.deliverylong);
          if (start && Number.isFinite(dLat) && Number.isFinite(dLon)) {
            fetchRoute(r.id, `active-${activeOrder.orderid}`, [start, [dLat, dLon]]);
          }
        }
      }
    });
  }, [riders, activeRiders, focusedRider, isAllActiveView, liveRiderLocations, fetchRoute]);

  // Auto-advance the selected slot when the wall-clock moves into a new slot's
  // window — BUT only if the user is still sitting on the slot that's just been
  // left, so a manual pick (e.g. "let me inspect Slot 1") is never overridden.
  // Polls every 30s; only fires when the current hour actually changes.
  // prevHourRef tracks the *fractional* hour (hour + minute/60) so the
  // ticker notices slot boundaries that fall mid-hour — slot 1 ending at
  // 12:30 means the auto-advance must fire on the 11:30→12:30 crossing,
  // not just on the wall-clock hour change.
  const prevHourRef = useRef(null);
  useEffect(() => {
    if (!shouldFetchLive) return;
    const nowFracHour = () => {
      const d = dayjs();
      return d.hour() + d.minute() / 60;
    };
    if (prevHourRef.current === null) prevHourRef.current = nowFracHour();
    const tick = () => {
      const h = nowFracHour();
      const fromSlot = getBatchForHour(prevHourRef.current, BATCHES);
      const toSlot = getBatchForHour(h, BATCHES);
      prevHourRef.current = h;
      if (!toSlot || toSlot === fromSlot) return;
      setSelectedBatch((cur) => (cur === fromSlot ? toSlot : cur));
    };
    const id = setInterval(tick, 30 * 1000);
    return () => clearInterval(id);
  }, [shouldFetchLive, BATCHES]);

  // Reset focusedStop when the focused kitchen changes so a stale stop from a
  // previously focused kitchen doesn't linger after switching kitchens.
  // (For riders, handleRiderFocus already clears focusedStop.)
  useEffect(() => {
    setFocusedStop(null);
  }, [focusedKitchen?.id]);

  // Scroll the active slot chip into the visible part of the horizontal scroller
  // — used when the default slot is set late in the day and overflows off-screen,
  // or when the user clicks a chip that's only partially visible.
  useEffect(() => {
    const btn = activeBatchRef.current;
    if (!btn || typeof btn.scrollIntoView !== 'function') return;
    btn.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
  }, [selectedBatch]);

  // When the user clicks a step in the focused-rider sidebar (sets focusedStop),
  // surface that order in the centered popup overlay so the details show up
  // without a second click. focusedStop is a slim { orderid, lat, lon }, so
  // hydrate the full order out of allOrders before passing it to the popup
  // (renderOrderPopupContent needs the rich timeline + status fields).
  // Wait ~350ms so MapController has a chance to recenter first (matches
  // the prior delay before openPopup was called).
  // COMMENTED OUT: as per user request to only show the popup when clicking the map number plots/markers.
  /*
  useEffect(() => {
    if (!focusedStop) return;
    const t = setTimeout(() => {
      const fullOrder = allOrders?.find?.((o) => String(o.orderid) === String(focusedStop.orderid));
      if (fullOrder) setCenterPopupOrder(fullOrder);
    }, 350);
    return () => clearTimeout(t);
  }, [focusedStop, allOrders]);
  */

  const startAnimation = () => {
    if (isAnimating) {
      setIsAnimating(false);
      setAnimatedSegments([]);
      setAnimatedActualProgress({});
      return;
    }

    setIsAnimating(true);
    setAnimatedSegments([]);
    setAnimatedActualProgress({});

    // Capture into locals so the setTimeout callbacks below don't read stale
    // state if the user toggles focus mid-animation.
    const isCompareFocused = compareOpen && focusedRider;
    const compareDeliveryToStep = isCompareFocused
      ? new Map(riderActualTracks.map((t) => [String(t.deliveryid), t.sequenceStep]))
      : null;

    const allSegs = [];
    riders.forEach(r => {
      if (!activeRiders.has(r.id)) return;
      if (focusedRider && focusedRider.id !== r.id) return;
      if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;

      const trips = {};
      r.orders.forEach(o => {
        const t = o.trip_number || 1;
        if (!trips[t]) trips[t] = [];
        trips[t].push(o);
      });

      Object.entries(trips).forEach(([tNum, tOrders]) => {
        // Filter orders by focused kitchen if active
        const filteredTOrders = focusedKitchen
          ? tOrders.filter(o => (o.pickupcustomer || o.kitchen_key || 'Unknown').toLowerCase().trim() === focusedKitchen.id)
          : tOrders;

        if (filteredTOrders.length === 0) return;

        // Use the full trip orders (unfiltered by kitchen) to build the cache key
        // so it matches the key used to fetch the route in the useEffect.
        const fullSorted = [...tOrders].sort((a, b) => (a.step || 0) - (b.step || 0));
        const fullPts = buildTripPoints(fullSorted);
        const cacheKey = getTripCacheKey(r.id, tNum, fullPts);

        const roadPath = osrmRoutes[cacheKey];
        const sorted = [...filteredTOrders].sort((a, b) => (a.step || 0) - (b.step || 0));

        // Aerial fallback — NaN-safe build
        const aerialPath = buildTripPoints(sorted);

        const isKitchenAerial = (viewMode === 'kitchens' || focusedKitchen);
        const path = roadPath || aerialPath;
        if (path.length < 2) return;

        // Compare mode for the focused rider: color each animation pair by
        // its step using STEP_PALETTE so the planned animation visually
        // matches the static per-step polylines + the right map's actual
        // GPS animation. We reuse splitPolylineByDrops to assign each
        // polyline-pair (path[i] → path[i+1]) to the step containing it.
        const isCompareRider = isCompareFocused && r.id === focusedRider.id;
        let pairColorAt = () => r.color; // (idx) → color
        if (isCompareRider) {
          const validDrops = sorted.filter(hasValidDrop);
          const dropCoords = validDrops.map((o) => [
            parseFloat(o.droplat || o.deliverylat),
            parseFloat(o.droplon || o.deliverylong)
          ]);
          const stepSegs = roadPath
            ? splitPolylineByDrops(roadPath, dropCoords)
            : (() => {
              const hasPickup = aerialPath.length > dropCoords.length;
              const out = [];
              for (let i = 0; i < dropCoords.length; i++) {
                const a = hasPickup ? i : i - 1;
                const b = hasPickup ? i + 1 : i;
                if (a < 0 || a >= aerialPath.length || b >= aerialPath.length) {
                  out.push([]);
                } else {
                  out.push([aerialPath[a], aerialPath[b]]);
                }
              }
              return out;
            })();
          // Map each path index → step index by accumulating segment lengths
          // (segments share endpoints so we use length-1 per segment).
          const pathIdxToStepIdx = [];
          let acc = 0;
          stepSegs.forEach((seg, sIdx) => {
            const segLen = Math.max(0, (seg?.length || 0) - 1);
            for (let k = 0; k < segLen; k++) pathIdxToStepIdx[acc + k] = sIdx;
            acc += segLen;
          });
          pairColorAt = (idx) => {
            const sIdx = pathIdxToStepIdx[idx];
            if (sIdx == null) return r.color;
            const order = sorted.filter(hasValidDrop)[sIdx];
            const ss = order ? compareDeliveryToStep.get(String(order.deliveryid)) : null;
            return ss ? stepColor(ss - 1) : r.color;
          };
        }

        for (let i = 0; i < path.length - 1; i++) {
          allSegs.push({
            from: path[i],
            to: path[i + 1],
            color: pairColorAt(i),
            delay: (parseInt(r.id.slice(-3)) || 0) * 0.05 + (parseInt(tNum) * 40) + i * (isKitchenAerial ? 40 : 8)
          });
        }
      });
    });

    allSegs.sort((a, b) => a.delay - b.delay);
    allSegs.forEach((s, idx) => {
      setTimeout(() => {
        setAnimatedSegments(prev => [...prev, s]);
        if (idx === allSegs.length - 1) {
          setTimeout(() => {
            setIsAnimating(false);
            setAnimatedActualProgress({});
          }, 1000);
        }
      }, s.delay);
    });

    // Compare-mode: progressively DRAW the actual-GPS polylines on the right
    // map — same visual style as the planned animation on the left (line
    // grows from start to end), but driven by a single rAF loop instead of
    // hundreds of per-pair setTimeouts. For each step we know:
    //   stepStart = idx * perStep       — when this step begins drawing
    //   stepEnd   = stepStart + perStep — when its polyline is fully drawn
    //   progress  = ceil(t * positions.length) where t = (now - stepStart) / perStep
    // The tick updates one state object containing all steps' progress so
    // React batches the re-render across the whole right map.
    if (isCompareFocused && riderActualTracks.length > 0) {
      const tracksSnapshot = [...riderActualTracks];
      // Snapshot the resolved positions per step at animation start. Prefer
      // OSRM-snapped path when available so the drawing follows real roads;
      // fall back to Kalman-smoothed coords otherwise.
      const stepPositions = tracksSnapshot.map((t) => {
        const snap = osrmTrackRoutes[t.deliveryid];
        if (Array.isArray(snap) && snap.length >= 2) return snap;
        return t.coords.map((p) => [p.lat, p.lng]);
      });
      const lastDelay = allSegs.length > 0
        ? allSegs[allSegs.length - 1].delay
        : tracksSnapshot.length * 800;
      const totalDuration = Math.max(lastDelay, tracksSnapshot.length * 600);
      const perStep = totalDuration / Math.max(1, tracksSnapshot.length);
      const animStartTs = Date.now();

      const tick = () => {
        if (!isAnimatingRef.current) return; // user clicked Stop
        const elapsed = Date.now() - animStartTs;
        const next = {};
        tracksSnapshot.forEach((t, idx) => {
          const positions = stepPositions[idx];
          if (!positions || positions.length < 2) return;
          const stepStart = idx * perStep;
          const stepEnd = stepStart + perStep;
          if (elapsed >= stepEnd) {
            next[t.sequenceStep] = positions.length;
          } else if (elapsed >= stepStart) {
            const ratio = (elapsed - stepStart) / perStep;
            next[t.sequenceStep] = Math.max(2, Math.ceil(ratio * positions.length));
          }
          // else: step hasn't started — leave undefined (filter will hide)
        });
        setAnimatedActualProgress(next);
        if (elapsed < totalDuration + 200) {
          requestAnimationFrame(tick);
        }
      };
      requestAnimationFrame(tick);
    }
  };

  const createKitchenIcon = (name, focused = false) => L.divIcon({
    className: '',
    iconSize: focused ? [56, 56] : [46, 46],
    iconAnchor: focused ? [28, 28] : [23, 23],
    popupAnchor: [0, focused ? -30 : -24],
    html: `<div class="kitchen-mark${focused ? ' is-focused' : ''}">${(name || 'K').charAt(0).toUpperCase()}</div>`
  });

  const getRiderColor = (rid) => riders.find(r => String(r.id) === String(rid))?.color || '#475569';

  const calculateEstMeters = (riderId, order) => {
    if (!riderId || !order || !hasValidDrop(order)) return null;
    const liveLoc = liveRiderLocations.find((l) => String(l.id) === String(riderId));
    if (!liveLoc) return null;

    const dropLatVal = toNum(order.droplat || order.deliverylat);
    const dropLonVal = toNum(order.droplon || order.deliverylong);
    if (!Number.isFinite(dropLatVal) || !Number.isFinite(dropLonVal)) return null;

    const distKm = haversineKm([liveLoc.lat, liveLoc.lon], [dropLatVal, dropLonVal]);
    return Math.round(distKm * 1000);
  };

  const formatMeters = (meters) => {
    if (meters === null || meters === undefined) return '';
    return meters >= 1000 ? `${(meters / 1000).toFixed(1)} km` : `${meters} m`;
  };


  // Shared rider-card markup, used in the "By Rider" panel and inside the focused-zone detail.
  const renderRiderCard = (r, i) => {
    const total = r.orders.length;
    const delivered = r.orders.filter((o) =>
      FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
    ).length;
    const isDone = total > 0 && delivered >= total;
    const activeOrder = r.orders.find((o) => {
      const s = String(o.orderstatus || '').toLowerCase();
      return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
    });
    const estMeters = activeOrder ? calculateEstMeters(r.id, activeOrder) : null;

    return (
      <div key={r.id} className="rcard" onClick={() => handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s`, borderLeft: `3px solid ${r.color}` }}>
        <div className="rcard-top">
          <div className="rcard-emo" style={{ background: '#f1f5f9', borderColor: '#e2e8f0', color: '#64748b' }}><MdTwoWheeler /></div>
          <div className="rcard-info">
            <div className="rcard-name">{r.riderName}</div>
            <div className="rcard-zone">{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips</div>
          </div>
          <div
            className={`rcard-badge ${isDone ? 'is-done' : ''}`}
            style={isDone ? undefined : { background: '#f1f5f9', color: '#475569' }}
            title={`${delivered} delivered of ${total} total`}
          >
            {delivered}/{total}
          </div>
        </div>
        <div className="bar-bg"><div className="bar-fg" style={{ width: `${Math.min(100, (total / 15) * 100)}%`, background: r.color }}></div></div>
        <div className="rcard-meta">
          <span><Ico><MdStraighten /></Ico>{r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span>
          {estMeters !== null && (
            <span className="rcard-est-meters" title="Estimated distance to next drop location">
              <Ico><MdMyLocation /></Ico>{formatMeters(estMeters)} to drop
            </span>
          )}
          <span><Ico><MdAccountBalanceWallet /></Ico>₹{r.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}</span>
        </div>
        <div className="step-ids">
          {r.orders.slice(0, 15).map(o => <span key={o.orderid} className="step-id">S{o.step}</span>)}
        </div>
      </div>
    );
  };

  // Returns true when the order's centered popup should stay open even after
  // the cursor leaves the marker: either explicitly pinned via click, or the
  // matching compare-step is focused (so clicking a step in the right panel
  // keeps the card visible while the maps recenter).
  const isOrderPopupPinned = (o) => {
    if (!o) return false;
    if (pinnedPopupsRef.current.has(String(o.orderid))) return true;
    if (compareOpen && focusedRider && o.deliveryid != null) {
      const track = riderActualTracks.find((t) => String(t.deliveryid) === String(o.deliveryid));
      if (track && focusedCompareStep === track.sequenceStep) return true;
    }
    return false;
  };

  // Shared order-popup body used by both the planned-route number markers
  // and the Compare mode actual-track drop pins. Extracted so clicking a
  // pin on either layer surfaces the exact same Timeline + Details + KM
  // chips — operators learn the layout once and trust it everywhere.
  // Hover behavior (keep-open while cursor is over the card) is owned by
  // the centered overlay wrapper, not this function.
  const renderOrderPopupContent = (o) => {
    const statusStyle = getStatusStyle(o.orderstatus);
    const orderRiderId = o.rider_id || o.userid;
    const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
    const estMeters = isDelivered ? null : calculateEstMeters(orderRiderId, o);

    return (
      <div style={{ height: '100%', width: '100%' }}>
        <div className="pu-header">
          <div className="pu-header-top">
            <div className="pu-id">ORDER #{o.orderid}</div>
            {o.orderstatus && (
              <span className="pu-status-chip" style={{ background: statusStyle.bg, color: statusStyle.fg }}>
                {statusStyle.label}
              </span>
            )}
          </div>
          <div className="pu-rider">
            <MdTwoWheeler /> <span>{o.rider_name || o.ridername || 'Unassigned'}</span>
          </div>
          {(o.deliverycustomer || o.customername) && (
            <div className="pu-customer" title={o.deliverycustomer || o.customername}>
              <MdMarkunreadMailbox />
              <span>{o.deliverycustomer || o.customername}</span>
            </div>
          )}
          {o.deliveryid != null && (
            <div className="pu-delivery-id">Delivery #{o.deliveryid}</div>
          )}
        </div>

        <div className="pu-body">
          {POPUP_TIMELINE.some((t) => o[t.key]) && (
            <div className="pu-section">
              <div className="pu-section-label">Timeline</div>
              <div className="pu-timeline">
                {POPUP_TIMELINE.map((t) => {
                  const time = formatTimeOnly(o[t.key]);
                  if (!time) return null;
                  return (
                    <div key={t.key} className={`pu-tl-row ${t.final ? 'delivered' : ''}`}>
                      <span className="pu-tl-dot" />
                      <span className="pu-tl-label">{t.label}</span>
                      <span className="pu-tl-time">{time}</span>
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          <div className="pu-section">
            <div className="pu-section-label">Details</div>
            <div className="pu-details-grid">
              {(o.pickupcustomer || o.locationname || o.pickuplocation) && (
                <div className="pu-detail">
                  <div className="pu-detail-icon"><MdRestaurant /></div>
                  <div className="pu-detail-body">
                    <div className="pu-detail-label">Pickup</div>
                    <div
                      className="pu-detail-value"
                      title={o.pickupcustomer || o.locationname || o.pickuplocation}
                    >
                      {o.pickupcustomer || o.locationname || o.pickuplocation}
                    </div>
                  </div>
                </div>
              )}
              {(o.deliverysuburb || o.deliveryaddress) && (
                <div className="pu-detail">
                  <div className="pu-detail-icon"><MdPlace /></div>
                  <div className="pu-detail-body">
                    <div className="pu-detail-label">Drop</div>
                    <div
                      className="pu-detail-value"
                      title={o.deliveryaddress || o.deliverysuburb}
                    >
                      {o.deliverysuburb || extractArea(o.deliveryaddress)}
                    </div>
                  </div>
                </div>
              )}
              {o.zone_name && (
                <div className="pu-detail">
                  <div className="pu-detail-icon"><MdMap /></div>
                  <div className="pu-detail-body">
                    <div className="pu-detail-label">Zone</div>
                    <div className="pu-detail-value" title={o.zone_name}>{o.zone_name}</div>
                  </div>
                </div>
              )}
              {(o.rider_id || o.userid) && (
                <div className="pu-detail">
                  <div className="pu-detail-icon"><MdTwoWheeler /></div>
                  <div className="pu-detail-body">
                    <div className="pu-detail-label">Rider ID</div>
                    <div className="pu-detail-value">#{o.rider_id || o.userid}</div>
                  </div>
                </div>
              )}
            </div>

            {(o.plannedkms != null || o.kms != null || o.actualkms != null || o.riderkms != null || estMeters !== null) && (
              <div className="pu-distance-row">
                {(o.plannedkms != null && o.plannedkms !== '') ? (
                  <div className="pu-distance-chip">
                    <span className="pu-distance-icon"><MdStraighten /></span>
                    <span className="pu-distance-label">Planned</span>
                    <span className="pu-distance-value">{parseFloat(o.plannedkms).toFixed(1)} km</span>
                  </div>
                ) : o.kms != null && o.kms !== '' ? (
                  <div className="pu-distance-chip">
                    <span className="pu-distance-icon"><MdStraighten /></span>
                    <span className="pu-distance-label">Planned</span>
                    <span className="pu-distance-value">{parseFloat(o.kms).toFixed(1)} km</span>
                  </div>
                ) : null}
                {o.actualkms != null && o.actualkms !== '' && (
                  <div className="pu-distance-chip">
                    <span className="pu-distance-icon"><MdStraighten /></span>
                    <span className="pu-distance-label">Actual</span>
                    <span className="pu-distance-value">{parseFloat(o.actualkms).toFixed(1)} km</span>
                  </div>
                )}
                {o.riderkms != null && o.riderkms !== '' && (
                  <div className="pu-distance-chip">
                    <span className="pu-distance-icon"><MdStraighten /></span>
                    <span className="pu-distance-label">Rider KMs</span>
                    <span className="pu-distance-value">{parseFloat(o.riderkms).toFixed(1)} km</span>
                  </div>
                )}
{estMeters !== null && (
                  <div className="pu-distance-chip pu-est-meters" title="Estimated distance to drop location">
                    <span className="pu-distance-icon"><MdMyLocation /></span>
                    <span className="pu-distance-label">Est. to Drop</span>
                    <span className="pu-distance-value">{formatMeters(estMeters)}</span>
                  </div>
                )}
              </div>
            )}
          </div>
        </div>
      </div>
    );
  };

  const renderMarkers = () => {
    // Compare "Actual GPS only" view: drop the planned order markers entirely.
    // The actual-track drop pins rendered later on the same map carry the
    // same step numbers and step-palette colors, so showing both creates
    // duplicate, slightly-offset pins that clutter the view.
    if (compareOpen && focusedRider && compareViewMode === 'actual') return null;

    let ordersToRender = allOrders;
    if (focusedZone) ordersToRender = focusedZone.orders;
    if (focusedKitchen) ordersToRender = focusedKitchen.orders;
    if (focusedRider) ordersToRender = focusedRider.orders;
    ordersToRender = ordersToRender.filter(hasValidDrop);

    // Pre-build the deliveryid → sequenceStep lookup once per render so each
    // marker can resolve its step palette color without an O(N) scan.
    const compareDeliveryToStep =
      compareOpen && focusedRider
        ? new Map(riderActualTracks.map((t) => [String(t.deliveryid), t.sequenceStep]))
        : null;

    return ordersToRender.map((o, idx) => {
      const rid = o.rider_id;
      const active = rid ? activeRiders.has(rid) : true;
      let color = getRiderColor(rid);

      // Compare mode: recolor and renumber the focused rider's markers by
      // sequenceStep (delivery-time order, same as the right map + timeline)
      // instead of rider color + planned step. Both maps now show the same
      // color + number for the same delivery — a step-3 pin on the left
      // planned map matches the step-3 polyline + drop pin on the right.
      let compareSeq;
      if (
        compareDeliveryToStep &&
        rid === focusedRider.id &&
        o.deliveryid != null
      ) {
        const ss = compareDeliveryToStep.get(String(o.deliveryid));
        if (ss) {
          color = stepColor(ss - 1);
          compareSeq = ss;
        }
      }

      const isRiderFocused = !!focusedRider;
      const showNumbers = isRiderFocused || !!focusedKitchen;
      const statusStyle = getStatusStyle(o.orderstatus);
      const statusLow = String(o.orderstatus || '').toLowerCase();
      const isDelivered = statusLow === 'delivered';
      const isPulsing = pulseOrderId && String(pulseOrderId) === String(o.orderid);
      // Flag SVG: pole + swallow-tail banner. A check glyph appears on the banner when delivered.
      const flagSvg = o.orderstatus
        ? `<svg class="cmark-flag" viewBox="0 0 18 22" xmlns="http://www.w3.org/2000/svg">
             <line x1="1.5" y1="0" x2="1.5" y2="22" stroke="#0f172a" stroke-width="1.6" stroke-linecap="round"/>
             <polygon points="2,1 17,1 13.5,5.5 17,10 2,10" fill="${statusStyle.bg}" stroke="#0f172a" stroke-width="0.6" stroke-linejoin="round"/>
             ${isDelivered ? '<polyline points="5,5.5 7,7.5 11,3.5" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' : ''}
           </svg>`
        : '';

      const icon = showNumbers
        ? (() => {
          const seq = compareSeq || o.step || (ordersToRender.indexOf(o) + 1);
          const sz = 32;
          return L.divIcon({
            className: '',
            iconSize: [sz, sz],
            iconAnchor: [sz / 2, sz / 2],
            popupAnchor: [0, -28],
            html: `<div class="cmark is-rider-focused${isPulsing ? ' pulse' : ''}" style="background:${color};width:${sz}px;height:${sz}px;font-size:${seq > 9 ? 12 : 14}px;opacity:${active ? 1 : 0.75}">${seq > 0 ? seq : ''}${flagSvg}</div>`
          });
        })()
        : L.divIcon({
          className: '',
          iconSize: [24, 30],
          iconAnchor: [2, 30],
          popupAnchor: [10, -25], // Lift popup above the flag, not just the larger 32px marker
          html: `<div class="cmark${isPulsing ? ' pulse' : ''}" style="width:24px;height:30px;opacity:${active ? 1 : 0.75}">${flagSvg}</div>`
        });

      return (
        <Marker
          key={`${o.orderid}-${showNumbers ? 'num' : 'flag'}`}
          position={[parseFloat(o.droplat || o.deliverylat), parseFloat(o.droplon || o.deliverylong)]}
          icon={icon}
          zIndexOffset={rid ? 100 : 0}
          ref={(inst) => {
            if (inst) orderMarkerRefs.current[String(o.orderid)] = inst;
            else delete orderMarkerRefs.current[String(o.orderid)];
          }}
          eventHandlers={{
            click: () => {
              const id = String(o.orderid);
              if (pinnedPopupsRef.current.has(id)) {
                pinnedPopupsRef.current.delete(id);
                setCenterPopupOrder(null);
              } else {
                pinnedPopupsRef.current.add(id);
                setCenterPopupOrder(o);
              }
            }
          }}
        />
      );
    });
  };

  const renderRoutes = () => {
    // Compare "Actual GPS only" view hides EVERY planned polyline on the
    // unified compare map — both the static rendering AND the Animate
    // Routes per-pair segments. Without this gate the animation pours the
    // planned route on top of the actual GPS in Actual-only mode, which
    // is the opposite of what that view exists to show. The actual-track
    // polylines render their own progressive draw via animatedActualProgress
    // (see riderActualTracks.map inside the MapContainer), so the actual
    // animation still plays correctly when planned is hidden.
    const hidePlanned = compareOpen && focusedRider && compareViewMode === 'actual';

    if (isAnimating) {
      if (hidePlanned) return [];
      return animatedSegments.map((s, i) => (
        <Polyline key={i} positions={[s.from, s.to]} pathOptions={{ color: s.color, weight: 6, opacity: 0.9, lineJoin: 'round', lineCap: 'round' }} />
      ));
    }

    const routes = [];
    const zoneRiderIds = focusedZone ? new Set(focusedZone.riders.map((zr) => String(zr.rider_id))) : null;
    if (hidePlanned) return routes;
    // visibleRiders === riders in every view except "All Active Routes", where
    // it's pre-filtered to riders whose live GPS is currently active.
    visibleRiders.forEach(r => {
      const isActive = activeRiders.has(r.id);
      if (focusedRider && focusedRider.id !== r.id) return;
      if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;
      if (zoneRiderIds && !zoneRiderIds.has(String(r.id))) return;

      // In the Active view, collapse the rider down to just their in-progress
      // delivery so the map draws only that one leg's polyline instead of the
      // whole day's route. Every other view keeps the full order list.
      const activeOrder = isAllActiveView ? getActiveOrder(r.orders) : null;
      const rOrders = isAllActiveView ? (activeOrder ? [activeOrder] : []) : r.orders;
      if (isAllActiveView && rOrders.length === 0) return;
      const trips = {};
      rOrders.forEach(o => {
        const t = o.trip_number || 1;
        if (!trips[t]) trips[t] = [];
        trips[t].push(o);
      });

      Object.entries(trips).forEach(([tNum, tOrders]) => {
        // Filter orders by focused kitchen if active
        const filteredTOrders = focusedKitchen
          ? tOrders.filter(o => (o.pickupcustomer || o.kitchen_key || 'Unknown').toLowerCase().trim() === focusedKitchen.id)
          : tOrders;

        if (filteredTOrders.length === 0) return;

        // Use the full trip orders (unfiltered by kitchen) to build the cache key
        // so it matches the key used to fetch the route in the useEffect.
        const fullSorted = [...tOrders].sort((a, b) => (a.step || 0) - (b.step || 0));
        const fullPts = buildTripPoints(fullSorted);
        const cacheKey = getTripCacheKey(r.id, tNum, fullPts);

        const roadPoints = osrmRoutes[cacheKey];
        const sorted = [...filteredTOrders].sort((a, b) => (a.step || 0) - (b.step || 0));

        // Active view: build a route scoped to just the in-progress leg and FOLLOW THE ROADS.
        //
        // Leg endpoints: the rider's CURRENT live GPS position → the drop. The
        // live position is preferred because (a) active riders always have a
        // live GPS fix in this view, so the leg always has 2 valid points even
        // when the order carries no pickup coordinates — the previous cause of a
        // completely missing line — and (b) it shows the rider's REMAINING route
        // to the customer. Falls back to the pickup when there's no live fix.
        const activeLegOrder = isAllActiveView ? sorted[0] : null;
        let activeStraightLeg = null;
        if (isAllActiveView && activeLegOrder) {
          const lp = liveRiderLocations.find((l) => String(l.id) === String(r.id));
          const start = (lp && Number.isFinite(lp.lat) && Number.isFinite(lp.lon))
            ? [lp.lat, lp.lon]
            : (hasValidPickup(activeLegOrder)
              ? [toNum(pickupLat(activeLegOrder)), toNum(pickupLon(activeLegOrder))]
              : null);
          const dLat = toNum(activeLegOrder.droplat || activeLegOrder.deliverylat);
          const dLon = toNum(activeLegOrder.droplon || activeLegOrder.deliverylong);
          if (start && Number.isFinite(dLat) && Number.isFinite(dLon)) {
            activeStraightLeg = [start, [dLat, dLon]];
          }
        }
        // Prefer the OSRM road-following route for the active leg (fetched as
        // `${r.id}-active-${orderid}` in the route-fetch effect); other views
        // keep the per-trip route.
        const roadToUse = isAllActiveView
          ? (activeLegOrder && activeStraightLeg ? osrmRoutes[getTripCacheKey(r.id, `active-${activeLegOrder.orderid}`, activeStraightLeg)] : undefined)
          : roadPoints;
        const hasRoad = Array.isArray(roadToUse) && roadToUse.length >= 2;
        const failed = roadToUse === false;
        // Other views: wait for OSRM so we don't flash an aerial line before the
        // road polyline lands. Active view: NEVER wait — draw the straight leg
        // immediately and let it upgrade to the road-following route, so the
        // line is never invisible while OSRM is in flight.
        if (!isAllActiveView && !hasRoad && !failed) return;

        const finalPoints = hasRoad
          ? roadToUse
          : (isAllActiveView ? activeStraightLeg : buildTripPoints(sorted));
        if (!finalPoints || finalPoints.length < 2) return;

        const isKitchenView = (viewMode === 'kitchens' || focusedKitchen);
        const opacity = isActive ? 1.0 : 0.1;
        const weight = isKitchenView ? 7 : 6;
        // Aerial fallback (OSRM permanently failed) is rendered dashed so it visually
        // reads as an estimate vs. an actual routed road polyline.
        // In Combined mode (planned + actual overlaid on a single map), the
        // planned polyline renders dashed so the operator can visually tell
        // it apart from the solid actual-GPS polyline drawn for the same step
        // in the same step-palette color. OSRM-failed segments stay dashed
        // either way — the existing aerial-fallback signal still wins.
        const isCompareTargetForDash =
          compareOpen && focusedRider && r.id === focusedRider.id;
        const combinedPlannedDash =
          isCompareTargetForDash && compareViewMode === 'combined' ? '6 5' : undefined;
        const dashArray = failed ? '8 6' : combinedPlannedDash;

        // Compare mode: recolor the planned polyline per step so the left
        // (planned) map's segment for delivery X uses the same STEP_PALETTE
        // color as the right (actual) map's GPS polyline for delivery X and
        // the timeline dot for X. Without this, the operator can't visually
        // link a step on the timeline to its planned segment on the left.
        const isCompareTarget =
          compareOpen && focusedRider && r.id === focusedRider.id;
        if (isCompareTarget) {
          const deliveryToStep = new Map(
            riderActualTracks.map((t) => [String(t.deliveryid), t.sequenceStep])
          );
          const validDrops = sorted.filter(hasValidDrop);
          const dropCoords = validDrops.map((o) => [
            parseFloat(o.droplat || o.deliverylat),
            parseFloat(o.droplon || o.deliverylong)
          ]);
          // hasRoad: split the OSRM polyline at each drop's nearest index.
          // !hasRoad: finalPoints is [pickup?, drop1, drop2, ...] — buildTripPoints
          // only prepends the pickup when one's available. Detect that and
          // align segment[i] with validDrops[i] either way.
          let segments;
          if (hasRoad) {
            segments = splitPolylineByDrops(finalPoints, dropCoords);
          } else {
            const hasPickup = finalPoints.length > dropCoords.length;
            segments = [];
            for (let i = 0; i < dropCoords.length; i++) {
              const idxA = hasPickup ? i : i - 1;
              const idxB = hasPickup ? i + 1 : i;
              if (
                idxA < 0 ||
                idxA >= finalPoints.length ||
                idxB >= finalPoints.length
              ) {
                segments.push([]);
              } else {
                segments.push([finalPoints[idxA], finalPoints[idxB]]);
              }
            }
          }
          // Combined view rail-offset: shift the planned route +5px
          // perpendicular to its direction of travel so it sits *next to*
          // the actual GPS polyline (which gets -5px below) instead of
          // stacking on the same pixels. When the rider follows the
          // dispatched route, the two layers now read as parallel rails
          // hugging the same road; when they diverge, the rails fan apart
          // and the deviation is unmissable. Planned-only and Actual-only
          // modes keep offset = 0 since there's only one layer to draw.
          const plannedOffset = compareViewMode === 'combined' ? 5 : 0;
          // One halo under the whole trip so crossing roads still read as
          // a single planned route. Per-step segments draw on top with their
          // step's color.
          routes.push(
            <Polyline
              key={`${r.id}-${tNum}-halo`}
              positions={finalPoints}
              pathOptions={{
                color: '#ffffff',
                weight: weight + 4,
                opacity: opacity * 0.5,
                lineJoin: 'round',
                lineCap: 'round',
                offset: plannedOffset
              }}
            />
          );
          segments.forEach((seg, i) => {
            if (!seg || seg.length < 2) return;
            const order = validDrops[i];
            const sequenceStep = order
              ? deliveryToStep.get(String(order.deliveryid))
              : null;
            // Combined view collapses the per-step palette to a single
            // indigo so the planned polyline reads as one layer; the
            // numbered drop pins (which keep STEP_PALETTE) still carry
            // the per-step identity. Planned-only mode keeps per-step
            // colors because there's no second layer to distinguish from.
            const color = compareViewMode === 'combined'
              ? COMBINED_PLANNED_COLOR
              : (sequenceStep ? stepColor(sequenceStep - 1) : r.color);
            const isFocusedThisStep =
              focusedCompareStep != null && focusedCompareStep === sequenceStep;
            // Focused segment pops; non-focused dim when *some* step is
            // focused so the active one stands out. When no step is focused
            // every segment renders at full opacity.
            const segWeight = isFocusedThisStep ? weight + 1.5 : weight;
            const segOpacity = isFocusedThisStep
              ? 1
              : focusedCompareStep
                ? opacity * 0.5
                : opacity;
            routes.push(
              <Polyline
                key={`${r.id}-${tNum}-step-${i}-${order?.deliveryid || i}`}
                positions={seg}
                pathOptions={{
                  color,
                  weight: segWeight,
                  opacity: segOpacity,
                  lineJoin: 'round',
                  lineCap: 'round',
                  dashArray,
                  offset: plannedOffset
                }}
              />
            );
          });
          return; // skip the default single-color render below
        }

        routes.push(
          <React.Fragment key={`${r.id}-${tNum}`}>
            <Polyline positions={finalPoints} pathOptions={{ color: '#ffffff', weight: weight + 4, opacity: opacity * 0.5, lineJoin: 'round', lineCap: 'round' }} />
            <Polyline positions={finalPoints} pathOptions={{ color: r.color, weight, opacity, lineJoin: 'round', lineCap: 'round', dashArray }} />
          </React.Fragment>
        );
      });
    });
    return routes;
  };

  const toggleRider = (rid) => {
    const newActive = new Set(activeRiders);
    if (newActive.has(rid)) newActive.delete(rid);
    else newActive.add(rid);
    setActiveRiders(newActive);
  };

  return (
    <div className={`dispatch-container${embedded ? ' embedded' : ''}${compareOpen ? ' compare-open' : ''}`}>
      {!embedded && (
        <div id="hdr">
          <div className="logo">
            <div className="logo-badge">D</div>
            <div className="logo-name">Dispatch</div>
            {appLocations && appLocations.length > 0 && (
              <div className="logo-city-wrap" ref={locationMenuRef}>
                <button
                  type="button"
                  className={`logo-city ${locationMenuOpen ? 'open' : ''}`}
                  onClick={() => setLocationMenuOpen((v) => !v)}
                  aria-haspopup="listbox"
                  aria-expanded={locationMenuOpen}
                  title="Switch hub"
                >
                  <MdPlace />
                  <span className="logo-city-text">{locationName || 'All locations'}</span>
                  <MdExpandMore className="logo-city-caret" />
                </button>
                {locationMenuOpen && (
                  <div className="logo-city-menu" role="listbox">
                    {appLocations.map((loc) => {
                      const isActive = String(loc.applocationid) === String(selectedAppLocationId);
                      return (
                        <button
                          key={loc.applocationid}
                          type="button"
                          role="option"
                          aria-selected={isActive}
                          className={`logo-city-option ${isActive ? 'active' : ''}`}
                          onClick={() => handleLocationPick(loc.applocationid)}
                        >
                          <MdPlace className="logo-city-option-icon" />
                          <span>{loc.locationname}</span>
                          {isActive && <span className="logo-city-option-check">✓</span>}
                        </button>
                      );
                    })}
                  </div>
                )}
              </div>
            )}
            <div id="dispatch-top-tabs" className="dtt-inline">
              <button
                type="button"
                className={`dtt-tab ${topView === 'live' ? 'active' : ''}`}
                onClick={() => setTopView('live')}
              >
                <span className="dtt-icon"><MdMap /></span>
                Live
              </button>
              <button
                type="button"
                className={`dtt-tab ${topView === 'analysis' ? 'active' : ''}`}
                onClick={() => setTopView('analysis')}
              >
                <span className="dtt-icon"><MdInsights /></span>
                Analysis
              </button>
            </div>
          </div>

          {/* Header right-cluster: profit/loss chip, total-orders pill, date picker.
              Sits to the LEFT of the running clock so the operator sees fleet
              health + current wave size + selected date together in one row. */}
          <div className="hdr-stats">
            {(() => {
              const isLoss = activeStats.profit < 0;
              const amount = Math.abs(activeStats.profit);
              return (
                <span
                  className={`strat-stat ${isLoss ? 'strat-stat-loss' : 'strat-stat-profit'}`}
                  title={`${isLoss ? 'Loss' : 'Revenue'} (${activeStats.label})`}
                >
                  <span className="strat-stat-icon">{isLoss ? <MdTrendingDown /> : <MdTrendingUp />}</span>
                  <span className="strat-stat-label">{isLoss ? 'Loss' : 'Revenue'}</span>
                  <span className="strat-stat-value">{isLoss ? '-' : ''}₹{amount.toFixed(0)}</span>
                </span>
              );
            })()}

            {shouldFetchLive && (
              <>
                {liveIsFetching && (
                  <span className="live-status">
                    <span className="live-dot" /> Loading {liveRows.length ? `· ${liveRows.length} loaded` : ''}
                  </span>
                )}
                {!liveIsFetching && !liveIsError && (
                  <span className="live-status live-status-ready">
                    <span className="live-dot ready" /> {filteredLiveRows.length} orders
                    <span className="live-status-sub"> / {liveRows.length} total</span>
                  </span>
                )}
                {liveIsError && (
                  <span className="live-status live-status-error">
                    <span className="live-dot error" /> Failed to load
                  </span>
                )}
                {(() => {
                  // Date-picker chip + custom calendar popover. Replaces the
                  // OS-native <input type="date"> dialog (which looks different
                  // on every browser and can't pick up the design system) with
                  // a single popover that always renders the same way. The
                  // chip has three regions:
                  //   • prev-day arrow  — one-click ±1 day scrubbing
                  //   • center card     — opens the calendar popover on click
                  //   • next-day arrow  — disabled when viewing today
                  // The popover itself carries the month grid + quick presets.
                  const today = dayjs().startOf('day');
                  const todayStr = today.format('YYYY-MM-DD');
                  const picked = dayjs(selectedDate);
                  const isToday = selectedDate === todayStr;
                  const isFuture = picked.isAfter(today, 'day');

                  const commitDate = (next) => {
                    if (!next) return;
                    const str = next.format('YYYY-MM-DD');
                    if (str === selectedDate) {
                      setDatePickerOpen(false);
                      return;
                    }
                    if (next.isAfter(today, 'day')) return; // guard future
                    setSelectedDate(str);
                    handleRiderFocus(null);
                    setFocusedKitchen(null);
                    setFocusedZone(null);
                    setDatePickerOpen(false);
                  };

                  const goPrevDay = () => commitDate(picked.subtract(1, 'day'));
                  const goNextDay = () => {
                    if (isToday || isFuture) return;
                    commitDate(picked.add(1, 'day'));
                  };

                  // Build a fixed 6×7 grid of dayjs instances starting from
                  // the Sunday before the month's first day. Days outside the
                  // visible month render as faded "other-month" cells so the
                  // grid never jumps in height as months change.
                  const monthStart = calViewMonth.startOf('month');
                  const gridStart = monthStart.subtract(monthStart.day(), 'day');
                  const cells = Array.from({ length: 42 }, (_, i) => gridStart.add(i, 'day'));

                  const prevMonth = () => setCalViewMonth((m) => m.subtract(1, 'month'));
                  const nextMonth = () => {
                    const candidate = calViewMonth.add(1, 'month');
                    // Allow navigating into the current month (so "today" can
                    // be reached) but not into purely-future months.
                    if (candidate.startOf('month').isAfter(today, 'month')) return;
                    setCalViewMonth(candidate);
                  };
                  const canGoNextMonth = !calViewMonth.add(1, 'month').startOf('month').isAfter(today, 'month');

                  const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

                  return (
                    <div className={`date-chip${isToday ? ' is-today' : ''}${datePickerOpen ? ' is-open' : ''}`} ref={datePickerRef}>
                      <button
                        type="button"
                        className="date-chip-nav"
                        onClick={goPrevDay}
                        aria-label="Previous day"
                        title="Previous day"
                      >
                        <MdChevronLeft />
                      </button>
                      <button
                        type="button"
                        className="date-chip-main"
                        onClick={() => setDatePickerOpen((o) => !o)}
                        aria-haspopup="dialog"
                        aria-expanded={datePickerOpen}
                      >
                        <span className="date-chip-icon" aria-hidden="true">
                          <MdCalendarToday />
                        </span>
                        <span className="date-chip-text">
                          <span className="date-chip-label">
                            Date{isToday && <span className="date-chip-today-pill">Today</span>}
                          </span>
                          <span className="date-chip-value">
                            {picked.isValid() ? picked.format('ddd, MMM D, YYYY') : '—'}
                          </span>
                        </span>
                        <span className={`date-chip-chevron${datePickerOpen ? ' is-open' : ''}`} aria-hidden="true">
                          <MdExpandMore />
                        </span>
                      </button>
                      <button
                        type="button"
                        className="date-chip-nav"
                        onClick={goNextDay}
                        disabled={isToday || isFuture}
                        aria-label="Next day"
                        title={isToday ? "You're viewing today" : 'Next day'}
                      >
                        <MdChevronRight />
                      </button>

                      {datePickerOpen && (
                        <div className="date-cal-popover" role="dialog" aria-label="Pick a date">
                          {/* Month header — month/year title flanked by
                              prev/next arrows. The next-month arrow disables
                              once we'd cross into a purely-future month so
                              the operator never lands on a month they can't
                              pick a date in. */}
                          <div className="date-cal-header">
                            <button
                              type="button"
                              className="date-cal-nav"
                              onClick={prevMonth}
                              aria-label="Previous month"
                            >
                              <MdChevronLeft />
                            </button>
                            <div className="date-cal-title">
                              {calViewMonth.format('MMMM YYYY')}
                            </div>
                            <button
                              type="button"
                              className="date-cal-nav"
                              onClick={nextMonth}
                              disabled={!canGoNextMonth}
                              aria-label="Next month"
                            >
                              <MdChevronRight />
                            </button>
                          </div>

                          <div className="date-cal-weekdays">
                            {WEEKDAYS.map((w) => (
                              <div key={w} className="date-cal-weekday">{w}</div>
                            ))}
                          </div>

                          <div className="date-cal-grid">
                            {cells.map((d) => {
                              const inMonth = d.month() === calViewMonth.month();
                              const isSel = d.format('YYYY-MM-DD') === selectedDate;
                              const isTodayCell = d.format('YYYY-MM-DD') === todayStr;
                              const disabled = d.isAfter(today, 'day');
                              const cls = [
                                'date-cal-day',
                                !inMonth && 'is-other-month',
                                isSel && 'is-selected',
                                isTodayCell && 'is-today',
                                disabled && 'is-disabled'
                              ].filter(Boolean).join(' ');
                              return (
                                <button
                                  key={d.format('YYYY-MM-DD')}
                                  type="button"
                                  className={cls}
                                  disabled={disabled}
                                  onClick={() => commitDate(d)}
                                  aria-current={isTodayCell ? 'date' : undefined}
                                  aria-pressed={isSel}
                                >
                                  {d.date()}
                                </button>
                              );
                            })}
                          </div>

                          {/* Quick presets — the three dates ops scrub to
                              most often. Saves a month-nav + a day-click for
                              the common cases. */}
                          <div className="date-cal-presets">
                            <button
                              type="button"
                              className="date-cal-preset"
                              onClick={() => commitDate(today)}
                            >
                              Today
                            </button>
                            <button
                              type="button"
                              className="date-cal-preset"
                              onClick={() => commitDate(today.subtract(1, 'day'))}
                            >
                              Yesterday
                            </button>
                            <button
                              type="button"
                              className="date-cal-preset"
                              onClick={() => commitDate(today.subtract(7, 'day'))}
                            >
                              −7 days
                            </button>
                          </div>
                        </div>
                      )}
                    </div>
                  );
                })()}
              </>
            )}
          </div>

          <div id="clock">{clock}</div>
        </div>
      )}

      {(embedded || topView === 'live') && (<>
        <div id="strat-row">
          <button className={`sbt ${viewMode === 'kitchens' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Location'); setViewMode('kitchens'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPlace /></span> By Location</button>
          <button
            className={`sbt ${viewMode === 'zones' ? 'active' : ''}`}
            onClick={() => { logger.info('View mode changed: By Zone'); setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}
          ><span className="sbt-icon"><MdMap /></span> By Zone</button>
          <button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Rider'); setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button>
          {!embedded && (
            <button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: All Active Routes'); setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span>Active</button>
          )}
          <button
            type="button"
            className={`sbt sbt-profitability ${viewMode === 'profitability' ? 'active' : ''}`}
            onClick={() => {
              logger.info('View mode changed: Profitability');
              setViewMode('profitability');
              handleRiderFocus(null);
              setFocusedKitchen(null);
              setFocusedZone(null);
            }}
            title="View overall and rider profitability"
          >
            <span className="sbt-icon"><MdAttachMoney /></span> Profitability
          </button>
          <button
            type="button"
            className={`sbt sbt-rider-info ${viewMode === 'rider-info' ? 'active' : ''}`}
            onClick={() => {
              setViewMode('rider-info');
              handleRiderFocus(null);
              setFocusedKitchen(null);
              setFocusedZone(null);
            }}
            title="Live GPS / battery / status snapshot for a rider"
          >
            <span className="sbt-icon"><MdInfoOutline /></span> Rider Info
          </button>
        </div>

        {shouldFetchLive && viewMode !== 'rider-info' && (
          <div id="batch-row">
            <span className="batch-label">Batch</span>
            {/* Status-wise (time-field) filter is hidden for now per spec —
              bucketing is locked to `assigntime`. Restore this block to bring
              back the Delivered/Pending/Assigned/... dropdown.
          <div className="time-field-wrap" ref={timeFieldMenuRef}>
            <button
              type="button"
              className={`time-field-btn ${timeFieldMenuOpen ? 'open' : ''}`}
              onClick={() => setTimeFieldMenuOpen((v) => !v)}
              aria-haspopup="listbox"
              aria-expanded={timeFieldMenuOpen}
              title="Bucket slots by this timestamp"
            >
              <MdAccessTime />
              <span className="time-field-text">{TIME_FIELDS.find((f) => f.id === selectedTimeField)?.label || 'Delivered'}</span>
              <MdExpandMore className="time-field-caret" />
            </button>
            {timeFieldMenuOpen && (
              <div className="time-field-menu" role="listbox">
                {TIME_FIELDS.map((f) => {
                  const isActive = f.id === selectedTimeField;
                  return (
                    <button
                      key={f.id}
                      type="button"
                      role="option"
                      aria-selected={isActive}
                      className={`time-field-option ${isActive ? 'active' : ''}`}
                      onClick={() => {
                        setSelectedTimeField(f.id);
                        setTimeFieldMenuOpen(false);
                      }}
                    >
                      <MdAccessTime className="time-field-option-icon" />
                      <span>{f.label}</span>
                      {isActive && <span className="time-field-option-check">✓</span>}
                    </button>
                  );
                })}
              </div>
            )}
          </div>
          */}
            {/* Slot editor (Edit slots button + panel) is hidden for now per
              spec — the three batches (Morning / Afternoon / Evening) are
              fixed. Restore this block to bring back the operator-editable
              start/end hours, add-slot, and reset-to-defaults controls.
          <div className="slot-edit-wrap" ref={slotEditRef}>
            <button
              type="button"
              className={`slot-edit-btn ${slotEditOpen ? 'open' : ''}`}
              onClick={() => setSlotEditOpen((v) => !v)}
              aria-haspopup="dialog"
              aria-expanded={slotEditOpen}
              title="Adjust slot timings"
            >
              <MdAccessTime />
              <span>Edit slots</span>
            </button>
            {slotEditOpen && (
              <div className="slot-edit-panel" role="dialog" aria-label="Edit slot timings">
                <div className="slot-edit-head">
                  <div className="slot-edit-title">Slot timings</div>
                  <div className="slot-edit-sub">Hours are 0–24 (24h clock). Half-hour steps allowed (e.g. 12.5 = 12:30). Start &lt; End.</div>
                </div>
                <div className="slot-edit-list">
                  {slotsConfig.map((s, idx) => (
                    <div key={s.id} className="slot-edit-row">
                      <span className="slot-edit-idx">{idx + 1}</span>
                      <label className="slot-edit-field">
                        <span className="slot-edit-field-label">Start</span>
                        <input
                          type="number"
                          min={0}
                          max={23.5}
                          step={0.5}
                          value={s.startHour}
                          onChange={(e) => {
                            const raw = parseFloat(e.target.value);
                            const snapped = Number.isFinite(raw) ? Math.round(raw * 2) / 2 : 0;
                            const v = Math.max(0, Math.min(23.5, snapped));
                            setSlotsConfig((cur) => cur.map((row, i) =>
                              i === idx
                                ? { ...row, startHour: v, label: formatSlotLabel(i, v), range: formatSlotRange(v, row.endHour) }
                                : row
                            ));
                          }}
                        />
                      </label>
                      <label className="slot-edit-field">
                        <span className="slot-edit-field-label">End</span>
                        <input
                          type="number"
                          min={0.5}
                          max={24}
                          step={0.5}
                          value={s.endHour}
                          onChange={(e) => {
                            const raw = parseFloat(e.target.value);
                            const snapped = Number.isFinite(raw) ? Math.round(raw * 2) / 2 : 0.5;
                            const v = Math.max(0.5, Math.min(24, snapped));
                            setSlotsConfig((cur) => cur.map((row, i) =>
                              i === idx
                                ? { ...row, endHour: v, range: formatSlotRange(row.startHour, v) }
                                : row
                            ));
                          }}
                        />
                      </label>
                      <span className="slot-edit-preview" title={`${formatSlotLabel(idx, s.startHour)} — ${formatSlotRange(s.startHour, s.endHour)}`}>
                        {formatSlotRange(s.startHour, s.endHour)}
                      </span>
                      <button
                        type="button"
                        className="slot-edit-remove"
                        onClick={() => setSlotsConfig((cur) => cur.filter((_, i) => i !== idx).map((row, i) => ({
                          ...row,
                          id: `slot-${i + 1}`,
                          label: formatSlotLabel(i, row.startHour)
                        })))}
                        disabled={slotsConfig.length <= 1}
                        title={slotsConfig.length <= 1 ? 'Keep at least one slot' : 'Remove this slot'}
                      >
                        ×
                      </button>
                    </div>
                  ))}
                </div>
                <div className="slot-edit-actions">
                  <button
                    type="button"
                    className="slot-edit-add"
                    onClick={() => setSlotsConfig((cur) => {
                      const last = cur[cur.length - 1];
                      const start = Math.min(23, (last?.endHour ?? 0));
                      const end = Math.min(24, start + 1);
                      const i = cur.length;
                      return [
                        ...cur,
                        {
                          id: `slot-${i + 1}`,
                          startHour: start,
                          endHour: end,
                          label: formatSlotLabel(i, start),
                          range: formatSlotRange(start, end)
                        }
                      ];
                    })}
                  >
                    + Add slot
                  </button>
                  <button
                    type="button"
                    className="slot-edit-reset"
                    onClick={() => setSlotsConfig(BATCHES_DEFAULT)}
                  >
                    Reset to defaults
                  </button>
                </div>
              </div>
            )}
          </div>
          */}
            {/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls
              horizontally when it overflows. */}
            <div className="batch-scroll">
              {BATCHES.map((b) => {
                const isActive = selectedBatch === b.id;
                return (
                  <button
                    key={b.id}
                    ref={isActive ? activeBatchRef : null}
                    className={`batch-btn batch-slot ${isActive ? 'active' : ''}`}
                    onClick={() => {
                      setSelectedBatch(b.id);
                      handleRiderFocus(null);
                      setFocusedKitchen(null);
                      setFocusedZone(null);
                    }}
                    title={`${b.label} (${b.range})`}
                  >
                    <span className="batch-btn-label">{b.label}</span>
                    <span className="batch-btn-count">{batchCounts[b.id] ?? 0}</span>
                  </button>
                );
              })}
            </div>
          </div>
        )}

        {viewMode === 'profitability' ? (
          <ProfitabilitySection
            riders={riders}
            handleRiderFocus={handleRiderFocus}
            focusedRider={focusedRider}
            totalDailyProfit={totalDailyProfit}
            selectedDate={selectedDate}
            batches={BATCHES}
          />
        ) : viewMode === 'rider-info' ? (
          <div className="rider-info-mode">
            <div className="ri-sidebar">
              <div className="ri-sb-head">
                <div className="ri-sb-title">Riders</div>
                <div className="ri-sb-sub">{ridersAllDay.length} {ridersAllDay.length === 1 ? 'rider' : 'riders'} today</div>
              </div>
              <div className="ri-search">
                <MdSearch className="ri-search-icon" />
                <input
                  type="text"
                  className="ri-search-input"
                  placeholder="Search by name or ID"
                  value={riderInfoSearch}
                  onChange={(e) => setRiderInfoSearch(e.target.value)}
                />
              </div>
              {(() => {
                const q = riderInfoSearch.trim().toLowerCase();
                const matched = ridersAllDay.filter((r) => {
                  if (!q) return true;
                  return String(r.riderName || '').toLowerCase().includes(q) || String(r.id).includes(q);
                });
                if (matched.length === 0) {
                  return <div className="ri-empty">{riderInfoSearch ? `No riders match "${riderInfoSearch}"` : 'No riders have orders today'}</div>;
                }
                return (
                  <div className="ri-rider-list">
                    {matched.map((r) => {
                      const isActive = String(riderInfoUserid) === String(r.id);
                      return (
                        <button
                          key={r.id}
                          type="button"
                          className={`ri-rider-item ${isActive ? 'active' : ''}`}
                          onClick={() => setRiderInfoUserid(r.id)}
                        >
                          <span className="ri-rider-dot" style={{ background: getStableRiderColor(r.id) }} />
                          <span className="ri-rider-info-block">
                            <span className="ri-rider-name">{r.riderName}</span>
                            <span className="ri-rider-meta">#{r.id}</span>
                          </span>
                          <span className="ri-rider-arrow">→</span>
                        </button>
                      );
                    })}
                  </div>
                );
              })()}
            </div>

            <div className="ri-main">
              {riderInfoUserid == null ? (
                <div className="ri-placeholder">
                  <div className="ri-placeholder-icon"><MdInfoOutline /></div>
                  <div className="ri-placeholder-title">Pick a rider</div>
                  <div className="ri-placeholder-sub">
                    Select a rider from the list on the left to see their live GPS,
                    battery, connection, and current order snapshot.
                  </div>
                </div>
              ) : (
                <>
                  {riderInfoFetching && !riderInfoData && (
                    <div className="ri-loading">Loading rider snapshot…</div>
                  )}

                  {riderInfoIsError && (
                    <div className="ri-error">
                      Couldn't load this rider's log. {riderInfoError?.message || ''}
                    </div>
                  )}

                  {riderInfoData && (() => {
                    const d = riderInfoData;
                    const lat = parseFloat(d.latitude);
                    const lon = parseFloat(d.longitude);
                    const hasCoords = Number.isFinite(lat) && Number.isFinite(lon);
                    const batteryNum = parseInt(String(d.battery || '').replace('%', ''), 10);
                    const batteryLow = Number.isFinite(batteryNum) && batteryNum <= 20;
                    const speedNum = parseFloat(d.speed);
                    const statusKey = String(d.status || '').toLowerCase();
                    return (
                      <div className="ri-snapshot">
                        <div className="ri-snap-head">
                          <div className="ri-snap-name">{d.username || `Rider #${d.userid}`}</div>
                          <div className="ri-snap-meta">
                            <span>#{d.userid}</span>
                            {d.status && (
                              <span className={`ri-status ri-status-${statusKey}`}>{d.status}</span>
                            )}
                            <span className={`ri-live ${riderInfoFetching ? 'is-refetching' : ''}`}>
                              <span className="ri-live-dot" />
                              {riderInfoFetching ? 'Updating…' : 'Live'}
                            </span>
                          </div>
                          {d.logdate && (
                            <div className="ri-snap-time">
                              <MdAccessTime /> Last seen {d.logdate}
                            </div>
                          )}
                        </div>

                        <div className="ri-snap-grid">
                          <div className={`ri-stat ${batteryLow ? 'ri-stat-warn' : ''}`}>
                            <div className="ri-stat-icon"><MdBatteryFull /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">Battery</div>
                              <div className="ri-stat-value">
                                {d.battery || '—'}
                                {d.is_charging && <span className="ri-stat-tag">Charging</span>}
                              </div>
                            </div>
                          </div>

                          <div className="ri-stat">
                            <div className="ri-stat-icon"><MdSignalCellularAlt /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">Connection</div>
                              <div className="ri-stat-value">{d.connection || '—'}</div>
                            </div>
                          </div>

                          <div className="ri-stat">
                            <div className="ri-stat-icon"><MdMyLocation /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">GPS Accuracy</div>
                              <div className="ri-stat-value">{d.accuracy ? `${d.accuracy} m` : '—'}</div>
                            </div>
                          </div>

                          <div className="ri-stat">
                            <div className="ri-stat-icon"><MdGpsFixed /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">Location Service</div>
                              <div className="ri-stat-value">{d.location_service || '—'}</div>
                            </div>
                          </div>

                          <div className="ri-stat">
                            <div className="ri-stat-icon"><MdSpeed /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">Speed</div>
                              <div className="ri-stat-value">
                                {Number.isFinite(speedNum) ? `${speedNum.toFixed(2)} km/h` : '—'}
                              </div>
                            </div>
                          </div>

                          <div className="ri-stat">
                            <div className="ri-stat-icon"><MdExplore /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">Heading</div>
                              <div className="ri-stat-value">{d.heading != null ? `${d.heading}°` : '—'}</div>
                            </div>
                          </div>

                          <div className="ri-stat">
                            <div className="ri-stat-icon"><MdPower /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">App State</div>
                              <div className="ri-stat-value">{d.is_background ? 'Background' : 'Foreground'}</div>
                            </div>
                          </div>

                          <div className="ri-stat">
                            <div className="ri-stat-icon"><MdInventory2 /></div>
                            <div className="ri-stat-body">
                              <div className="ri-stat-label">Current Order</div>
                              <div className="ri-stat-value">{d.orderid || '—'}</div>
                            </div>
                          </div>
                        </div>

                        {hasCoords && (
                          <div className="ri-map-section">
                            <div className="ri-coords-label">
                              <MdLocationOn /> {lat.toFixed(6)}, {lon.toFixed(6)}
                            </div>
                            <div className="ri-map">
                              {/* `key` forces a remount when the rider changes so the
                                MapContainer re-centers on the new coords (leaflet's
                                center prop is only read on mount). */}
                              <MapContainer
                                key={`${d.userid}-${lat}-${lon}`}
                                center={[lat, lon]}
                                zoom={13}
                                scrollWheelZoom={false}
                                style={{ height: '100%', width: '100%' }}
                              >
                                <TileLayer
                                  url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                                  attribution='&copy; OpenStreetMap contributors'
                                />
                                <Marker position={[lat, lon]}>
                                  {/* Permanent banner above the pin — Nominatim
                                    reverse-geocode tells the operator which
                                    suburb/area the rider is in. Falls back to
                                    a "Locating…" hint while the request is in
                                    flight so the pin never looks unlabeled. */}
                                  <Tooltip
                                    direction="top"
                                    offset={[0, -10]}
                                    permanent
                                    className="ri-area-banner"
                                  >
                                    {riderInfoArea?.area || 'Locating area…'}
                                  </Tooltip>
                                  <Popup>
                                    <div style={{ fontWeight: 700, marginBottom: 2 }}>{d.username || `Rider #${d.userid}`}</div>
                                    {riderInfoArea?.area && (
                                      <div style={{ fontSize: 12, color: '#0f172a', marginBottom: 4 }}>
                                        {riderInfoArea.area}
                                      </div>
                                    )}
                                    <div style={{ fontSize: 11, color: '#64748b' }}>
                                      {d.logdate ? `Last seen ${d.logdate}` : `${lat.toFixed(6)}, ${lon.toFixed(6)}`}
                                    </div>
                                  </Popup>
                                </Marker>
                              </MapContainer>
                            </div>
                          </div>
                        )}
                      </div>
                    );
                  })()}
                </>
              )}
            </div>
          </div>
        ) : (
          <div id="body" className={`${sidebarCollapsed ? 'sidebar-collapsed' : ''} ${compareOpen ? 'compare-mode' : ''} ${compareOpen && compareDataCollapsed ? 'compare-data-collapsed' : ''}`.trim()}>
            <button
              type="button"
              className={`sidebar-toggle-tab${sidebarCollapsed ? ' is-collapsed' : ''}`}
              onClick={() => setSidebarCollapsed((v) => !v)}
              title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
              aria-label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
            >
              {sidebarCollapsed ? <MdChevronRight /> : <MdChevronLeft />}
            </button>
            {compareOpen && focusedRider && (
              <button
                type="button"
                className={`compare-data-toggle-tab${compareDataCollapsed ? ' is-collapsed' : ''}`}
                onClick={() => setCompareDataCollapsed((v) => !v)}
                title={compareDataCollapsed ? 'Show details panel' : 'Hide details panel'}
                aria-label={compareDataCollapsed ? 'Show details panel' : 'Hide details panel'}
              >
                {compareDataCollapsed ? <MdChevronLeft /> : <MdChevronRight />}
              </button>
            )}
            <div id="sidebar">
              {/* Sidebar header — replaces the top-bar meta line. Hidden when a specific
              rider is focused, since the focused-rider view already shows that rider's
              stats prominently (name + Orders/Distance/Profit tiles). */}
              {!focusedRider && (
                <div className="sb-header">
                  <div className="sb-header-top">
                    <div className="sb-header-title">
                      <span className="sb-title-bar" aria-hidden="true" />
                      <span className="sb-title-text">RIDER DISPATCH</span>
                    </div>
                    <span className="sb-header-scope" title={activeStats.label}>
                      <span className="sb-scope-dot" />
                      {activeStats.label}
                    </span>
                  </div>

                  <div className="sb-header-tiles">
                    <div className="sb-tile sb-tile-orders">
                      <span className="sb-tile-icon"><MdInventory2 /></span>
                      <div className="sb-tile-body">
                        <div className="sb-tile-value">{activeStats.orders}</div>
                        <div className="sb-tile-label">{activeStats.orders === 1 ? 'Order' : 'Orders'}</div>
                      </div>
                    </div>
                    <div className="sb-tile sb-tile-riders">
                      <span className="sb-tile-icon"><MdTwoWheeler /></span>
                      <div className="sb-tile-body">
                        <div className="sb-tile-value">{activeStats.riders}</div>
                        <div className="sb-tile-label">{activeStats.riders === 1 ? 'Rider' : 'Riders'}</div>
                      </div>
                    </div>
                  </div>
                </div>
              )}

              {/* Stats strip hidden for now — restore by removing this comment wrapper.
          <div id="stats-strip">
            <div className="sc"><div className="sc-lbl">Orders</div><div className="sc-val g">{activeStats.orders}</div><div className="sc-sub">{activeStats.label}</div></div>
            <div className="sc"><div className="sc-lbl">Riders</div><div className="sc-val">{activeStats.riders}</div><div className="sc-sub">Active</div></div>
            <div className="sc"><div className="sc-lbl">Distance</div><div className="sc-val">{activeStats.km.toFixed(1)} km</div><div className="sc-sub">Kilometers</div></div>
            <div className="sc"><div className="sc-lbl">Profit</div><div className="sc-val g">₹{activeStats.profit.toFixed(0)}</div><div className="sc-sub">Earned</div></div>
          </div>
          */}

              {((focusedRider && !isAllActiveView) || focusedKitchen) ? (
                <div id="route-detail">
                  <button className="rd-back" onClick={() => { handleRiderFocus(null); setFocusedKitchen(null); }}>← Back to {focusedZone ? focusedZone.name : 'list'}</button>
                  {focusedRider ? (
                    <>
                      <div className="rd-rider-name" style={{ color: focusedRider.color }}>{focusedRider.riderName}</div>
                      {(() => {
                        const actualKm = focusedRider.orders.reduce((s, o) => s + parseFloat(o.actualkms || 0), 0);
                        const riderKm = focusedRider.orders.reduce((s, o) => s + parseFloat(o.riderkms || 0), 0);
                        return (
                          <div className="rd-stats-grid">
                            <div className="rd-stat rd-stat-orders">
                              <div className="rd-stat-icon"><MdInventory2 /></div>
                              <div className="rd-stat-value">{focusedRider.orders.length}</div>
                              <div className="rd-stat-label">Orders</div>
                            </div>
                            <div className="rd-stat rd-stat-distance">
                              <div className="rd-stat-icon"><MdStraighten /></div>
                              <div className="rd-stat-value">{actualKm.toFixed(1)}<span className="rd-stat-unit">km</span></div>
                              <div className="rd-stat-label">Actual KMs</div>
                            </div>
                            <div className="rd-stat rd-stat-distance">
                              <div className="rd-stat-icon"><MdStraighten /></div>
                              <div className="rd-stat-value">{riderKm.toFixed(1)}<span className="rd-stat-unit">km</span></div>
                              <div className="rd-stat-label">Trip KMs</div>
                            </div>
                          </div>
                        );
                      })()}
                      {(() => {
                        const trips = {};
                        focusedRider.orders.forEach(o => {
                          const t = o.trip_number || 1;
                          if (!trips[t]) trips[t] = [];
                          trips[t].push(o);
                        });
                        // Identify the rider's currently-going-on order — first non-final,
                        // non-skipped stop in (trip, step) order. Highlighted in light green
                        // so users see at a glance which delivery is in progress.
                        const sortedAll = [...focusedRider.orders].sort((a, b) => {
                          const tA = a.trip_number || 1;
                          const tB = b.trip_number || 1;
                          if (tA !== tB) return tA - tB;
                          return (a.step || 0) - (b.step || 0);
                        });
                        const activeOrder = sortedAll.find((o) => {
                          const s = String(o.orderstatus || '').toLowerCase();
                          return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
                        });
                        const activeOrderId = activeOrder ? activeOrder.orderid : null;

                        // Completion-time epoch for a delivery. Mirrors the Compare
                        // timeline's `tsOf` (Dispatch.js — riderActualTracks builder)
                        // so the sequence number on the sidebar in 'time' mode lines
                        // up with the Compare timeline's sequenceStep.
                        const completionTs = (o) => {
                          const t = o.deliverytime || o.expecteddeliverytime;
                          if (!t) return Number.MAX_SAFE_INTEGER;
                          const d = dayjs(t);
                          return d.isValid() ? d.valueOf() : Number.MAX_SAFE_INTEGER;
                        };

                        const isTimeMode = tripSortMode === 'time';
                        let prevKitchenKey = null;
                        return Object.entries(trips)
                          .sort(([a], [b]) => Number(a) - Number(b))
                          .map(([tNum, tOrders]) => {
                            // 'planned' keeps the trip's incoming step order (the
                            // caller already sorted by trip_number, step). 'time'
                            // re-sorts inside each trip by completion time; step
                            // is the tiebreaker so two deliveries logged in the
                            // same minute still render in dispatched order.
                            // Kitchen-transition badges are computed off the displayed
                            // order so the "Switch to X" rows stay accurate after
                            // reordering — without this they'd reference the
                            // original sequence and confuse the operator.
                            const displayOrders = isTimeMode
                              ? [...tOrders].sort((a, b) => {
                                const diff = completionTs(a) - completionTs(b);
                                if (diff !== 0) return diff;
                                return (a.step || 0) - (b.step || 0);
                              })
                              : tOrders;
                            return (
                              <div key={tNum} className="trip-block">
                                <div className="trip-header" style={{ background: `${focusedRider.color}12`, borderColor: `${focusedRider.color}30` }}>
                                  <span className="th-badge" style={{ background: focusedRider.color }}>Trip {tNum}</span>
                                  <span className="trip-stats">
                                    <span><Ico><MdLocationOn /></Ico>{tOrders.length} stops</span>
                                    <span><Ico><MdStraighten /></Ico>{tOrders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span>
                                  </span>
                                  {/* iOS-style segmented control. Track sits in a soft
                                    inset bg; the active item is a clean white "thumb"
                                    with a subtle shadow. We deliberately don't fill
                                    the active pill with the rider's color — the
                                    `Trip N` badge to the left already carries that
                                    identity, and doubling the accent makes the row
                                    read as competing pills instead of a controlled
                                    hierarchy. The CSS owns all interactive states. */}
                                  <div
                                    className="trip-sort-toggle"
                                    role="group"
                                    aria-label="Sort stops by"
                                    data-mode={isTimeMode ? 'time' : 'planned'}
                                  >
                                    <button
                                      type="button"
                                      className={`trip-sort-pill ${!isTimeMode ? 'is-active' : ''}`}
                                      aria-pressed={!isTimeMode}
                                      onClick={() => setTripSortMode('planned')}
                                      title="Sort stops by planned step (dispatched order)"
                                    >
                                      <MdFormatListBulleted aria-hidden="true" />
                                      <span>Planned</span>
                                    </button>
                                    <button
                                      type="button"
                                      className={`trip-sort-pill ${isTimeMode ? 'is-active' : ''}`}
                                      aria-pressed={isTimeMode}
                                      onClick={() => setTripSortMode('time')}
                                      title="Sort stops by completion time (which delivery was done first)"
                                    >
                                      <MdAccessTime aria-hidden="true" />
                                      <span>By time</span>
                                    </button>
                                  </div>
                                </div>
                                <div className="zone-order-grid">
                                  {displayOrders.map((o, idx) => {
                                    const kitchenKey = (o.kitchen_key || o.pickupcustomer || 'Unknown').toLowerCase().trim();
                                    const showTransition = prevKitchenKey !== null && kitchenKey !== prevKitchenKey;
                                    prevKitchenKey = kitchenKey;
                                    const isStopActive = focusedStop && focusedStop.orderid === o.orderid;
                                    const isGoingOn = activeOrderId && o.orderid === activeOrderId;
                                    const lat = parseFloat(o.droplat || o.deliverylat);
                                    const lon = parseFloat(o.droplon || o.deliverylong);
                                    const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
                                    const statusStyle = getStatusStyle(o.orderstatus);
                                    const profit = parseFloat(o.profit || 0);
                                    const isLoss = profit < 0;
                                    const estMeters = calculateEstMeters(focusedRider.id, o);
                                    // Badge number is ALWAYS the planned step (o.step)
                                    // — the same number regardless of sort mode. Time
                                    // mode only reorders the cards; the badge keeps
                                    // its dispatched identity so operators can still
                                    // map a card on screen back to "step 3 of the
                                    // planned route" without mental translation.
                                    const displayNum = o.step || idx + 1;
                                    // "Not yet delivered" indicator for time mode — we
                                    // pushed these rows to the end via MAX_SAFE_INTEGER
                                    // sort key, but a visual cue makes that obvious
                                    // without forcing the operator to read the status pill.
                                    const isUndeliveredInTimeMode =
                                      isTimeMode && !o.deliverytime;

                                    return (
                                      <React.Fragment key={o.orderid}>
                                        {showTransition && (
                                          <div className="kitchen-transition"><span className="kt-ico"><MdSwapHoriz /></span> Switch to <strong>{o.pickupcustomer}</strong></div>
                                        )}
                                        <div
                                          className={`zone-order-card ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''} ${isGoingOn ? 'going-on' : ''} ${isUndeliveredInTimeMode ? 'is-pending-time' : ''}`}
                                          role={canFocus ? 'button' : undefined}
                                          tabIndex={canFocus ? 0 : undefined}
                                          onClick={canFocus ? () => setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }) : undefined}
                                          onKeyDown={canFocus ? (e) => {
                                            if (e.key === 'Enter' || e.key === ' ') {
                                              e.preventDefault();
                                              setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon });
                                            }
                                          } : undefined}
                                          title={canFocus ? (isStopActive ? 'Click to show full trip' : `Show ${o.deliverycustomer || `order #${o.orderid}`} on map`) : undefined}
                                        >
                                          <div className="zone-order-card-head">
                                            <div
                                              className="zone-order-num"
                                              title={`Planned step ${o.step || idx + 1}`}
                                            >
                                              {displayNum}
                                            </div>
                                            <div className="zone-order-id-block">
                                              <div className="zone-order-id">Order #{o.orderid}</div>
                                            </div>
                                            {(() => {
                                              // Stack the status pill and delivery time vertically on
                                              // the right of the header so the operator sees the order
                                              // outcome and the wall-clock time it landed at a glance.
                                              const actual = formatTimeOnly(o.deliverytime);
                                              const expected = formatTimeOnly(o.expecteddeliverytime);
                                              const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
                                              const showEstDrop = !isDelivered && estMeters !== null;
                                              if (!o.orderstatus && !actual && !expected && !showEstDrop) return null;
                                              return (
                                                <div className="zone-order-status-stack">
                                                  {o.orderstatus && (
                                                    <span
                                                      className="zone-order-status"
                                                      style={{ background: statusStyle.bg, color: statusStyle.fg }}
                                                    >
                                                      {statusStyle.label}
                                                    </span>
                                                  )}
                                                  {(actual || expected) && (
                                                    <span
                                                      className={`zone-order-time ${actual ? '' : 'is-expected'}`}
                                                      title={actual ? `Delivered at ${actual}` : `Expected at ${expected}`}
                                                    >
                                                      <MdAccessTime />{actual || expected}
                                                    </span>
                                                  )}
                                                  {showEstDrop && (
                                                    <span
                                                      className="zone-order-est-drop"
                                                      title="Estimated distance to drop location"
                                                    >
                                                      <MdMyLocation />{formatMeters(estMeters)}
                                                    </span>
                                                  )}
                                                </div>
                                              );
                                            })()}
                                            {onChangeRider && (
                                              <button
                                                type="button"
                                                className="zone-order-change-rider"
                                                title="Change rider"
                                                onClick={(e) => {
                                                  e.stopPropagation();
                                                  onChangeRider(o, focusedRider);
                                                }}
                                              >
                                                <MdSwapHoriz />
                                              </button>
                                            )}
                                          </div>

                                          <div className="zone-order-customer">
                                            <Ico><MdMarkunreadMailbox /></Ico>{o.deliverycustomer || '—'}
                                          </div>

                                          {o.pickupcustomer && (
                                            <div className="zone-order-line" title={`Kitchen: ${o.pickupcustomer}`}>
                                              <Ico><MdRestaurant /></Ico>{o.pickupcustomer}
                                            </div>
                                          )}
                                          {(o.deliverysuburb || o.deliveryaddress) && (
                                            <div className="zone-order-line" title={o.deliveryaddress || o.deliverysuburb}>
                                              <Ico><MdLocationOn /></Ico>{o.deliverysuburb || extractArea(o.deliveryaddress)}
                                            </div>
                                          )}
                                          {o.ordernotes && (
                                            <div className="zone-order-line zone-order-notes" title={o.ordernotes}>
                                              <Ico><MdNotes /></Ico>{o.ordernotes}
                                            </div>
                                          )}

                                          <div className="zone-order-stats">
                                            <span className="zone-order-chip" title="Distance">
                                              <Ico><MdStraighten /></Ico>{o.actualkms || o.kms || 0} km
                                            </span>
                                            <span className={`zone-order-chip ${isLoss ? 'is-loss' : 'is-profit'}`} title="Profit">
                                              <Ico><MdAccountBalanceWallet /></Ico>{isLoss ? '-' : ''}₹{Math.abs(profit).toFixed(0)}
                                            </span>
                                            {o.deliverycharge != null && (
                                              <span className="zone-order-chip" title="Delivery charge">
                                                ₹{parseFloat(o.deliverycharge).toFixed(0)} chg
                                              </span>
                                            )}
                                            {o.ordertype && (
                                              <span className={`zone-order-chip zone-order-type type-${String(o.ordertype).toLowerCase()}`}>
                                                {o.ordertype}
                                              </span>
                                            )}
                                            <span className="zone-order-chip zone-order-trip">
                                              T{o.trip_number || tNum} · S{o.step || idx + 1}
                                            </span>
                                          </div>
                                        </div>
                                      </React.Fragment>
                                    );
                                  })}
                                </div>
                              </div>
                            );
                          });
                      })()}
                    </>
                  ) : (
                    <>
                      <div className="rd-rider-name" style={{ color: '#f59e0b' }}>{focusedKitchen.kitchenName}</div>
                      <div className="rd-rider-sub">
                        <span><Ico><MdInventory2 /></Ico>{focusedKitchen.orders.length} orders</span>
                        <span><Ico><MdTwoWheeler /></Ico>{focusedKitchen.riders.size} riders</span>
                      </div>
                      {/* Render the kitchen's orders with the same zone-order-card layout
                      used by the focused-zone view, so By Location, By Zone, and By
                      Rider all look consistent. Kitchen name is omitted from each
                      card because the focused kitchen header already provides it. */}
                      <div className="zone-detail-section">
                        <div className="zone-section-label">Orders <span className="section-count">({focusedKitchen.orders.length})</span></div>
                        {focusedKitchen.orders.length === 0 ? (
                          <div className="zone-suburb-panel-empty">No orders for this kitchen.</div>
                        ) : (
                          <div className="zone-order-grid">
                            {focusedKitchen.orders.map((o, idx) => {
                              const lat = parseFloat(o.droplat || o.deliverylat);
                              const lon = parseFloat(o.droplon || o.deliverylong);
                              const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
                              const isStopActive = focusedStop && focusedStop.orderid === o.orderid;
                              const statusStyle = getStatusStyle(o.orderstatus);
                              const profit = parseFloat(o.profit || 0);
                              const isLoss = profit < 0;

                              const orderRiderId = o.rider_id || o.userid;
                              const riderForOrder = orderRiderId ? riders.find(r => String(r.id) === String(orderRiderId)) : null;
                              const activeOrderId = (() => {
                                if (!riderForOrder) return null;
                                const sortedAll = [...riderForOrder.orders].sort((a, b) => {
                                  const tA = a.trip_number || 1;
                                  const tB = b.trip_number || 1;
                                  if (tA !== tB) return tA - tB;
                                  return (a.step || 0) - (b.step || 0);
                                });
                                const active = sortedAll.find((x) => {
                                  const s = String(x.orderstatus || '').toLowerCase();
                                  return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
                                });
                                return active ? active.orderid : null;
                              })();
                              const isGoingOn = activeOrderId && o.orderid === activeOrderId;
                              const estMeters = orderRiderId ? calculateEstMeters(orderRiderId, o) : null;

                              return (
                                <div
                                  key={o.orderid}
                                  className={`zone-order-card ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''} ${isGoingOn ? 'going-on' : ''}`}
                                  role={canFocus ? 'button' : undefined}
                                  tabIndex={canFocus ? 0 : undefined}
                                  onClick={canFocus ? () => setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }) : undefined}
                                >
                                  <div className="zone-order-card-head">
                                    <div className="zone-order-num">{o.step || idx + 1}</div>
                                    <div className="zone-order-id-block">
                                      <div className="zone-order-id">Order #{o.orderid}</div>
                                      <div className="zone-order-rider">
                                        <Ico><MdTwoWheeler /></Ico>{o.rider_name || o.ridername || 'Unassigned'}
                                      </div>
                                    </div>
                                    {(() => {
                                      const actual = formatTimeOnly(o.deliverytime);
                                      const expected = formatTimeOnly(o.expecteddeliverytime);
                                      const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
                                      const showEstDrop = !isDelivered && estMeters !== null;
                                      if (!o.orderstatus && !actual && !expected && !showEstDrop) return null;
                                      return (
                                        <div className="zone-order-status-stack">
                                          {o.orderstatus && (
                                            <span
                                              className="zone-order-status"
                                              style={{ background: statusStyle.bg, color: statusStyle.fg }}
                                            >
                                              {statusStyle.label}
                                            </span>
                                          )}
                                          {(actual || expected) && (
                                            <span
                                              className={`zone-order-time ${actual ? '' : 'is-expected'}`}
                                              title={actual ? `Delivered at ${actual}` : `Expected at ${expected}`}
                                            >
                                              <MdAccessTime />{actual || expected}
                                            </span>
                                          )}
                                          {showEstDrop && (
                                            <span
                                              className="zone-order-est-drop"
                                              title="Estimated distance to drop location"
                                            >
                                              <MdMyLocation />{formatMeters(estMeters)}
                                            </span>
                                          )}
                                        </div>
                                      );
                                    })()}
                                  </div>

                                  <div className="zone-order-customer">
                                    <Ico><MdMarkunreadMailbox /></Ico>{o.deliverycustomer || '—'}
                                  </div>

                                  {o.pickupcustomer && (
                                    <div className="zone-order-line" title={`Kitchen: ${o.pickupcustomer}`}>
                                      <Ico><MdRestaurant /></Ico>{o.pickupcustomer}
                                    </div>
                                  )}
                                  {(o.deliverysuburb || o.deliveryaddress) && (
                                    <div className="zone-order-line" title={o.deliveryaddress || o.deliverysuburb}>
                                      <Ico><MdLocationOn /></Ico>{o.deliverysuburb || extractArea(o.deliveryaddress)}
                                    </div>
                                  )}
                                  {o.ordernotes && (
                                    <div className="zone-order-line zone-order-notes" title={o.ordernotes}>
                                      <Ico><MdNotes /></Ico>{o.ordernotes}
                                    </div>
                                  )}

                                  <div className="zone-order-stats">
                                    <span className="zone-order-chip" title="Distance">
                                      <Ico><MdStraighten /></Ico>{o.actualkms || o.kms || 0} km
                                    </span>
                                    <span className={`zone-order-chip ${isLoss ? 'is-loss' : 'is-profit'}`} title="Profit">
                                      <Ico><MdAccountBalanceWallet /></Ico>{isLoss ? '-' : ''}₹{Math.abs(profit).toFixed(0)}
                                    </span>
                                    {o.deliverycharge != null && (
                                      <span className="zone-order-chip" title="Delivery charge">
                                        ₹{parseFloat(o.deliverycharge).toFixed(0)} chg
                                      </span>
                                    )}
                                    {o.ordertype && (
                                      <span className={`zone-order-chip zone-order-type type-${String(o.ordertype).toLowerCase()}`}>
                                        {o.ordertype}
                                      </span>
                                    )}
                                    <span className="zone-order-chip zone-order-trip">
                                      T{o.trip_number || '-'} · S{o.step || idx + 1}
                                    </span>
                                  </div>
                                </div>
                              );
                            })}
                          </div>
                        )}
                      </div>
                    </>
                  )}
                </div>
              ) : focusedZone ? (
                <div id="route-detail">
                  <button className="rd-back" onClick={() => setFocusedZone(null)}>← Back to zones</button>
                  <div className="rd-rider-name" style={{ color: '#3b82f6' }}>{focusedZone.name}</div>
                  <div className="rd-rider-sub">
                    <span><Ico><MdInventory2 /></Ico>{focusedZone.totalOrders} {focusedZone.totalOrders === 1 ? 'order' : 'orders'}</span>
                  </div>

                  <div className="zone-detail-section">
                    <div className="zone-section-label">Orders <span className="section-count">({focusedZone.orders.length})</span></div>
                    {focusedZone.orders.length === 0 ? (
                      <div className="zone-suburb-panel-empty">No orders in this zone.</div>
                    ) : (
                      <div className="zone-order-grid">
                        {focusedZone.orders.map((o, idx) => {
                          const lat = parseFloat(o.droplat || o.deliverylat);
                          const lon = parseFloat(o.droplon || o.deliverylong);
                          const canFocus = Number.isFinite(lat) && Number.isFinite(lon);
                          const isStopActive = focusedStop && focusedStop.orderid === o.orderid;
                          const statusStyle = getStatusStyle(o.orderstatus);
                          const profit = parseFloat(o.profit || 0);
                          const isLoss = profit < 0;

                          const orderRiderId = o.rider_id || o.userid;
                          const riderForOrder = orderRiderId ? riders.find(r => String(r.id) === String(orderRiderId)) : null;
                          const activeOrderId = (() => {
                            if (!riderForOrder) return null;
                            const sortedAll = [...riderForOrder.orders].sort((a, b) => {
                              const tA = a.trip_number || 1;
                              const tB = b.trip_number || 1;
                              if (tA !== tB) return tA - tB;
                              return (a.step || 0) - (b.step || 0);
                            });
                            const active = sortedAll.find((x) => {
                              const s = String(x.orderstatus || '').toLowerCase();
                              return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
                            });
                            return active ? active.orderid : null;
                          })();
                          const isGoingOn = activeOrderId && o.orderid === activeOrderId;
                          const estMeters = orderRiderId ? calculateEstMeters(orderRiderId, o) : null;

                          return (
                            <div
                              key={o.orderid}
                              className={`zone-order-card ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''} ${isGoingOn ? 'going-on' : ''}`}
                              role={canFocus ? 'button' : undefined}
                              tabIndex={canFocus ? 0 : undefined}
                              onClick={canFocus ? () => setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }) : undefined}
                            >
                              <div className="zone-order-card-head">
                                <div className="zone-order-num">
                                  {o.step || idx + 1}
                                </div>
                                <div className="zone-order-id-block">
                                  <div className="zone-order-id">Order #{o.orderid}</div>
                                  <div className="zone-order-rider">
                                    <Ico><MdTwoWheeler /></Ico>{o.rider_name || o.ridername || 'Unassigned'}
                                  </div>
                                </div>
                                {(() => {
                                  const actual = formatTimeOnly(o.deliverytime);
                                  const expected = formatTimeOnly(o.expecteddeliverytime);
                                  const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
                                  const showEstDrop = !isDelivered && estMeters !== null;
                                  if (!o.orderstatus && !actual && !expected && !showEstDrop) return null;
                                  return (
                                    <div className="zone-order-status-stack">
                                      {o.orderstatus && (
                                        <span
                                          className="zone-order-status"
                                          style={{ background: statusStyle.bg, color: statusStyle.fg }}
                                        >
                                          {statusStyle.label}
                                        </span>
                                      )}
                                      {(actual || expected) && (
                                        <span
                                          className={`zone-order-time ${actual ? '' : 'is-expected'}`}
                                          title={actual ? `Delivered at ${actual}` : `Expected at ${expected}`}
                                        >
                                          <MdAccessTime />{actual || expected}
                                        </span>
                                      )}
                                      {showEstDrop && (
                                        <span
                                          className="zone-order-est-drop"
                                          title="Estimated distance to drop location"
                                        >
                                          <MdMyLocation />{formatMeters(estMeters)}
                                        </span>
                                      )}
                                    </div>
                                  );
                                })()}
                              </div>

                              <div className="zone-order-customer">
                                <Ico><MdMarkunreadMailbox /></Ico>{o.deliverycustomer || '—'}
                              </div>

                              {o.pickupcustomer && (
                                <div className="zone-order-line" title={`Kitchen: ${o.pickupcustomer}`}>
                                  <Ico><MdRestaurant /></Ico>{o.pickupcustomer}
                                </div>
                              )}
                              {(o.deliverysuburb || o.deliveryaddress) && (
                                <div className="zone-order-line" title={o.deliveryaddress || o.deliverysuburb}>
                                  <Ico><MdLocationOn /></Ico>{o.deliverysuburb || extractArea(o.deliveryaddress)}
                                </div>
                              )}
                              {o.ordernotes && (
                                <div className="zone-order-line zone-order-notes" title={o.ordernotes}>
                                  <Ico><MdNotes /></Ico>{o.ordernotes}
                                </div>
                              )}

                              <div className="zone-order-stats">
                                <span className="zone-order-chip" title="Distance">
                                  <Ico><MdStraighten /></Ico>{o.actualkms || o.kms || 0} km
                                </span>
                                <span className={`zone-order-chip ${isLoss ? 'is-loss' : 'is-profit'}`} title="Profit">
                                  <Ico><MdAccountBalanceWallet /></Ico>{isLoss ? '-' : ''}₹{Math.abs(profit).toFixed(0)}
                                </span>
                                {o.deliverycharge != null && (
                                  <span className="zone-order-chip" title="Delivery charge">
                                    ₹{parseFloat(o.deliverycharge).toFixed(0)} chg
                                  </span>
                                )}
                                {o.ordertype && (
                                  <span className={`zone-order-chip zone-order-type type-${String(o.ordertype).toLowerCase()}`}>
                                    {o.ordertype}
                                  </span>
                                )}
                                <span className="zone-order-chip zone-order-trip">
                                  T{o.trip_number || '-'} · S{o.step || idx + 1}
                                </span>
                              </div>
                            </div>
                          );
                        })}
                      </div>
                    )}

                  </div>
                </div>
              ) : (
                <div id="riders-panel">
                  {!(isAllActiveView && activeDeliveryCount === 0) && (
                    <div className="ph">{
                      viewMode === 'zones' ? 'Zone dispatch' :
                        viewMode === 'kitchens' ? 'Kitchen dispatch' :
                          isAllActiveView ? 'Active rider dispatch' :
                            'Rider dispatch'
                    }</div>
                  )}
                  <div id="rider-cards">
                    {allOrders.length === 0 && !liveIsFetching ? (
                      (() => {
                        const slotLabel = BATCHES.find(b => b.id === selectedBatch)?.label;
                        const hasDayData = shouldFetchLive && liveRows.length > 0;
                        return (
                          <div className="empty-slot">
                            <div className="empty-slot-icon">
                              <MdInventory2 />
                            </div>
                            <div className="empty-slot-title">
                              {slotLabel ? `No orders in ${slotLabel}` : 'No orders'}
                            </div>
                            <div className="empty-slot-sub">
                              {hasDayData
                                ? `${liveRows.length} order${liveRows.length === 1 ? '' : 's'} exist in other slots today`
                                : 'No deliveries found for this date'}
                            </div>
                          </div>
                        );
                      })()
                    ) : viewMode === 'zones' ? (
                      zoneCards.map((z, i) => {
                        const delivered = z.statusCounts.delivered || 0;
                        const profitNeg = z.totalProfit < 0;
                        return (
                          <div key={z.id} className="rcard zone-card" onClick={() => setFocusedZone(z)} style={{ animationDelay: `${i * 0.05}s` }}>
                            <div className="zone-card-header">
                              <div className="zone-card-emoji"><MdMap /></div>
                              <div className="zone-card-titles">
                                <div className="zone-card-name">{z.name}</div>
                                <div className="zone-card-sub">
                                  {z.activeRidersCount} {z.activeRidersCount === 1 ? 'rider' : 'riders'} · {z.totalOrders} {z.totalOrders === 1 ? 'order' : 'orders'}
                                </div>
                              </div>
                              <span className="zone-card-arrow" aria-hidden="true">→</span>
                            </div>

                            {/* Status segments + delivered counter */}
                            {z.totalOrders > 0 && (
                              <div className="zone-progress-row">
                                <div
                                  className="zone-status-bar"
                                  title={Object.entries(z.statusCounts)
                                    .map(([k, v]) => `${getStatusStyle(k).label}: ${v}`)
                                    .join(' · ')}
                                >
                                  {Object.entries(z.statusCounts).map(([status, count]) => {
                                    const style = getStatusStyle(status);
                                    const pct = (count / z.totalOrders) * 100;
                                    return (
                                      <div
                                        key={status}
                                        className="zone-status-seg"
                                        style={{ width: `${pct}%`, background: style.bg }}
                                      />
                                    );
                                  })}
                                </div>
                                <div className="zone-progress-label">
                                  {delivered}/{z.totalOrders}
                                </div>
                              </div>
                            )}

                            {/* Stat pills */}
                            <div className="zone-stat-pills">
                              <span className="zone-stat-pill" title="Areas covered">
                                <span className="zone-stat-icon"><MdLocationOn /></span>
                                <span className="zone-stat-value">{z.suburbs.length}</span>
                                <span className="zone-stat-label">{z.suburbs.length === 1 ? 'area' : 'areas'}</span>
                              </span>
                              <span className="zone-stat-pill" title="Total distance">
                                <span className="zone-stat-icon"><MdStraighten /></span>
                                <span className="zone-stat-value">{z.totalKms.toFixed(1)}</span>
                                <span className="zone-stat-label">km</span>
                              </span>
                              <span className="zone-stat-pill" title="Kitchens">
                                <span className="zone-stat-icon"><MdRestaurant /></span>
                                <span className="zone-stat-value">{z.kitchens.length}</span>
                                <span className="zone-stat-label">{z.kitchens.length === 1 ? 'kitchen' : 'kitchens'}</span>
                              </span>
                              <span className={`zone-stat-pill ${profitNeg ? 'profit-negative' : 'profit-positive'}`} title="Total profit">
                                <span className="zone-stat-icon"><MdAccountBalanceWallet /></span>
                                <span className="zone-stat-value">
                                  {profitNeg ? `-₹${Math.abs(z.totalProfit).toFixed(0)}` : `₹${z.totalProfit.toFixed(0)}`}
                                </span>
                              </span>
                            </div>

                            {z.suburbs.length > 0 && (
                              <div className="zone-card-suburbs">
                                <span className="zone-card-suburbs-text">
                                  {z.suburbs.slice(0, 3).map((s) => s.name).join(' · ')}
                                </span>
                                {z.suburbs.length > 3 && (
                                  <span className="zone-card-suburbs-more">+{z.suburbs.length - 3}</span>
                                )}
                              </div>
                            )}
                          </div>
                        );
                      })
                    ) : viewMode === 'kitchens' ? (
                      kitchens.map((k, i) => (
                        <div key={k.id} className="rcard" onClick={() => setFocusedKitchen(k)} style={{ animationDelay: `${i * 0.05}s` }}>
                          <div className="rcard-top">
                            <div className="rcard-emo" style={{ background: '#f59e0b18', borderColor: '#f59e0b50', color: '#f59e0b' }}><MdRestaurant /></div>
                            <div className="rcard-info">
                              <div className="rcard-name">{k.kitchenName}</div>
                              <div className="rcard-zone">{k.riders.size} riders · <Ico><MdAccountBalanceWallet /></Ico>₹{k.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}</div>
                            </div>
                            <div className="rcard-badge" style={{ background: '#f59e0b18', color: '#f59e0b' }}>{k.orders.length}</div>
                          </div>
                          <div className="bar-bg"><div className="bar-fg" style={{ width: `${Math.min(100, (k.orders.length / 20) * 100)}%`, background: '#f59e0b' }}></div></div>
                          <div className="rcard-meta"><span><Ico><MdStraighten /></Ico>{k.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span><span>{k.riders.size} riders</span></div>
                          <div className="step-ids">
                            {Array.from(k.riders).slice(0, 10).map(rid => (
                              <span key={rid} className="step-id" style={{ color: getRiderColor(rid) }}>{riders.find(r => r.id === rid)?.riderName.split(' ')[0]}</span>
                            ))}
                          </div>
                        </div>
                      ))
                    ) : isAllActiveView ? (
                      <ActiveSection
                        visibleRiders={activeViewRiders}
                        riders={riders}
                        focusedStop={focusedStop}
                        handleRiderFocus={handleRiderFocus}
                        setFocusedStop={setFocusedStop}
                        calculateEstMeters={calculateEstMeters}
                        getRiderColor={getRiderColor}
                        formatMeters={formatMeters}
                        isLoading={shouldFetchLive && liveIsFetching && activeViewRiders.length === 0}
                      />
                    ) : (
                      visibleRiders.map(renderRiderCard)
                    )}
                  </div>
                </div>
              )}
            </div>

            <div id="map-wrap" className={`${viewMode === 'kitchens' ? 'view-mode-kitchens' : ''} ${compareOpen ? 'compare-split' : ''}`.trim()}>
              <MapContainer
                center={[11.022, 76.982]}
                zoom={12}
                scrollWheelZoom
                style={{ height: '100%', width: '100%' }}
                zoomControl={false}
                renderer={plannedMapRendererRef.current}
                inertia
                inertiaDeceleration={2400}
                inertiaMaxSpeed={2000}
                wheelDebounceTime={20}
                wheelPxPerZoomLevel={80}
                zoomSnap={0.25}
              >
                <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution='&copy; OpenStreetMap contributors' />
                <ZoomControl position="bottomright" />
                {compareOpen && <CaptureMap targetRef={leftMapRef} />}
                <MapAutoResize trigger={`${sidebarCollapsed}|${compareOpen}|${compareDataCollapsed}`} />
                <MapController focusedItem={compareFocusItem || ((focusedRider || focusedKitchen) && focusedStop) || focusedRider || focusedKitchen || focusedZone} viewMode={viewMode} orders={allViewOrders} kitchens={kitchens} locationKey={selectedAppLocationId} extraPoints={allViewLivePoints} />
                {kitchens
                  .filter(k => Number.isFinite(k.lat) && Number.isFinite(k.lon))
                  .filter(k => !focusedRider || k.riders.has(focusedRider.id))
                  .map((k, i) => (
                    <Marker
                      key={`k-${i}`}
                      position={[k.lat, k.lon]}
                      icon={createKitchenIcon(k.kitchenName, focusedKitchen?.id === k.id)}
                      zIndexOffset={focusedKitchen?.id === k.id ? 4000 : 2000}
                      eventHandlers={{
                        click: () => setFocusedKitchen(k),
                        mouseover: (e) => e.target.openPopup(),
                        mouseout: (e) => e.target.closePopup()
                      }}
                    >
                      <Popup className="kitchen-popup" maxWidth={220} minWidth={200} autoPan={true} autoPanPadding={[20, 20]}>
                        <div className="kp-header">KITCHEN</div>
                        <div className="kp-name">{k.kitchenName}</div>
                        <div className="kp-stat">
                          <span className="kp-stat-lbl">Orders</span>
                          <span className="kp-stat-val">{k.orders.length}</span>
                        </div>
                      </Popup>
                    </Marker>
                  ))
                }
                {renderMarkers()}
                {renderRoutes()}

                {/* Live rider GPS markers from /partners/getriderlogs/. Mirrors the
                Reports → Riders Logs map: green pin when the rider's last log
                row is `active`, red otherwise, with the rider's username as a
                label. Scoped to riders who actually have orders in the
                currently selected slot — `riders` is derived from
                filteredLiveRows so it already reflects the slot filter. A
                rider with zero orders in the current slot is hidden, even if
                getriderlogs still returns their GPS row. When a specific
                rider is focused, only that one is shown. */}
                {liveRiderLocations
                  .filter((r) =>
                    isAllActiveView
                      ? ((r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id)))
                      : riders.some((rd) => String(rd.id) === String(r.id))
                  )
                  .filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))
                  .map((r) => {
                    const isActive = r.status === 'active' || r.status === 'pending';
                    const pinColor = isActive ? '#16a34a' : '#dc2626';
                    // Look up the rider's in-progress order so the popup can show
                    // where they're heading next (drop customer/area + originating
                    // kitchen). Falls back to nothing when every order is final.
                    const matchingRider = riders.find((rd) => String(rd.id) === String(r.id));
                    const nextOrder = matchingRider?.orders
                      ?.slice()
                      .sort((a, b) => {
                        const tA = a.trip_number || 1;
                        const tB = b.trip_number || 1;
                        if (tA !== tB) return tA - tB;
                        return (a.step || 0) - (b.step || 0);
                      })
                      .find((o) => {
                        const s = String(o.orderstatus || '').toLowerCase();
                        return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
                      });
                    const nextDropArea = nextOrder
                      ? (nextOrder.deliverysuburb || extractArea(nextOrder.deliveryaddress))
                      : null;
                    // Marker icon. ONLY the "All Active Routes" view gets the
                    // Swiggy/Zomato/Rapido-style live bike badge (rounded glyph
                    // + pulsing ring) that smoothly glides between GPS fixes.
                    // Every other view keeps the original teardrop pin exactly
                    // as before. Icons are cached per rider (liveIconCacheRef);
                    // the sig includes the view so switching modes rebuilds it.
                    const safeName = (r.username || '').replace(/[<>&"']/g, '');
                    const safeOrder = r.orderid ? String(r.orderid).replace(/[<>&"']/g, '') : '';
                    const iconSig = `${isAllActiveView ? 'bike' : 'pin'}|${pinColor}|${safeName}|${safeOrder}|${isActive ? 1 : 0}`;
                    let iconEntry = liveIconCacheRef.current.get(r.id);
                    if (!iconEntry || iconEntry.sig !== iconSig) {
                      const icon = isAllActiveView
                        ? L.divIcon({
                          className: '',
                          iconSize: [160, 44],
                          iconAnchor: [22, 22],
                          popupAnchor: [0, -22],
                          html: `<div class="live-rider-bike ${isActive ? 'is-active' : 'is-idle'}" style="--pin-color:${pinColor}">
                           <span class="live-rider-bike-pulse"></span>
                           <span class="live-rider-bike-badge">
                             <svg viewBox="0 0 24 24" width="20" height="20" fill="#fff" aria-hidden="true"><path d="M19.44 9.03 15.41 5H11v2h3.59l2 2H5c-2.8 0-5 2.2-5 5s2.2 5 5 5c2.46 0 4.45-1.69 4.9-4h1.65l2.77-2.77c-.21.54-.32 1.14-.32 1.77 0 2.76 2.24 5 5 5s5-2.24 5-5c0-2.65-2.06-4.77-4.66-4.97ZM7.82 15C7.4 16.15 6.28 17 5 17c-1.63 0-3-1.37-3-3s1.37-3 3-3c1.28 0 2.4.85 2.82 2H5v2h2.82ZM19 17c-1.63 0-3-1.37-3-3s1.37-3 3-3 3 1.37 3 3-1.37 3-3 3Z"/></svg>
                           </span>
                           <span class="live-rider-bike-label">${safeName}${safeOrder ? ` <span>#${safeOrder}</span>` : ''}</span>
                         </div>`
                        })
                        : L.divIcon({
                          className: '',
                          iconSize: [140, 56],
                          iconAnchor: [12, 41],
                          popupAnchor: [58, -40],
                          html: `<div class="live-rider-pin" style="--pin-color:${pinColor}">
                           <div class="live-rider-pin-marker"></div>
                           <div class="live-rider-pin-label">${safeName}${safeOrder ? ` <span>#${safeOrder}</span>` : ''}</div>
                         </div>`
                        });
                      iconEntry = { sig: iconSig, icon };
                      liveIconCacheRef.current.set(r.id, iconEntry);
                    }
                    const liveIcon = iconEntry.icon;
                    // Shared interaction handlers — identical for both marker types.
                    const liveEventHandlers = {
                      click: (e) => {
                        const idStr = String(r.id);
                        if (pinnedLivePopupsRef.current.has(idStr)) {
                          pinnedLivePopupsRef.current.delete(idStr);
                          e.target.closePopup();
                        } else {
                          pinnedLivePopupsRef.current.add(idStr);
                          e.target.openPopup();
                        }
                        const match = riders.find((rd) => String(rd.id) === idStr);
                        if (match) handleRiderFocus(match);
                      },
                      popupclose: () => {
                        pinnedLivePopupsRef.current.delete(String(r.id));
                      }
                    };
                    // Animated bike (with imperative gliding) only in the active
                    // view; the plain react-leaflet Marker everywhere else so
                    // existing views behave exactly as they did before.
                    const LiveMarker = isAllActiveView ? AnimatedRiderMarker : Marker;
                    const positionProps = isAllActiveView
                      ? { target: [r.lat, r.lon], duration: 1000 }
                      : { position: [r.lat, r.lon] };
                    return (
                      <LiveMarker
                        key={`live-${r.id}`}
                        {...positionProps}
                        icon={liveIcon}
                        zIndexOffset={2500}
                        eventHandlers={liveEventHandlers}
                      >
                        <Popup maxWidth={260} autoPan={true} autoPanPadding={[20, 20]} className="dispatch-popup live-rider-popup">
                          <div className="pu-hdr-live">
                            <div className="pu-hdr-left">
                              <span className="pu-live-indicator" style={{ '--pulse-color': pinColor }}>
                                <span className="pu-live-dot"></span>
                              </span>
                              <span className="pu-hdr-title">LIVE GPS</span>
                            </div>
                          </div>
                          <div className="pu-rider-profile">
                            <div className="pu-avatar" style={{ backgroundColor: `${pinColor}12`, color: pinColor }}>
                              <MdDirectionsBike />
                            </div>
                            <div className="pu-rider-info-text">
                              <div className="pu-rider-name-row">
                                <span className="pu-rider-name">{r.username || `Rider #${r.id}`}</span>
                                {r.status && (
                                  <span className={`pu-status-badge ${r.status.toLowerCase() === 'active' ? 'active' : 'idle'}`}>
                                    {r.status}
                                  </span>
                                )}
                              </div>
                              <div className="pu-rider-meta">Rider ID: #{r.id}</div>
                            </div>
                          </div>
                          <div className="pu-body-content">
                            {r.orderid && (
                              <div className="pu-info-row">
                                <span className="pu-info-label">Active Order</span>
                                <span className="pu-info-value pu-order-badge">#{r.orderid}</span>
                              </div>
                            )}
                            {nextOrder && (
                              <div className="pu-info-row">
                                <span className="pu-info-label">Next Stop</span>
                                <span className="pu-info-value" style={{ color: '#4f46e5' }}>
                                  #{nextOrder.step || '?'} · {nextOrder.deliverycustomer || '—'}
                                </span>
                              </div>
                            )}
                            {nextDropArea && (
                              <div className="pu-info-row">
                                <span className="pu-info-label">Next Location</span>
                                <span
                                  className="pu-info-value"
                                  title={nextOrder.deliveryaddress || nextDropArea}
                                >
                                  {nextDropArea}
                                </span>
                              </div>
                            )}
                            {nextOrder?.pickupcustomer && (
                              <div className="pu-info-row">
                                <span className="pu-info-label">Pickup</span>
                                <span className="pu-info-value" title={nextOrder.pickupcustomer}>
                                  {nextOrder.pickupcustomer}
                                </span>
                              </div>
                            )}
                            {r.contactno && (
                              <div className="pu-info-row">
                                <span className="pu-info-label">Phone</span>
                                <a href={`tel:${r.contactno}`} className="pu-info-value pu-phone-link">
                                  {r.contactno}
                                </a>
                              </div>
                            )}
                            {r.logdate && (
                              <div className="pu-info-row">
                                <span className="pu-info-label">Last Seen</span>
                                <span className="pu-info-value pu-time-stamp">
                                  <MdAccessTime className="inline-icon" />{' '}
                                  {dayjs(r.logdate).isValid() ? dayjs(r.logdate).format('hh:mm:ss A') : r.logdate}
                                </span>
                              </div>
                            )}
                            <div className="pu-info-row">
                              <span className="pu-info-label">Position</span>
                              <span className="pu-info-value pu-coordinates">
                                {r.lat.toFixed(5)}, {r.lon.toFixed(5)}
                              </span>
                            </div>
                          </div>
                        </Popup>
                      </LiveMarker>
                    );
                  })}

                {/* Compare mode — actual GPS tracks for the focused rider, drawn
                on the same MapContainer as the planned route. Gated by
                compareViewMode so the segmented control on the map's top-left
                can hide them ("Planned only") or hide the planned polylines
                instead ("Actual only"). In Combined mode both render and the
                planned polylines switch to a dashed stroke (see renderRoutes)
                so the operator can read overlap at a glance. */}
                {compareOpen && focusedRider && compareViewMode !== 'planned' && (riderActualTracks.map((t, i) => {
                  if (t.coords.length === 0) return null;
                  // `color` drives the drop pin, start pin, and tooltip header so
                  // those keep their per-step palette identity (the same colors
                  // the timeline uses). `polylineColor` is what the GPS line
                  // itself draws with — collapsed to a single emerald in Combined
                  // view so the actual layer reads as one cohesive trail next to
                  // the indigo planned rail; Actual-only mode keeps step palette
                  // on the polyline since there's no second layer to confuse with.
                  const color = stepColor(i);
                  const polylineColor = compareViewMode === 'combined'
                    ? COMBINED_ACTUAL_COLOR
                    : color;
                  const startPos = [t.coords[0].lat, t.coords[0].lng];
                  const endPos = [t.coords[t.coords.length - 1].lat, t.coords[t.coords.length - 1].lng];
                  const snapped = osrmTrackRoutes[t.deliveryid];
                  const hasRoad = Array.isArray(snapped) && snapped.length >= 2;
                  const fullPositions = hasRoad
                    ? snapped
                    : t.coords.map((p) => [p.lat, p.lng]);

                  let positions = fullPositions;
                  let drawPolyline = true;
                  if (isAnimating) {
                    const progress = animatedActualProgress[t.sequenceStep] || 0;
                    if (progress < 2) {
                      drawPolyline = false;
                    } else {
                      positions = fullPositions.slice(0, Math.min(progress, fullPositions.length));
                    }
                  }

                  const isFocusedStep = focusedCompareStep === t.sequenceStep;
                  const statusLow = String(t.orderstatus || '').toLowerCase();
                  const isDelivered = FINAL_STATUSES.has(statusLow);
                  const isSkipped = SKIPPED_STATUSES.has(statusLow);
                  const delta = compareDeltas.find((d) => d.sequenceStep === t.sequenceStep);
                  const isAnomaly = !!delta?.anomaly;
                  // Look up the corresponding order in the focused rider's set so
                  // the actual-track drop pin can render the same rich popup the
                  // planned-route number marker uses (Timeline, Details, KM chips).
                  // Matched by deliveryid since order.orderid is not in the track
                  // payload but deliveryid is the join key for /getdeliverylogs.
                  const orderForTrack = focusedRider?.orders?.find(
                    (o) => o.deliveryid != null && String(o.deliveryid) === String(t.deliveryid)
                  );

                  const statusStyle = getStatusStyle(t.orderstatus);
                  const flagSvg = t.orderstatus
                    ? `<svg class="cmark-flag" viewBox="0 0 18 22" xmlns="http://www.w3.org/2000/svg">
                       <line x1="1.5" y1="0" x2="1.5" y2="22" stroke="#0f172a" stroke-width="1.6" stroke-linecap="round"/>
                       <polygon points="2,1 17,1 13.5,5.5 17,10 2,10" fill="${statusStyle.bg}" stroke="#0f172a" stroke-width="0.6" stroke-linejoin="round"/>
                       ${isDelivered ? '<polyline points="5,5.5 7,7.5 11,3.5" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>' : ''}
                     </svg>`
                    : '';

                  const dropClasses = ['compare-step-pin'];
                  if (isFocusedStep) dropClasses.push('is-focused');
                  if (isDelivered) dropClasses.push('is-delivered');
                  if (isSkipped) dropClasses.push('is-skipped');
                  if (isAnomaly) dropClasses.push('is-anomaly');

                  const dropPinHtml =
                    `<div class="${dropClasses.join(' ')}" style="--pin-color:${color}">` +
                    `<span class="compare-step-pin-num">${t.sequenceStep}</span>` +
                    (isDelivered
                      ? '<svg class="compare-step-pin-check" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
                      '<path d="M2.5 6.5 L5 9 L9.5 3.5" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>' +
                      '</svg>'
                      : '') +
                    '</div>';
                  const sequenceIcon = L.divIcon({
                    className: '',
                    iconSize: [36, 36],
                    iconAnchor: [18, 18],
                    // Lift the popup ~22px above the pin so its tip clears the
                    // marker icon. Without this the popup centers on the marker,
                    // its body overlaps the pin, and the cursor moves onto the
                    // popup → fires mouseout on the marker → popup snaps shut.
                    popupAnchor: [0, -22],
                    html: dropPinHtml
                  });

                  const showStartMarker = t.sequenceStep === 1;
                  const startIconEl = showStartMarker
                    ? L.divIcon({
                      className: '',
                      iconSize: [40, 40],
                      iconAnchor: [20, 20],
                      html:
                        `<div class="compare-start-pin" style="--pin-color:${color}">` +
                        '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
                        '<path d="M19 6h-2c0-2.76-2.24-5-5-5S7 3.24 7 6H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-7-3c1.66 0 3 1.34 3 3H9c0-1.66 1.34-3 3-3zm0 10c-2.76 0-5-2.24-5-5h2c0 1.66 1.34 3 3 3s3-1.34 3-3h2c0 2.76-2.24 5-5 5z" fill="currentColor"/>' +
                        '</svg></div>'
                    })
                    : null;

                  const handleStepClick = (e) => {
                    if (e.originalEvent) e.originalEvent.stopPropagation();
                    setFocusedCompareStep((prev) =>
                      prev === t.sequenceStep ? null : t.sequenceStep
                    );
                  };

                  // Drop pin click — toggles step focus AND opens the rich order
                  // popup, so the actual / combined view answers the same "tell me
                  // about this delivery" question the planned-route number marker
                  // answers. The pickup (start) pin keeps the focus-only handler;
                  // there's no per-delivery order modal attached to step 1's
                  // origin since it's the rider's pickup point, not the drop.
                  const handleEndMarkerClick = (e) => {
                    if (e.originalEvent) e.originalEvent.stopPropagation();
                    setFocusedCompareStep((prev) =>
                      prev === t.sequenceStep ? null : t.sequenceStep
                    );
                    if (orderForTrack && e.target && typeof e.target.openPopup === 'function') {
                      e.target.openPopup();
                    }
                  };

                  // Combined view rail-offset (negative side): mirror image of
                  // the +5px shift applied to the planned polyline in
                  // renderRoutes. With planned at +5 and actual at -5, the two
                  // layers sit as parallel rails ~10px apart when they share
                  // a road, so the operator can read both even on tight match.
                  // Actual-only and Planned-only modes leave offset = 0 since
                  // there's only one layer drawing on the map.
                  const actualOffset = compareViewMode === 'combined' ? -5 : 0;

                  return (
                    <React.Fragment key={`actual-${t.deliveryid}`}>
                      {drawPolyline && (
                        <Polyline
                          positions={positions}
                          pathOptions={{
                            color: '#ffffff',
                            weight: isFocusedStep ? 11 : 9,
                            opacity: isFocusedStep ? 0.75 : 0.55,
                            lineJoin: 'round',
                            lineCap: 'round',
                            offset: actualOffset
                          }}
                        />
                      )}
                      {drawPolyline && (
                        <Polyline
                          positions={positions}
                          pathOptions={{
                            color: polylineColor,
                            weight: isFocusedStep ? 6.5 : 5,
                            opacity: isFocusedStep ? 1 : focusedCompareStep ? 0.55 : 0.95,
                            lineJoin: 'round',
                            lineCap: 'round',
                            offset: actualOffset
                          }}
                        />
                      )}

                      {showStartMarker && (
                        <Marker
                          position={startPos}
                          icon={startIconEl}
                          zIndexOffset={isFocusedStep ? 900 : 100}
                          eventHandlers={{ click: handleStepClick }}
                        >
                          <Tooltip
                            direction="top"
                            offset={[0, -12]}
                            opacity={1}
                            className="compare-tooltip"
                          >
                            <div className="cmp-tip">
                              <div className="cmp-tip-header">
                                <span className="cmp-tip-step" style={{ background: color }}>
                                  <MdLocalMall />
                                </span>
                                <div className="cmp-tip-title-stack">
                                  <div className="cmp-tip-title">
                                    {t.pickupcustomer || 'Pickup'}
                                  </div>
                                  <div className="cmp-tip-sub">
                                    {t.coords[0]?.logdate
                                      ? `Picked up · ${dayjs(t.coords[0].logdate).format('hh:mm A')}`
                                      : 'Rider trip origin'}
                                  </div>
                                </div>
                              </div>
                              <div className="cmp-tip-action">Click for step 1 details</div>
                            </div>
                          </Tooltip>
                        </Marker>
                      )}

                      <Marker
                        position={endPos}
                        icon={sequenceIcon}
                        zIndexOffset={isFocusedStep ? 1000 : 200}
                        eventHandlers={
                          orderForTrack
                            ? {
                              // Click-only: hovering the numbered pin does
                              // nothing. Earlier the modal opened on mouseover,
                              // which made the rich order card flash open while
                              // panning the map and blocked the operator from
                              // reading other steps in the trail. Click toggles
                              // the modal — same `pinnedPopupsRef` pattern the
                              // planned-route marker uses, so the modal stays
                              // pinned (doesn't auto-close on mouse leave) until
                              // the user clicks the pin again, the × button, or
                              // the map background.
                              click: (e) => {
                                if (e.originalEvent) e.originalEvent.stopPropagation();
                                setFocusedCompareStep((prev) =>
                                  prev === t.sequenceStep ? null : t.sequenceStep
                                );
                                if (popupHoverTimerRef.current) {
                                  clearTimeout(popupHoverTimerRef.current);
                                  popupHoverTimerRef.current = null;
                                }
                                const id = String(orderForTrack.orderid);
                                if (pinnedPopupsRef.current.has(id)) {
                                  pinnedPopupsRef.current.delete(id);
                                  setCenterPopupOrder(null);
                                } else {
                                  pinnedPopupsRef.current.add(id);
                                  setCenterPopupOrder(orderForTrack);
                                }
                              }
                            }
                            : { click: handleEndMarkerClick }
                        }
                      >
                        {!orderForTrack && (
                          <Tooltip
                            direction="top"
                            offset={[0, -20]}
                            opacity={1}
                            className="compare-tooltip"
                          >
                            {(() => {
                              const ss = getStatusStyle(t.orderstatus);
                              return (
                                <div className="cmp-tip">
                                  <div className="cmp-tip-header">
                                    <span className="cmp-tip-step" style={{ background: color }}>
                                      {t.sequenceStep}
                                    </span>
                                    <div className="cmp-tip-title-stack">
                                      <div className="cmp-tip-title">
                                        {t.deliverycustomer || `Step ${t.sequenceStep}`}
                                      </div>
                                      <div className="cmp-tip-sub">
                                        {t.deliverytime
                                          ? `Delivered ${dayjs(t.deliverytime).format('hh:mm A')}`
                                          : `${t.coords.length} GPS pings`}
                                      </div>
                                    </div>
                                    {t.orderstatus && (
                                      <span
                                        className="cmp-tip-tag"
                                        style={{ background: ss.bg, color: ss.fg }}
                                      >
                                        {ss.label}
                                      </span>
                                    )}
                                  </div>
                                  {isAnomaly && (
                                    <div className="cmp-tip-anomaly">
                                      Deviation flagged — see details below
                                    </div>
                                  )}
                                  <div className="cmp-tip-action">
                                    {isFocusedStep ? 'Click to deselect' : 'Click for details'}
                                  </div>
                                </div>
                              );
                            })()}
                          </Tooltip>
                        )}
                      </Marker>
                    </React.Fragment>
                  );
                }))}
              </MapContainer>

              {compareOpen && focusedRider && (
                <div className="compare-view-switcher" role="group" aria-label="Compare view layer">
                  <button
                    type="button"
                    className={compareViewMode === 'actual' ? 'is-active' : ''}
                    onClick={() => setCompareViewMode('actual')}
                    title="Show only the rider's actual GPS trail"
                  >
                    Actual
                  </button>
                  <button
                    type="button"
                    className={compareViewMode === 'planned' ? 'is-active' : ''}
                    onClick={() => setCompareViewMode('planned')}
                    title="Show only the dispatched planned route"
                  >
                    Planned
                  </button>
                  <button
                    type="button"
                    className={compareViewMode === 'combined' ? 'is-active' : ''}
                    onClick={() => setCompareViewMode('combined')}
                    title="Overlay planned (dashed) and actual (solid) on one map"
                  >
                    Combined
                  </button>
                </div>
              )}

              <div id="ov-tl">
                {/* <div className="ov-card">
              <div className="ov-stats">
                <div><div className="osv g">{activeStats.orders}</div><div className="osl">Orders</div></div>
                <div><div className="osv">{activeStats.riders}</div><div className="osl">Riders</div></div>
                <div><div className="osv g">₹{activeStats.profit.toFixed(0)}</div><div className="osl">Profit</div></div>
              </div>
            </div> */}
              </div>

              {/* Right-corner rider/kitchen legend hidden per spec — the same
                delivered/total count now renders inside the left sidebar's
                rider card badge (see renderRiderCard). Restore this block to
                bring back the floating top-right chip list.
            <div id="ov-tr">
              {viewMode === 'kitchens' ? (
                kitchens.slice(0, 10).map(k => {
                  const total = k.orders.length;
                  const delivered = k.orders.filter((o) =>
                    FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
                  ).length;
                  const isDone = total > 0 && delivered >= total;
                  return (
                    <div key={k.id} className={`rchip ${focusedKitchen?.id === k.id ? 'active' : ''}`} onClick={() => setFocusedKitchen(k)} title={`${delivered} delivered of ${total} total`}>
                      <div className="rchip-dot" style={{ background: '#f59e0b' }}></div>
                      <span style={{ maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{k.kitchenName}</span>
                      <span className={`rchip-n ${isDone ? 'is-done' : ''}`}>{delivered}/{total}</span>
                    </div>
                  );
                })
              ) : (
                riders.slice(0, 10).map(r => {
                  const total = r.orders.length;
                  const delivered = r.orders.filter((o) =>
                    FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
                  ).length;
                  const isDone = total > 0 && delivered >= total;
                  return (
                    <div key={r.id} className={`rchip ${focusedRider?.id === r.id ? 'active' : ''}`} onClick={() => handleRiderFocus(r)} title={`${delivered} delivered of ${total} total`}>
                      <div className="rchip-dot" style={{ background: r.color }}></div>
                      <span style={{ maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.riderName}</span>
                      <span className={`rchip-n ${isDone ? 'is-done' : ''}`}>{delivered}/{total}</span>
                    </div>
                  );
                })
              )}
            </div>
            */}

              <div id="ov-br">
                <button className={`sbt ${isAnimating ? 'active' : ''}`} onClick={startAnimation} style={{ boxShadow: 'var(--shadow-lg)', background: isAnimating ? 'var(--accent)' : '#fff' }}>
                  <span>{isAnimating ? '⏹' : '▶'}</span> {isAnimating ? 'Stop' : 'Animate Routes'}
                </button>
                {/* Compare planned vs. actual: when no rider is focused yet we
                auto-pick the first rider with orders, so the button always
                does something visible. The comparison itself is per-rider. */}
                <button
                  type="button"
                  className={`sbt ${compareOpen ? 'active' : ''}`}
                  onClick={() => {
                    if (compareOpen) { setCompareOpen(false); return; }
                    if (!focusedRider) {
                      // Prefer a rider whose orders have deliveryids — those are
                      // the only ones /getdeliverylogs/ can return tracks for, so
                      // picking such a rider guarantees the comparison map has
                      // something to draw.
                      const withDelivery = riders.find((r) =>
                        (r.orders || []).some((o) => o.deliveryid != null && o.deliveryid !== '' && o.deliveryid !== 0)
                      );
                      const fallback = riders.find((r) => r.orders && r.orders.length > 0);
                      const pick = withDelivery || fallback;
                      if (pick) {
                        handleRiderFocus(pick);
                        // In controlled mode focusedRider won't update until the parent
                        // re-renders with the new selectedRiderId, so defer the open.
                        if (isControlled) {
                          pendingCompareRef.current = true;
                          return;
                        }
                      }
                    }
                    setCompareOpen(true);
                  }}
                  title={focusedRider
                    ? `Compare planned vs. actual route for ${focusedRider.riderName}`
                    : 'Compare planned vs. actual route (will focus the first rider)'}
                  style={{
                    boxShadow: 'var(--shadow-lg)',
                    background: compareOpen
                      ? 'linear-gradient(135deg, #662582, #9255AB)'
                      : '#fff',
                    marginLeft: 8,
                    color: compareOpen ? '#fff' : undefined
                  }}
                >
                  <span className="sbt-icon"><MdSwapHoriz /></span>
                  {compareOpen ? 'Exit Compare' : 'Compare'}
                </button>
              </div>
            </div>

            {/* Compare-mode second pane — actual GPS tracks per delivery for the
            focused rider. Fans out /deliveries/getdeliverylogs/?deliveryid=X
            (one query per order, cached by deliveryid) and overlays each
            track as a colored polyline + start/end markers. Renders alongside
            #map-wrap when compareOpen so the operator can eyeball planned vs.
            actual side by side. */}
            {compareOpen && focusedRider && (
              <div id="compare-map-wrap">
                {(() => {
                  const total = riderActualTracks.length;
                  const loaded = riderActualTracks.filter((t) => t.coords.length > 0).length;
                  const loading = riderActualTracks.filter((t) => t.isLoading).length;
                  const pct = total > 0 ? Math.round((loaded / total) * 100) : 0;
                  const isDone = total > 0 && loaded === total && loading === 0;
                  const focusedDelta =
                    focusedCompareStep != null
                      ? compareDeltas.find((d) => d.sequenceStep === focusedCompareStep)
                      : null;
                  return (
                    <div className="compare-header-v2">
                      <div className="compare-header-row">
                        <div className="compare-title">
                          <span className="compare-title-dot" style={{ background: focusedRider.color }} />
                          <span className="compare-title-name">{focusedRider.riderName}</span>
                          <span className="compare-title-badge">ACTUAL vs PLANNED</span>
                        </div>
                        <div className="compare-header-tools">
                          {focusedCompareStep != null && (
                            <button
                              type="button"
                              className="compare-overall-btn"
                              onClick={() => setFocusedCompareStep(null)}
                              title="Zoom out to the whole day"
                            >
                              <MdPublic /> Overall
                            </button>
                          )}
                          <button
                            type="button"
                            className={`compare-timeline-toggle${compareTimelineOpen ? ' is-open' : ''}`}
                            onClick={() => setCompareTimelineOpen((v) => !v)}
                            title={compareTimelineOpen
                              ? 'Hide planned/actual timeline'
                              : 'Show planned/actual timeline'}
                            aria-expanded={compareTimelineOpen}
                          >
                            <MdExpandMore />
                          </button>
                        </div>
                      </div>

                      {/* Step timeline — every delivery as a tappable dot in chronological
                      order. The operator can drill into any step to scrutinize that
                      single delivery on both maps. Filled = delivered, ring = pending,
                      dim = cancelled/skipped, ring with spinner = GPS still loading.
                      Whole strip (timeline + progress + legend) collapses via the
                      header chevron — open by default. */}
                      {compareTimelineOpen && (
                        <>
                          <div className="compare-timeline-wrap">
                            <div className="compare-timeline-container">
                              <div className="compare-timeline-labels">
                                <div className="compare-timeline-label">Planned</div>
                                <div className="compare-timeline-label">Actual</div>
                              </div>
                              <div className="compare-timeline-scrollable">
                                {/* Planned Row */}
                                <div className="compare-timeline-track is-planned">
                                  {plannedOrdered.map((d, i) => {
                                    const statusLow = String(d.orderstatus || '').toLowerCase();
                                    const isDelivered = FINAL_STATUSES.has(statusLow);
                                    const isSkipped = SKIPPED_STATUSES.has(statusLow);
                                    const isFocused = focusedCompareStep === d.sequenceStep;
                                    const isLoading = d.isLoading && d.coordsCount === 0;
                                    const isNoData = !d.isLoading && d.coordsCount === 0;
                                    const plannedStepNum = d.order?.step || d.sequenceStep;
                                    const color = stepColor(plannedStepNum - 1);
                                    const cls = [
                                      'compare-step',
                                      isFocused && 'is-focused',
                                      isDelivered && 'is-delivered',
                                      isSkipped && 'is-skipped',
                                      !isDelivered && !isSkipped && 'is-pending',
                                      isLoading && 'is-loading',
                                      isNoData && 'is-no-data',
                                      d.anomaly && 'is-anomaly'
                                    ].filter(Boolean).join(' ');
                                    return (
                                      <React.Fragment key={`step-p-${d.deliveryid}`}>
                                        {i > 0 && <span className="compare-step-spacer" />}
                                        <button
                                          type="button"
                                          className={cls}
                                          style={{ '--step-color': color }}
                                          onClick={() =>
                                            setFocusedCompareStep((prev) =>
                                              prev === d.sequenceStep ? null : d.sequenceStep
                                            )
                                          }
                                          title={
                                            `Planned Step ${plannedStepNum}` +
                                            (d.deliverycustomer ? ` · ${d.deliverycustomer}` : '') +
                                            (d.expectedTs ? ` · ${d.expectedTs.format('hh:mm A')}` : '') +
                                            (d.anomaly ? ' · deviation flagged' : '')
                                          }
                                        >
                                          <span className="compare-step-circle">
                                            {isLoading ? <span className="compare-step-spin" /> : plannedStepNum}
                                          </span>
                                          {d.expectedTs && (
                                            <span className="compare-step-tick">
                                              {d.expectedTs.format('HH:mm')}
                                            </span>
                                          )}
                                        </button>
                                      </React.Fragment>
                                    );
                                  })}
                                </div>

                                {/* Actual Row */}
                                <div className="compare-timeline-track is-actual">
                                  {actualOrdered.map((d, i) => {
                                    const statusLow = String(d.orderstatus || '').toLowerCase();
                                    const isDelivered = FINAL_STATUSES.has(statusLow);
                                    const isSkipped = SKIPPED_STATUSES.has(statusLow);
                                    const isFocused = focusedCompareStep === d.sequenceStep;
                                    const isLoading = d.isLoading && d.coordsCount === 0;
                                    const isNoData = !d.isLoading && d.coordsCount === 0;
                                    const plannedStepNum = d.order?.step || d.sequenceStep;
                                    const color = stepColor(plannedStepNum - 1);
                                    const cls = [
                                      'compare-step',
                                      isFocused && 'is-focused',
                                      isDelivered && 'is-delivered',
                                      isSkipped && 'is-skipped',
                                      !isDelivered && !isSkipped && 'is-pending',
                                      isLoading && 'is-loading',
                                      isNoData && 'is-no-data',
                                      d.anomaly && 'is-anomaly'
                                    ].filter(Boolean).join(' ');
                                    return (
                                      <React.Fragment key={`step-a-${d.deliveryid}`}>
                                        {i > 0 && <span className="compare-step-spacer" />}
                                        <button
                                          type="button"
                                          className={cls}
                                          style={{ '--step-color': color }}
                                          onClick={() =>
                                            setFocusedCompareStep((prev) =>
                                              prev === d.sequenceStep ? null : d.sequenceStep
                                            )
                                          }
                                          title={
                                            `Actual Visit ${i + 1} (Planned Step ${plannedStepNum})` +
                                            (d.deliverycustomer ? ` · ${d.deliverycustomer}` : '') +
                                            (d.actualTs ? ` · ${d.actualTs.format('hh:mm A')}` : '') +
                                            (d.anomaly ? ' · deviation flagged' : '')
                                          }
                                        >
                                          <span className="compare-step-circle">
                                            {isLoading ? <span className="compare-step-spin" /> : plannedStepNum}
                                          </span>
                                          {d.actualTs && (
                                            <span className="compare-step-tick">
                                              {d.actualTs.format('HH:mm')}
                                            </span>
                                          )}
                                          {d.anomaly && <span className="compare-step-flag" title="Deviation flagged" />}
                                        </button>
                                      </React.Fragment>
                                    );
                                  })}
                                </div>
                              </div>
                            </div>
                            <div className="compare-progress-strip">
                              <div className="compare-progress-bar-wrap">
                                <div
                                  className={`compare-progress-bar-fill${isDone ? ' is-done' : ''}`}
                                  style={{ width: `${pct}%` }}
                                />
                              </div>
                              <span className="compare-progress-text">
                                {loading > 0 ? `Loading GPS… ${loaded}/${total}` : `${loaded}/${total} tracks`}
                              </span>
                            </div>
                          </div>

                          {/* Legend strip — one line of icons so the operator instantly knows
                      what each line/marker means. Lives in the header so it doesn't
                      compete with the map for vertical real estate. */}
                          {(() => {
                            // Two color stories depending on view mode:
                            //   • Combined: polylines collapse to fixed indigo (planned)
                            //     and emerald (actual) so the two overlaid layers can be
                            //     told apart at a glance. Legend swatches mirror this.
                            //   • Planned-only / Actual-only: single layer on the map,
                            //     so polylines keep STEP_PALETTE and the swatch shows
                            //     the focused step's color or a step-gradient strip
                            //     (signals "varies by step").
                            const isCombined = compareViewMode === 'combined';
                            const stepSwatchBg = focusedDelta
                              ? stepColor(focusedDelta.sequenceStep - 1)
                              : `linear-gradient(90deg, ${STEP_PALETTE.slice(0, 6).join(', ')})`;
                            const plannedSwatchBg = isCombined ? COMBINED_PLANNED_COLOR : stepSwatchBg;
                            const actualSwatchBg = isCombined ? COMBINED_ACTUAL_COLOR : stepSwatchBg;
                            return (
                              <div className="compare-legend">
                                <span className="compare-legend-item">
                                  <span
                                    className="compare-legend-swatch is-step-color is-dashed"
                                    style={{ background: plannedSwatchBg }}
                                  />
                                  Planned (dashed)
                                </span>
                                <span className="compare-legend-item">
                                  <span
                                    className="compare-legend-swatch is-step-color"
                                    style={{ background: actualSwatchBg }}
                                  />
                                  Actual GPS (solid)
                                </span>
                                <span className="compare-legend-note">
                                  Kalman-smoothed GPS · OSRM road-snapped
                                </span>
                              </div>
                            );
                          })()}
                        </>
                      )}
                    </div>
                  );
                })()}

              </div>
            )}

            {/* Right-side data panel — full-height column in Compare mode. Renders
            day-overview tiles (distance, deviation, on-time, profit), the
            focused-step delta when a step is selected, a deviations list of
            anomaly steps, and a per-step list showing whether each step was
            followed correctly (delivered + within plan), the delivery time
            vs expected, and the order profit. Clicking any step row mirrors
            the timeline click — sets focusedCompareStep so both maps zoom
            onto that delivery. */}
            {compareOpen && focusedRider && (
              <CompareDataPanel
                focusedRider={focusedRider}
                compareDeltas={compareDeltas}
                compareSummary={compareSummary}
                actualOrdered={actualOrdered}
                focusedCompareStep={focusedCompareStep}
                setFocusedCompareStep={setFocusedCompareStep}
                sequenceOpen={sequenceOpen}
                setSequenceOpen={setSequenceOpen}
                expandedSeqGroups={expandedSeqGroups}
                setExpandedSeqGroups={setExpandedSeqGroups}
                onClose={() => setCompareOpen(false)}
              />
            )}
          </div>
        )}

        {/* Centered order popup — sibling of the map (NOT inside leaflet's
          transformed panes) so position: fixed actually pins it to the
          viewport. Replaces the marker-attached leaflet Popup so the rich
          order card stays fully visible at the screen center on small
          laptop displays. */}
        {centerPopupOrder && (
          <div
            className="dispatch-popup-center"
            role="dialog"
            aria-label={`Order ${centerPopupOrder.orderid} details`}
            onMouseEnter={() => {
              if (popupHoverTimerRef.current) {
                clearTimeout(popupHoverTimerRef.current);
                popupHoverTimerRef.current = null;
              }
            }}
            onMouseLeave={() => {
              if (isOrderPopupPinned(centerPopupOrder)) return;
              if (popupHoverTimerRef.current) clearTimeout(popupHoverTimerRef.current);
              popupHoverTimerRef.current = setTimeout(() => {
                setCenterPopupOrder(null);
                popupHoverTimerRef.current = null;
              }, 200);
            }}
          >
            <div className="dispatch-popup-card dispatch-popup">
              <button
                type="button"
                className="dispatch-popup-center-close"
                aria-label="Close order details"
                onClick={() => {
                  pinnedPopupsRef.current.delete(String(centerPopupOrder.orderid));
                  setCenterPopupOrder(null);
                }}
              >
                ×
              </button>
              {renderOrderPopupContent(centerPopupOrder)}
            </div>
          </div>
        )}
      </>)}

      {!embedded && topView === 'analysis' && (
        <div id="dispatch-analysis">
          <div className="da-picker-row">
            {ANALYSIS_BATCH_WINDOWS.map((b) => {
              const result = analysisResults[b.key];
              const isLoading = analysisLoadingWindow === b.key;
              const hasError = result?.data?.success === false;
              const statusBg = hasError ? '#fee2e2' : result ? `${b.color}22` : '#f1f5f9';
              const statusFg = hasError ? '#dc2626' : result ? b.color : '#64748b';
              const statusLabel = isLoading
                ? 'Loading…'
                : hasError
                  ? '! Failed'
                  : result
                    ? `✓ ${result.fetchedAt}`
                    : 'Fetch';
              const isActive = activeBatchKey === b.key;
              return (
                <button
                  key={b.key}
                  type="button"
                  className={`da-picker ${result ? 'has-data' : ''} ${isLoading ? 'is-loading' : ''} ${isActive ? 'is-active' : ''}`}
                  onClick={() => !isLoading && handleFetchAnalysisBatch(b.key)}
                  style={{
                    borderColor: isActive ? b.color : hasError ? '#fecaca' : result ? b.border : '#e2e8f0',
                    background: hasError ? '#fef2f2' : result ? b.bg : '#ffffff',
                    boxShadow: isActive ? `0 0 0 2px ${b.color}33` : undefined
                  }}
                  disabled={isLoading}
                >
                  <div className="da-picker-head">
                    <div
                      className="da-picker-badge"
                      style={{ background: `${b.color}22`, color: b.color }}
                    >
                      {b.label[0]}
                    </div>
                    <div className="da-picker-meta">
                      <div className="da-picker-name">{b.label}</div>
                      <div className="da-picker-range">{b.timeRange}</div>
                    </div>
                    <span
                      className="da-picker-status"
                      style={{ background: statusBg, color: statusFg }}
                    >
                      {statusLabel}
                    </span>
                  </div>
                  <div className="da-picker-sub">{b.sub}</div>
                </button>
              );
            })}
          </div>

          {(() => {
            if (!activeBatchKey) {
              return (
                <div className="da-empty">
                  Pick a batch above to view its efficiency analysis.
                </div>
              );
            }
            const activeMeta = ANALYSIS_BATCH_WINDOWS.find((b) => b.key === activeBatchKey);
            const cached = analysisResults[activeBatchKey];
            const isLoading = analysisLoadingWindow === activeBatchKey;

            if (isLoading && !cached) {
              return (
                <div className="da-empty">Loading {activeMeta.label} batch…</div>
              );
            }
            if (!cached) return null;

            const raw = cached.data || {};
            if (raw.success === false) {
              return (
                <div
                  className="da-result-card da-result-card-error"
                  style={{ borderColor: '#fecaca', borderTopColor: '#ef4444' }}
                >
                  <div className="da-result-head">
                    <div>
                      <div className="da-result-title">{activeMeta.label} Batch</div>
                      <div className="da-result-sub">
                        {activeMeta.timeRange} · Fetched at {cached.fetchedAt}
                      </div>
                    </div>
                    <button
                      type="button"
                      className="da-result-refresh"
                      title="Retry"
                      onClick={() => handleFetchAnalysisBatch(activeBatchKey)}
                      disabled={isLoading}
                      style={{ background: '#fee2e2', color: '#dc2626' }}
                    >
                      <MdRefresh />
                    </button>
                  </div>
                  <div className="da-error">
                    <div className="da-error-title">
                      <MdErrorOutline />
                      <span>{raw?.error?.code || 'Request failed'}</span>
                    </div>
                    <div className="da-error-msg">
                      {raw?.error?.message || 'The server returned an error.'}
                    </div>
                    {raw?.request_id && (
                      <div className="da-error-meta">request_id: {raw.request_id}</div>
                    )}
                  </div>
                </div>
              );
            }

            const fleet = raw.fleet_summary || {};
            // Renamed from `riders` to `apiRiders` so the outer-component
            // `riders` array (built from live dispatch data, has the real
            // riderName fields) stays accessible inside this block — we
            // join against it below to swap placeholder API names like
            // "Rider 883" for actual operator-facing names.
            const apiRiders = Array.isArray(raw.rider_timelines) ? raw.rider_timelines : [];
            const subs = Array.isArray(raw.substitution_opportunities) ? raw.substitution_opportunities : [];
            const rec = raw.top_recommendation;
            const hasRecRider = !!(rec && (rec.idle_rider_name || rec.idle_rider_id));
            const hasRec = !!(rec && rec.action && rec.action !== 'none' && hasRecRider);
            const win = raw.window || {};

            // userid → real rider name. The /batch/efficiency response only
            // ships placeholder names ("Rider 883") because the workolik
            // solver doesn't have our auth/users table. We resolve here:
            //   1. against `riders` (current dispatch data — has riderName)
            //   2. against `ridersAllDay` (full-day list built from live rows)
            //   3. fall back to whatever the API sent.
            const resolveRiderName = (uidLike, fallback) => {
              const uid = String(uidLike ?? '').trim();
              if (!uid) return fallback || '—';
              // The placeholder "Rider 883" pattern is what we WANT to upgrade,
              // so don't treat it as a usable fallback — keep looking. Only
              // accept the fallback if it's a real name (no "Rider <digits>"
              // pattern).
              const fallbackIsPlaceholder = typeof fallback === 'string' && /^Rider\s+\d+$/i.test(fallback.trim());
              const liveMatch = Array.isArray(riders)
                ? riders.find((x) => String(x.id) === uid)
                : null;
              if (liveMatch && liveMatch.riderName) return liveMatch.riderName;
              const allDayMatch = Array.isArray(ridersAllDay)
                ? ridersAllDay.find((x) => String(x.id) === uid)
                : null;
              if (allDayMatch && allDayMatch.riderName) return allDayMatch.riderName;
              if (!fallbackIsPlaceholder && fallback) return fallback;
              return `Rider ${uid}`;
            };

            // ─── Fleet-window math for the gantt + duration display ─────────
            const fleetStartSec = parseHMS(fleet.fleet_start);
            const fleetEndSec = parseHMS(fleet.fleet_done);
            const fleetTotalSec =
              fleetStartSec != null && fleetEndSec != null && fleetEndSec > fleetStartSec
                ? fleetEndSec - fleetStartSec
                : null;

            // ─── Utilisation health bucket ─────────────────────────────────
            const util = parseFloat(fleet.avg_utilisation_pct);
            const utilSafe = Number.isFinite(util) ? util : null;
            const utilHealth =
              utilSafe == null ? 'unknown'
                : utilSafe >= 85 ? 'good'
                  : utilSafe >= 70 ? 'ok'
                    : 'low';
            const utilColor =
              utilHealth === 'good' ? '#10b981'
                : utilHealth === 'ok' ? '#f59e0b'
                  : utilHealth === 'low' ? '#ef4444'
                    : '#94a3b8';

            // ─── Severity for the recommendation banner ────────────────────
            const severity = analysisSeverity({
              assessment: rec?.fleet_balance_assessment,
              spreadMin: parseFloat(fleet.finish_time_spread_minutes),
              stdev: parseFloat(fleet.load_balance_stdev)
            });

            // ─── Augment each rider with gantt percentages + per-rider util ─
            //   `apiRiders` is the API payload's `rider_timelines`; we also
            //   attach `displayName` (resolved against the in-app riders
            //   lookup) so the JSX below renders real names instead of the
            //   placeholder "Rider {userid}" the API returns.
            const ridersWithGantt = apiRiders.map((r) => {
              const startSec = parseHMS(r.started_at);
              const endSec = parseHMS(r.finished_at);
              let startPct = 0;
              let endPct = 100;
              if (fleetStartSec != null && fleetTotalSec) {
                if (startSec != null) startPct = Math.max(0, Math.min(100, ((startSec - fleetStartSec) / fleetTotalSec) * 100));
                if (endSec != null) endPct = Math.max(0, Math.min(100, ((endSec - fleetStartSec) / fleetTotalSec) * 100));
              }
              const activePct = Math.max(0, endPct - startPct);
              const activeMin = parseFloat(r.active_minutes);
              const idleMin = parseFloat(r.idle_minutes);
              // Per-rider utilisation = active / (active + idle). Falls back to
              // the fleet avg when individual fields are missing so the bar's
              // color never goes "unknown" mid-list.
              const totalRiderMin = (Number.isFinite(activeMin) ? activeMin : 0) + (Number.isFinite(idleMin) ? idleMin : 0);
              const riderUtilPct = totalRiderMin > 0 ? (activeMin / totalRiderMin) * 100 : utilSafe;
              const riderHealth =
                riderUtilPct == null ? 'unknown'
                  : riderUtilPct >= 85 ? 'good'
                    : riderUtilPct >= 70 ? 'ok'
                      : 'low';
              const riderColor =
                riderHealth === 'good' ? '#10b981'
                  : riderHealth === 'ok' ? '#f59e0b'
                    : riderHealth === 'low' ? '#ef4444'
                      : '#94a3b8';
              const displayName = resolveRiderName(r.userid, r.name);
              return { ...r, displayName, _startPct: startPct, _endPct: endPct, _activePct: activePct, _riderUtilPct: riderUtilPct, _riderColor: riderColor };
            });
            // Sort: latest-finish first (= the rider who held up the fleet).
            // Operators care about who's the bottleneck, not who's first.
            ridersWithGantt.sort((a, b) => (parseHMS(b.finished_at) || 0) - (parseHMS(a.finished_at) || 0));

            return (
              <div className="da-detail">
                <div
                  className="da-detail-head"
                  style={{ borderTopColor: activeMeta.color, background: activeMeta.bg }}
                >
                  <div>
                    <div className="da-detail-title">
                      {activeMeta.label} Batch
                      <span className="da-detail-sub-inline">
                        {raw.date ? ` · ${raw.date}` : ''}
                        {win.from && win.to ? ` · ${win.from} – ${win.to}` : ''}
                      </span>
                    </div>
                    <div className="da-detail-sub">
                      Fetched at {cached.fetchedAt} · Input deliveries: {raw.input_delivery_count ?? '—'}
                    </div>
                  </div>
                  <div className="da-detail-actions">
                    {/* Live indicator: confirms to the operator that the
                        panel is polling and not just a stale snapshot. */}
                    <span
                      className={`da-live-tag ${isLoading ? 'is-active' : ''}`}
                      title={`Auto-refreshing every ${Math.round(ANALYSIS_POLL_MS / 1000)}s`}
                    >
                      <span className="da-live-dot" />
                      Live · {Math.round(ANALYSIS_POLL_MS / 1000)}s
                    </span>
                    <button
                      type="button"
                      className="da-result-refresh"
                      title="Refresh now"
                      onClick={() => {
                        // Force refetch (bypass cache)
                        setAnalysisResults((prev) => {
                          const next = { ...prev };
                          delete next[activeBatchKey];
                          return next;
                        });
                        batchEfficiencyMutation.mutate({
                          batch: activeBatchKey,
                          tenantId: ANALYSIS_TENANT_ID
                        });
                      }}
                      disabled={isLoading}
                      style={{ background: `${activeMeta.color}22`, color: activeMeta.color }}
                    >
                      <MdRefresh />
                    </button>
                  </div>
                </div>

                {/* ─── Hero strip: the three numbers an ops lead glances at
                    first — workload, headcount, and how long the batch
                    actually ran. Sits above everything else so the answer
                    to "was today big or small?" is the first thing visible. */}
                <div className="da-section">
                  <div className="da-hero-row">
                    <div className="da-hero-card">
                      <div className="da-hero-icon" style={{ background: '#6625821f', color: '#662582' }}>
                        <MdOutlineInventory2 />
                      </div>
                      <div className="da-hero-value">{analysisFormatNum(fleet.total_orders)}</div>
                      <div className="da-hero-label">Total Orders</div>
                      {raw.input_delivery_count != null && raw.input_delivery_count !== fleet.total_orders && (
                        <div className="da-hero-sub">
                          {raw.input_delivery_count} input
                        </div>
                      )}
                    </div>
                    <div className="da-hero-card">
                      <div className="da-hero-icon" style={{ background: '#0ea5e91f', color: '#0ea5e9' }}>
                        <MdOutlineTwoWheeler />
                      </div>
                      <div className="da-hero-value">{analysisFormatNum(fleet.total_riders)}</div>
                      <div className="da-hero-label">Active Riders</div>
                      {fleet.orders_per_rider_avg != null && (
                        <div className="da-hero-sub">
                          {fleet.orders_per_rider_avg} avg orders / rider
                        </div>
                      )}
                    </div>
                    <div className="da-hero-card">
                      <div className="da-hero-icon" style={{ background: '#8b5cf61f', color: '#8b5cf6' }}>
                        <MdOutlineAccessTime />
                      </div>
                      <div className="da-hero-value">
                        {fleet.total_duration_minutes != null ? `${fleet.total_duration_minutes}` : '—'}
                        {fleet.total_duration_minutes != null && (
                          <span className="da-hero-unit"> min</span>
                        )}
                      </div>
                      <div className="da-hero-label">Fleet Window</div>
                      {fleet.fleet_start && fleet.fleet_done && (
                        <div className="da-hero-sub">
                          {fleet.fleet_start} → {fleet.fleet_done}
                        </div>
                      )}
                    </div>
                  </div>
                </div>

                {/* ─── Health KPI band: efficiency + balance signals.
                    These tell the operator HOW the batch ran — utilisation
                    ring (visual gauge), load balance stdev (lower = better
                    workload spread), finish-time spread (lower = tighter
                    finish), and average active minutes per rider. */}
                <div className="da-section">
                  <div className="da-section-label">Fleet Health</div>
                  <div className="da-health-row">
                    {/* Utilisation — the only visual gauge: lets a busy
                        ops lead spot "we left 30% of the day on the table"
                        without reading the number. */}
                    <div className="da-health-card">
                      <div className="da-ring" style={{ '--ring-color': utilColor, '--ring-pct': utilSafe != null ? Math.max(0, Math.min(100, utilSafe)) : 0 }}>
                        <span className="da-ring-num">
                          {utilSafe != null ? `${utilSafe.toFixed(0)}` : '—'}
                          {utilSafe != null && <span className="da-ring-unit">%</span>}
                        </span>
                      </div>
                      <div className="da-health-meta">
                        <div className="da-health-label">Avg Utilisation</div>
                        <div className="da-health-sub" style={{ color: utilColor }}>
                          {utilHealth === 'good' ? 'Strong' : utilHealth === 'ok' ? 'Moderate' : utilHealth === 'low' ? 'Low' : '—'}
                        </div>
                      </div>
                    </div>
                    <div className="da-health-card da-health-card-stat">
                      <div className="da-health-value">
                        {fleet.load_balance_stdev != null ? parseFloat(fleet.load_balance_stdev).toFixed(2) : '—'}
                      </div>
                      <div className="da-health-label">Load Balance σ</div>
                      <div className="da-health-sub">
                        {fleet.load_balance_stdev != null
                          ? (parseFloat(fleet.load_balance_stdev) > 1.0 ? 'Uneven workload' : 'Workload balanced')
                          : 'Stdev of orders / rider'}
                      </div>
                    </div>
                    <div className="da-health-card da-health-card-stat">
                      <div className="da-health-value">
                        {fleet.finish_time_spread_minutes != null ? `${fleet.finish_time_spread_minutes}` : '—'}
                        {fleet.finish_time_spread_minutes != null && (
                          <span className="da-health-unit"> min</span>
                        )}
                      </div>
                      <div className="da-health-label">Finish Spread</div>
                      <div className="da-health-sub">
                        {fleet.finish_time_stdev_minutes != null
                          ? `σ ${parseFloat(fleet.finish_time_stdev_minutes).toFixed(1)} min`
                          : 'First → last finish'}
                      </div>
                    </div>
                    <div className="da-health-card da-health-card-stat">
                      <div className="da-health-value">
                        {fleet.avg_active_minutes != null ? `${parseFloat(fleet.avg_active_minutes).toFixed(0)}` : '—'}
                        {fleet.avg_active_minutes != null && (
                          <span className="da-health-unit"> min</span>
                        )}
                      </div>
                      <div className="da-health-label">Avg Active</div>
                      <div className="da-health-sub">
                        Per rider, this batch
                      </div>
                    </div>
                  </div>
                </div>

                {/* ─── Top Recommendation banner ───────────────────────────
                    Three states:
                      • Actionable rec      → blue insight banner with full
                                              rider + kitchen + activation rules.
                      • No action, imbalance → AMBER warning surfacing the
                                              backend's `reason` +
                                              `fleet_balance_assessment` so
                                              operators don't miss a real
                                              "57-min spread" signal hidden
                                              behind a generic empty state.
                      • No action, balanced  → green success summary. */}
                <div className="da-section">
                  <div className="da-section-label">Top Recommendation</div>
                  {hasRec ? (
                    <div className="da-rec-banner is-action">
                      <div className="da-rec-banner-icon"><MdInsights /></div>
                      <div className="da-rec-banner-body">
                        <div className="da-rec-head">
                          <div className="da-rec-action">
                            <span>{(rec.action || 'recommendation').replaceAll('_', ' ')}</span>
                          </div>
                          {rec.fleet_improvement_minutes != null && (
                            <span
                              className="da-rec-improve"
                              style={
                                rec.fleet_improvement_minutes > 0
                                  ? { background: '#dcfce7', color: '#166534' }
                                  : { background: '#f1f5f9', color: '#475569' }
                              }
                            >
                              {rec.fleet_improvement_minutes > 0 ? '↑' : '•'} Fleet improves by {rec.fleet_improvement_minutes} min
                            </span>
                          )}
                        </div>
                        <div className="da-rec-line">
                          <strong>{resolveRiderName(rec.idle_rider_id, rec.idle_rider_name)}</strong>
                          {rec.primary_kitchen && (
                            <> · primary kitchen <strong>{rec.primary_kitchen}</strong></>
                          )}
                          {rec.second_kitchen && (
                            <> → also serve <strong>{rec.second_kitchen}</strong> after {rec.second_kitchen_dispatch_after || '—'}</>
                          )}
                        </div>
                        {rec.description && (
                          <div className="da-rec-desc">{rec.description}</div>
                        )}
                        {rec?.fleet_balance_assessment && (
                          <div className="da-rec-assess">{rec.fleet_balance_assessment}</div>
                        )}
                        {rec.activate_when?.rules?.length > 0 && (
                          <div className="da-rec-rules">
                            <div className="da-rec-rules-head">
                              Activate when ({rec.activate_when.condition || 'AND'}):
                            </div>
                            {rec.activate_when.rules.map((rule, i) => (
                              <div key={i} className="da-rec-rule">
                                <code>{rule.field} {rule.operator} {rule.value}</code>
                                {rule.reason && <span className="da-rec-rule-why"> — {rule.reason}</span>}
                              </div>
                            ))}
                          </div>
                        )}
                      </div>
                    </div>
                  ) : (
                    <div className={`da-rec-banner is-${severity}`}>
                      <div className="da-rec-banner-icon">
                        {severity === 'warn' ? <MdWarning /> : severity === 'success' ? <MdCheckCircle /> : <MdInsights />}
                      </div>
                      <div className="da-rec-banner-body">
                        <div className="da-rec-action">
                          <span>
                            {severity === 'warn'
                              ? 'Fleet imbalance detected — no feasible substitution'
                              : severity === 'success'
                                ? 'Fleet ran balanced — no reassignment needed'
                                : 'No action recommended'}
                          </span>
                        </div>
                        {rec?.reason && (
                          <div className="da-rec-desc">{rec.reason}</div>
                        )}
                        {rec?.fleet_balance_assessment && (
                          <div className="da-rec-assess">{rec.fleet_balance_assessment}</div>
                        )}
                      </div>
                    </div>
                  )}
                </div>

                {ridersWithGantt.length > 0 && (
                  <div className="da-section">
                    <div className="da-section-label">
                      Rider Timelines <span className="da-section-count">({ridersWithGantt.length})</span>
                      <span className="da-section-hint">Sorted by latest finish — bottleneck riders first</span>
                    </div>
                    {/* Fleet timeline header: shared axis labels so each
                        rider's bar is reads against a common reference. */}
                    <div className="da-gantt-axis">
                      <span>{fleet.fleet_start || '—'}</span>
                      <span className="da-gantt-axis-mid">
                        {fleet.total_duration_minutes != null ? `${fleet.total_duration_minutes} min batch` : 'Batch window'}
                      </span>
                      <span>{fleet.fleet_done || '—'}</span>
                    </div>
                    <div className="da-timeline-list">
                      {ridersWithGantt.map((r) => {
                        const isActive = String(r.status || '').toLowerCase() === 'active';
                        const completed = r.completed_orders ?? r.order_count;
                        const pending = r.pending_orders ?? 0;
                        const completePct = (completed != null && r.order_count > 0)
                          ? Math.max(0, Math.min(100, (completed / r.order_count) * 100))
                          : 0;
                        const idleHigh = r.idle_minutes != null && r.idle_minutes > 30;
                        const lowConf = r.kitchen_confidence != null && r.kitchen_confidence < 0.7;
                        return (
                          <div key={r.userid} className="da-timeline-card">
                            {/* Top: identity + status pill. The status pill
                                stays the existing .da-pill so colour rules
                                already documented elsewhere apply. */}
                            <div className="da-timeline-top">
                              <div className="da-timeline-name">
                                <MdTwoWheeler style={{ color: r._riderColor }} />
                                <span title={r.name && r.name !== r.displayName ? `API name: ${r.name}` : undefined}>
                                  {r.displayName}
                                </span>
                                <span className="da-timeline-id">#{r.userid}</span>
                              </div>
                              <span className={`da-pill ${isActive ? 'is-active' : 'is-idle'}`}>
                                {r.status}
                              </span>
                            </div>

                            {/* Gantt bar: bottom-aligned bar shows when this
                                rider was working inside the fleet window.
                                The filled portion is their active span; the
                                tail (after finished_at) reads as "fleet
                                kept going without me" — i.e. idle time on
                                the back end. Width and offset are computed
                                as % of total fleet duration. */}
                            <div className="da-gantt-row">
                              <div
                                className="da-gantt-track"
                                title={`${r.started_at} → ${r.finished_at}`}
                              >
                                <div
                                  className="da-gantt-fill"
                                  style={{
                                    left: `${r._startPct}%`,
                                    width: `${Math.max(2, r._activePct)}%`,
                                    background: r._riderColor
                                  }}
                                >
                                  <span className="da-gantt-fill-start">{r.started_at}</span>
                                  <span className="da-gantt-fill-end">{r.finished_at}</span>
                                </div>
                                {r._endPct < 99 && (
                                  <div
                                    className="da-gantt-idle"
                                    style={{ left: `${r._endPct}%`, width: `${100 - r._endPct}%` }}
                                    title={`Idle ${r.idle_minutes} min after finishing`}
                                  />
                                )}
                              </div>
                              <span
                                className="da-gantt-util"
                                style={{ background: `${r._riderColor}22`, color: r._riderColor }}
                                title={`Per-rider utilisation: ${r._riderUtilPct != null ? r._riderUtilPct.toFixed(0) : '—'}%`}
                              >
                                {r._riderUtilPct != null ? `${r._riderUtilPct.toFixed(0)}%` : '—'}
                              </span>
                            </div>

                            {/* Stats row: kitchen (with confidence dot
                                when low), orders mini-bar, pace, active,
                                idle, free-window, and last GPS position. */}
                            <div className="da-timeline-mid">
                              {r.kitchen ? (
                                <span className="da-chip" title={r.kitchen_confidence != null ? `Kitchen confidence: ${(r.kitchen_confidence * 100).toFixed(0)}%` : undefined}>
                                  <MdRestaurant /> {r.kitchen}
                                  {r.kitchen_confidence != null && (
                                    <span
                                      className={`da-conf-dot ${lowConf ? 'is-low' : 'is-ok'}`}
                                      aria-label={`Confidence ${(r.kitchen_confidence * 100).toFixed(0)}%`}
                                    />
                                  )}
                                </span>
                              ) : (
                                <span
                                  className="da-chip"
                                  style={{ background: '#f8fafc', color: '#64748b', borderColor: '#e2e8f0', borderStyle: 'dashed' }}
                                  title="Solver did not identify a primary kitchen for this rider"
                                >
                                  <MdRestaurant /> Unassigned
                                </span>
                              )}
                              <span className="da-chip da-chip-orders" title={`Completed ${completed} of ${r.order_count}`}>
                                <MdInventory2 />
                                <span className="da-orders-label">
                                  {completed}/{r.order_count}
                                </span>
                                <span className="da-orders-bar">
                                  <span
                                    className="da-orders-bar-fill"
                                    style={{ width: `${completePct}%` }}
                                  />
                                </span>
                                {pending > 0 && (
                                  <span className="da-orders-pending">{pending} pending</span>
                                )}
                              </span>
                              {r.pace_orders_per_hour != null && (
                                <span className="da-chip" title="Delivery pace this batch">
                                  <MdSpeed /> {parseFloat(r.pace_orders_per_hour).toFixed(1)} / hr
                                </span>
                              )}
                              {r.active_minutes != null && (
                                <span className="da-chip" title="Time actively delivering">
                                  <MdTimer /> {parseFloat(r.active_minutes).toFixed(0)} min active
                                </span>
                              )}
                              <span
                                className="da-chip"
                                style={idleHigh ? { background: '#fef3c7', color: '#92400e', borderColor: '#fde68a' } : undefined}
                                title="Idle time after this rider finished — fleet kept running"
                              >
                                <MdAccessTime /> {r.idle_minutes ?? 0} min idle
                              </span>
                              {/* Free window: time available before the next
                                  batch starts. Often equals idle_minutes but
                                  diverges when the next batch sits closer to
                                  this rider's finish — operators use this to
                                  decide who can take an extra run. */}
                              {r.free_window_minutes != null && (
                                <span
                                  className="da-chip"
                                  style={
                                    r.free_window_minutes >= 30
                                      ? { background: '#ecfdf5', color: '#065f46', borderColor: '#a7f3d0' }
                                      : r.free_window_minutes > 0
                                        ? { background: '#fefce8', color: '#854d0e', borderColor: '#fde68a' }
                                        : { background: '#fef2f2', color: '#991b1b', borderColor: '#fecaca' }
                                  }
                                  title="Free window before the next batch — capacity available for a follow-up run"
                                >
                                  <MdTimer /> {parseFloat(r.free_window_minutes).toFixed(0)} min free
                                </span>
                              )}
                            </div>
                          </div>
                        );
                      })}
                    </div>
                  </div>
                )}

                {subs.length > 0 && (
                  <div className="da-section">
                    <div className="da-section-label">
                      Substitution Opportunities <span className="da-section-count">({subs.length})</span>
                    </div>
                    <div className="da-sub-list">
                      {subs.map((s, i) => {
                        const idle = s.idle_rider || {};
                        const relieved = s.most_relieved_rider || {};
                        const improved = s.fleet_improvement_minutes ?? 0;
                        return (
                          <div key={i} className="da-sub-card">
                            <div className="da-sub-head">
                              <div className="da-sub-title">
                                <strong>{resolveRiderName(idle.userid, idle.name)}</strong>{' '}
                                covers <strong>{s.target_kitchen}</strong>
                              </div>
                              <span
                                className="da-sub-improve"
                                style={
                                  improved > 0
                                    ? { background: '#dcfce7', color: '#166534' }
                                    : { background: '#f1f5f9', color: '#475569' }
                                }
                              >
                                Fleet {improved > 0 ? '↑' : '•'} {improved} min
                              </span>
                            </div>
                            <div className="da-sub-meta">
                              <span className="da-chip">
                                <MdStraighten /> {s.travel_to_kitchen_km} km
                              </span>
                              <span className="da-chip">
                                <MdTimer /> {s.travel_to_kitchen_minutes} min travel
                              </span>
                              <span className="da-chip">
                                <MdAccessTime /> arrives {s.arrive_at_kitchen}
                              </span>
                              <span className="da-chip">
                                <MdInventory2 /> {s.total_orders_transferred} orders
                              </span>
                              <span className="da-chip">
                                <MdStraighten /> +{s.extra_km_for_idle_rider} km for idle rider
                              </span>
                            </div>
                            {(relieved.name || relieved.userid != null) && (
                              <div className="da-sub-relieved">
                                <MdTrendingUp />
                                Most relieved: <strong>{resolveRiderName(relieved.userid, relieved.name)}</strong>{' '}
                                ({relieved.original_finish} → {relieved.new_finish}, saves{' '}
                                {relieved.time_saved_minutes} min)
                              </div>
                            )}
                            {Array.isArray(s.orders_to_transfer) && s.orders_to_transfer.length > 0 && (
                              <div className="da-sub-transfers">
                                <div className="da-sub-transfers-head">Orders transferred</div>
                                {s.orders_to_transfer.map((o) => {
                                  const imp = o.improvement_minutes ?? 0;
                                  return (
                                    <div key={o.deliveryid} className="da-transfer-row">
                                      <span className="da-transfer-id">#{o.deliveryid}</span>
                                      <span className="da-transfer-from">
                                        from {resolveRiderName(o.from_rider_id || o.from_userid, o.from_rider_name)}
                                      </span>
                                      <span className="da-transfer-time">
                                        {o.original_delivery_time} → {o.estimated_delivery_time}
                                      </span>
                                      <span
                                        className="da-transfer-imp"
                                        style={
                                          imp > 0
                                            ? { background: '#dcfce7', color: '#166534' }
                                            : imp < 0
                                              ? { background: '#fee2e2', color: '#991b1b' }
                                              : { background: '#f1f5f9', color: '#475569' }
                                        }
                                      >
                                        {imp > 0 ? '+' : ''}{imp} min
                                      </span>
                                    </div>
                                  );
                                })}
                              </div>
                            )}
                          </div>
                        );
                      })}
                    </div>
                  </div>
                )}
              </div>
            );
          })()}
        </div>
      )}

      {/* ─── Rider last-position modal (Analysis section).
          A small Leaflet map pinned to the rider's `last_position` from
          /batch/efficiency. Mounted at the top level so the MapContainer
          is created/destroyed exactly once per open — Leaflet maps are
          expensive to remount and behave badly when their parent moves
          around in the tree. */}
      {riderPositionModal && (
        <div
          className="da-pos-modal-backdrop"
          role="dialog"
          aria-modal="true"
          aria-label={`Last known position of ${riderPositionModal.name}`}
          onClick={(e) => {
            if (e.target === e.currentTarget) setRiderPositionModal(null);
          }}
        >
          <div className="da-pos-modal-card">
            <div className="da-pos-modal-head">
              <div className="da-pos-modal-title-wrap">
                <div
                  className="da-pos-modal-avatar"
                  style={{ background: `${riderPositionModal.color || '#662582'}22`, color: riderPositionModal.color || '#662582' }}
                >
                  <MdTwoWheeler />
                </div>
                <div>
                  <div className="da-pos-modal-title">{riderPositionModal.name}</div>
                  <div className="da-pos-modal-sub">
                    #{riderPositionModal.userid}
                    {riderPositionModal.kitchen && <> · {riderPositionModal.kitchen}</>}
                    {riderPositionModal.finished_at && <> · finished {riderPositionModal.finished_at}</>}
                  </div>
                </div>
              </div>
              <div className="da-pos-modal-actions">
                <span
                  className={`da-pill ${String(riderPositionModal.status || '').toLowerCase() === 'active' ? 'is-active' : 'is-idle'}`}
                >
                  {riderPositionModal.status || '—'}
                </span>
                <button
                  type="button"
                  className="da-pos-modal-close"
                  aria-label="Close"
                  onClick={() => setRiderPositionModal(null)}
                >
                  <MdClose />
                </button>
              </div>
            </div>

            <div className="da-pos-modal-map">
              <MapContainer
                key={`${riderPositionModal.userid}-${riderPositionModal.lat}-${riderPositionModal.lon}`}
                center={[riderPositionModal.lat, riderPositionModal.lon]}
                /* Zoom 13 ≈ city-district view: enough surrounding roads
                   and landmarks to give the marker spatial context. Was
                   16 (block level) which felt over-zoomed for ops. */
                zoom={13}
                scrollWheelZoom
                style={{ width: '100%', height: '100%' }}
                /* Leaflet caches the container size at construction; the
                   modal mounts mid-animation (transform + opacity) so the
                   measured size is stale. Force a recalculation on the
                   next frame and once more after the rise animation
                   completes (~240ms) so the tile pane fills correctly. */
                whenReady={(e) => {
                  const map = e.target;
                  requestAnimationFrame(() => map.invalidateSize());
                  setTimeout(() => map.invalidateSize(), 260);
                }}
              >
                <TileLayer
                  attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
                  url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                />
                <Marker position={[riderPositionModal.lat, riderPositionModal.lon]}>
                  <Popup>
                    <strong>{riderPositionModal.name}</strong>
                    <br />
                    {riderPositionModal.lat.toFixed(6)}, {riderPositionModal.lon.toFixed(6)}
                  </Popup>
                </Marker>
              </MapContainer>
            </div>

            <div className="da-pos-modal-foot">
              <div className="da-pos-modal-coord">
                <MdLocationOn />
                <span>
                  <strong>Lat:</strong> {riderPositionModal.lat.toFixed(6)}
                  <span className="da-pos-modal-sep">·</span>
                  <strong>Lon:</strong> {riderPositionModal.lon.toFixed(6)}
                </span>
              </div>
              <button
                type="button"
                className="da-pos-modal-copy"
                onClick={() => {
                  const txt = `${riderPositionModal.lat.toFixed(6)}, ${riderPositionModal.lon.toFixed(6)}`;
                  if (navigator?.clipboard?.writeText) navigator.clipboard.writeText(txt);
                }}
                title="Copy coordinates"
              >
                Copy
              </button>
            </div>
          </div>
        </div>
      )}

    </div>
  );
};

export default Dispatch;
