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';
import dayjs from 'dayjs';
import { useInfiniteQuery, useQueries, useQuery } 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,
  MdGpsFixed,
  MdPower,
  MdSearch,
  MdChevronLeft,
  MdChevronRight,
  MdLocalMall,
  MdCheckCircle,
  MdErrorOutline,
  MdWarning,
  MdClose,
  MdFormatListBulleted,
  MdTimer
} from 'react-icons/md';
import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs } from '../../api/api';
import {
  STATUS_STYLES,
  getStatusStyle,
  FINAL_STATUSES,
  SKIPPED_STATUSES,
  STEP_PALETTE,
  stepColor
} from './dispatchShared';
import CompareDataPanel from './CompareDataPanel';
import './Dispatch.css';

// Phosphor "motorcycle" (filled) — clean side-view bike that reads well at small sizes.
const MOTORBIKE_SVG = `<svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="#fff" d="M200,112a40,40,0,0,0-12.07,1.86L161.6,72H200a8,8,0,0,1,8,8v8a8,8,0,0,0,16,0V80a24,24,0,0,0-24-24H160a8,8,0,0,0-6.79,3.77l-9.34,15.06L130.39,52A8,8,0,0,0,124,48H88a8,8,0,0,0,0,16h31.69l13.34,21.84L107.5,128H56A40,40,0,1,0,96,168.4V160a8,8,0,0,1,16,0v8.4a40.06,40.06,0,0,0,32,31.2V184a8,8,0,0,1,16,0v15.6A40,40,0,1,0,200,112ZM56,184a24,24,0,1,1,24-24A24,24,0,0,1,56,184Zm70.46-44.71h0L141,116.45,156.6,142h0a40,40,0,0,0-14,28h-16A40.16,40.16,0,0,0,126.46,139.29ZM200,184a24,24,0,1,1,24-24A24,24,0,0,1,200,184Z"/></svg>`;

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 slots — operator's mental model of the day's waves.
// Each entry covers a half-open hour range [startHour, endHour). Hours that
// fall outside every slot (e.g. 11 AM, the gap between Slot 1 and Slot 2)
// produce a null batch and the order won't appear in any chip.
// Slot 5 ends at 24 so anything from 8 PM until midnight buckets there.
// Default slot layout. Used as the seed for the editable slot config the
// operator can tweak at runtime — see slotsConfig state + the slot-edit
// popover below. Don't read BATCHES_DEFAULT directly at runtime; read
// component state instead so user edits take effect.
const BATCHES_DEFAULT = [
  { id: 'slot-1', label: 'Slot 1 · 8 AM',    range: '8–11 AM',    startHour: 8,  endHour: 11 },
  { id: 'slot-2', label: 'Slot 2 · 12 PM',   range: '12–3 PM',    startHour: 12, endHour: 15 },
  { id: 'slot-3', label: 'Slot 3 · 3 PM',    range: '3–7 PM',     startHour: 15, endHour: 19 },
  { id: 'slot-4', label: 'Slot 4 · 7 PM',    range: '7–8 PM',     startHour: 19, endHour: 20 },
  { id: 'slot-5', label: 'Slot 5 · 8 PM',    range: 'After 8 PM', startHour: 20, endHour: 24 }
];

const SLOTS_STORAGE_KEY = 'dispatch.slots.v1';

// Build a label like "Slot 1 · 8 AM" from a startHour (24h). Mirrors the
// human-readable form the defaults use, so user-edited slots still look
// consistent in the UI.
const formatSlotLabel = (idx, startHour) => {
  const h = ((startHour + 11) % 12) + 1;
  const ampm = startHour >= 12 && startHour < 24 ? 'PM' : 'AM';
  return `Slot ${idx + 1} · ${h} ${ampm}`;
};

const formatHourLabel = (h) => {
  const hr = ((h + 11) % 12) + 1;
  const ampm = h >= 12 && h < 24 ? 'PM' : 'AM';
  return `${hr} ${ampm}`;
};

const formatSlotRange = (startHour, endHour) => {
  if (endHour >= 24) return `After ${formatHourLabel(startHour)}`;
  return `${formatHourLabel(startHour)}–${formatHourLabel(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: 'delivery', label: 'Delivery', keys: ['deliverytime', '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'] }
];

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 = 'delivery', 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;
  return getBatchForHour(d.hour(), 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;
}

// 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 for GPS pings ─────────────────────────────────────────
//
// Two independent 1D Kalman filters (one for lat, one for lng) applied to a
// chronologically sorted list of GPS pings. Per-axis state: [position,
// velocity]. Constant-velocity dynamics with random acceleration as process
// noise; measurement model H = [1, 0] (we measure position only).
//
// Why a Kalman filter here:
//   Raw /getdeliverylogs pings contain jitter (multi-path in dense urban
//   areas), brief stationary noise (rider parked at the drop, GPS still
//   wandering), and occasional outliers (cold-start fix). The Kalman pass
//   fuses each ping with the predicted trajectory from prior pings, weighted
//   by their relative uncertainty. The output is a smooth polyline that
//   tracks the rider's real path without the zig-zags and bunched-up dots
//   near drops, and it costs O(N) — runs once per delivery on fetch.
//
// Tuning:
//   processNoise (q)   — random-acceleration variance (deg²/s²). Lower = a
//                        smoother result but slower to follow sharp turns.
//   measurementNoise (r) — GPS-fix variance (deg²). Higher = trust pings
//                          less, lean on the predicted state more.
//   The defaults below correspond loosely to ~5m GPS accuracy and gentle
//   urban acceleration. Bump q if smoothing eats genuine turns; bump r if
//   the line still wiggles between pings.
function kalmanSmoothGps(pings, options = {}) {
  if (!Array.isArray(pings) || pings.length === 0) return [];
  if (pings.length === 1) {
    return [{ lat: pings[0].lat, lng: pings[0].lng, logdate: pings[0].logdate }];
  }

  const processNoise =
    options.processNoise != null ? options.processNoise : 1e-9;
  const measurementNoise =
    options.measurementNoise != null ? options.measurementNoise : 1e-7;

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

  // Run a 1D Kalman over one axis (lat or lng). State: [pos, vel]; cov: 2x2.
  const smoothAxis = (axisKey) => {
    let x = pings[0][axisKey]; // position estimate
    let v = 0;                  // velocity estimate
    // Initial covariance — large enough that the first few measurements
    // dominate over the initial state.
    let p00 = 1, p01 = 0, p10 = 0, p11 = 1;
    const out = [x];
    let prevTs = tsOf(pings[0]);

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

      // ─── Predict ───
      // x' = F x          where F = [[1, dt], [0, 1]]
      const xPred = x + v * dt;
      const vPred = v;
      // P' = F P F^T + Q  where Q = q · [[dt⁴/4, dt³/2], [dt³/2, dt²]]
      const dt2 = dt * dt;
      const dt3 = dt2 * dt;
      const dt4 = dt3 * dt;
      const np00 = p00 + dt * (p01 + p10) + dt2 * p11 + (dt4 / 4) * processNoise;
      const np01 = p01 + dt * p11 + (dt3 / 2) * processNoise;
      const np10 = p10 + dt * p11 + (dt3 / 2) * processNoise;
      const np11 = p11 + dt2 * processNoise;

      // ─── Update ───
      // y = z − Hx'       (innovation, measurement vs prediction)
      const z = pings[i][axisKey];
      const y = z - xPred;
      // S = H P' H^T + R  (innovation covariance)
      const S = np00 + measurementNoise;
      // K = P' H^T / S    (Kalman gain)
      const K0 = np00 / S;
      const K1 = np10 / S;
      // x = x' + K y
      x = xPred + K0 * y;
      v = vPred + K1 * y;
      // P = (I − K H) P'
      p00 = (1 - K0) * np00;
      p01 = (1 - K0) * np01;
      p10 = np10 - K1 * np00;
      p11 = np11 - K1 * np01;

      out.push(x);
    }
    return out;
  };

  const lats = smoothAxis('lat');
  const lngs = smoothAxis('lng');
  return pings.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 }
];

// Compute one "live position" per rider: midpoint between the last delivered drop and the next non-final drop.
// If nothing delivered yet, midpoint is kitchen pickup → first drop. If all delivered, returns null.
const computeRiderPosition = (r) => {
  const sorted = [...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);
  });

  const nextIdx = sorted.findIndex((o) => {
    const s = String(o.orderstatus || '').toLowerCase();
    return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
  });
  if (nextIdx === -1) return null;

  const next = sorted[nextIdx];
  if (!hasValidDrop(next)) return null;
  const nextLat = toNum(next.droplat || next.deliverylat);
  const nextLon = toNum(next.droplon || next.deliverylong);

  // Pick the previous reference point: previous order's drop if available, else this order's pickup (kitchen).
  let prevLat;
  let prevLon;
  const prev = nextIdx > 0 ? sorted[nextIdx - 1] : null;
  if (prev && hasValidDrop(prev)) {
    prevLat = toNum(prev.droplat || prev.deliverylat);
    prevLon = toNum(prev.droplon || prev.deliverylong);
  } else if (hasValidPickup(next)) {
    prevLat = toNum(pickupLat(next));
    prevLon = toNum(pickupLon(next));
  } else {
    return null;
  }

  const aerialLat = (prevLat + nextLat) / 2;
  const aerialLon = (prevLon + nextLon) / 2;

  const completedCount = sorted.filter((o) => FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())).length;

  return {
    id: r.id,
    color: r.color,
    riderName: r.riderName,
    aerialLat,
    aerialLon,
    prevLat,
    prevLon,
    nextLat,
    nextLon,
    completedCount,
    totalCount: sorted.length,
    nextStep: next.step || nextIdx + 1,
    nextCustomer: next.deliverycustomer || ''
  };
};

// Walk a polyline and return the [lat,lon] point at half the total length.
// Uses planar distance — fine at city scale and avoids a haversine import.
const polylineMidpoint = (points) => {
  if (!points || points.length < 2) return null;
  const segLens = [];
  let total = 0;
  for (let i = 0; i < points.length - 1; i++) {
    const dx = points[i + 1][0] - points[i][0];
    const dy = points[i + 1][1] - points[i][1];
    const d = Math.sqrt(dx * dx + dy * dy);
    segLens.push(d);
    total += d;
  }
  if (total === 0) return points[0];
  const target = total / 2;
  let acc = 0;
  for (let i = 0; i < segLens.length; i++) {
    if (acc + segLens[i] >= target) {
      const t = (target - acc) / segLens[i];
      return [
        points[i][0] + t * (points[i + 1][0] - points[i][0]),
        points[i][1] + t * (points[i + 1][1] - points[i][1])
      ];
    }
    acc += segLens[i];
  }
  return points[points.length - 1];
};

// 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;
};

// 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'];

// 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;
};

// 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>
);

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
}) => {
  // 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);
  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);
  // 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());
  // Short-lived close timer for the Compare drop-pin popup. Gives the
  // cursor a ~120ms window to travel from the marker onto the popup
  // without firing mouseout → closePopup mid-transit. Any new mouseover
  // cancels a pending close, so the popup glides instead of flickering.
  const compareHoverTimerRef = useRef(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 = delivery time
  // (operator's primary mental model — "did this order land in the X-Y wave?").
  // Switching to Assigned/Accepted/Arrived/Pickup/Started rebuckets every row
  // through `getRowBatch(_, selectedTimeField)`.
  const [selectedTimeField, setSelectedTimeField] = useState('delivery');
  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 (!Array.isArray(parsed) || parsed.length === 0) 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.
      return parsed.map((s, i) => ({
        id: s.id || `slot-${i + 1}`,
        startHour: Number(s.startHour) || 0,
        endHour: Number(s.endHour) || 24,
        label: formatSlotLabel(i, Number(s.startHour) || 0),
        range: formatSlotRange(Number(s.startHour) || 0, Number(s.endHour) || 24)
      }));
    } catch (e) {
      return BATCHES_DEFAULT;
    }
  });
  const BATCHES = slotsConfig;
  const [slotEditOpen, setSlotEditOpen] = useState(false);
  const slotEditRef = useRef(null);

  // 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 30s 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 ? 30_000 : false,
    refetchIntervalInBackground: false,
    staleTime: 15 * 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) => {
    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'));

  // 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 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. The synthetic
  // bike markers driven by riderPositions are route-progress estimates, not
  // real GPS, so they stay separate.
  const { data: ridersLocationLogs } = useQuery({
    queryKey: [selectedAppLocationId, selectedDate, ''],
    queryFn: fetchRidersLogs,
    refetchInterval: 30_000,
    refetchIntervalInBackground: false,
    staleTime: 15 * 1000,
    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]);
  // Default to the slot containing the current hour; if we're outside every slot
  // window (e.g. before 8 AM or in the 11–12 gap) fall back to the first slot.
  const [selectedBatch, setSelectedBatch] = useState(() => {
    return getBatchForHour(dayjs().hour(), 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
  });

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

  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);
    }
    return out;
  }, [livePagesData]);

  // 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]);

  // 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 = {};
    orders.forEach(o => {
      const key = o.rider_id || o.userid || 'unknown';
      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]
        };
      }
      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;

  // 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]
  );

  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'
      };
    }
    return {
      orders: stats.totalOrders,
      riders: stats.totalRiders,
      km: stats.totalKms,
      profit: stats.totalProfit,
      label: 'Total Fleet'
    };
  }, [focusedRider, focusedKitchen, stats]);

  // Live rider positions (Rapido-style bikes on the map)
  const riderPositions = useMemo(() => riders.map(computeRiderPosition).filter(Boolean), [riders]);

  // 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);
    } 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) => {
    const cacheKey = `${riderId}-${tripKey}`;
    // 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;
    if (points.length < 2) 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);
      });
    });
  }, [riders, activeRiders, focusedRider, fetchRoute]);

  // Fetch a road route for each rider's CURRENT segment (prev stop → next stop) so the
  // bike can sit on the polyline rather than the aerial midpoint. Cached under a `seg-<step>`
  // trip key so it doesn't collide with the full-trip route under `<tripNum>`.
  useEffect(() => {
    riderPositions.forEach((p) => {
      if (focusedKitchen) return;
      if (focusedRider && focusedRider.id !== p.id) return;
      if (!activeRiders.has(p.id)) return;
      const pts = [[p.prevLat, p.prevLon], [p.nextLat, p.nextLon]];
      fetchRoute(p.id, `seg-${p.nextStep}`, pts);
    });
  }, [riderPositions, focusedRider, focusedKitchen, activeRiders, 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.
  const prevHourRef = useRef(null);
  useEffect(() => {
    if (!shouldFetchLive) return;
    if (prevHourRef.current === null) prevHourRef.current = dayjs().hour();
    const tick = () => {
      const h = dayjs().hour();
      if (h === prevHourRef.current) return;
      const fromSlot = getBatchForHour(prevHourRef.current, BATCHES);
      prevHourRef.current = h;
      const toSlot = getBatchForHour(h, BATCHES);
      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),
  // also open that marker's popup so they see the order details without a second click.
  // Wait one frame so MapController has a chance to recenter first.
  useEffect(() => {
    if (!focusedStop) return;
    const t = setTimeout(() => {
      const marker = orderMarkerRefs.current[String(focusedStop.orderid)];
      if (marker && typeof marker.openPopup === 'function') marker.openPopup();
    }, 350);
    return () => clearTimeout(t);
  }, [focusedStop]);

  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;

        const cacheKey = `${r.id}-${tNum}`;
        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 => r.id === rid)?.color || '#475569';

  // Shared rider-card markup, used in the "By Rider" panel and inside the focused-zone detail.
  const renderRiderCard = (r, i) => (
    <div key={r.id} className="rcard" onClick={() => handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}>
      <div className="rcard-top">
        <div className="rcard-emo" style={{ background: `${r.color}18`, borderColor: `${r.color}50`, color: r.color }}><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" style={{ background: `${r.color}18`, color: r.color }}>{r.orders.length}</div>
      </div>
      <div className="bar-bg"><div className="bar-fg" style={{ width: `${Math.min(100, (r.orders.length / 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><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>
  );

  // 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.
  const renderOrderPopupContent = (o) => {
    const statusStyle = getStatusStyle(o.orderstatus);
    return (
      <>
        <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.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 && (
                <div className="pu-detail">
                  <div className="pu-detail-icon"><MdRestaurant /></div>
                  <div className="pu-detail-body">
                    <div className="pu-detail-label">Kitchen</div>
                    <div className="pu-detail-value" title={o.pickupcustomer}>{o.pickupcustomer}</div>
                  </div>
                </div>
              )}
              {(o.locationname || o.pickuplocation) && (
                <div className="pu-detail">
                  <div className="pu-detail-icon"><MdPlace /></div>
                  <div className="pu-detail-body">
                    <div className="pu-detail-label">Pickup</div>
                    <div className="pu-detail-value" title={o.locationname || o.pickuplocation}>{o.locationname || o.pickuplocation}</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.kms != null || o.actualkms != null || o.riderkms != null) && (
              <div className="pu-distance-row">
                {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">{o.kms} km</span>
                  </div>
                )}
                {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">{o.actualkms} km</span>
                  </div>
                )}
                {o.riderkms != null && o.riderkms !== '' && (
                  <div className="pu-distance-chip">
                    <span className="pu-distance-icon"><MdDirectionsBike /></span>
                    <span className="pu-distance-label">Rider</span>
                    <span className="pu-distance-value">{parseFloat(o.riderkms).toFixed(2)} km</span>
                  </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 (focusedRider) ordersToRender = focusedRider.orders;
    if (focusedKitchen) ordersToRender = focusedKitchen.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;
        }
      }

      // Use the 'step' field from data, fallback to index. In compare mode
      // override with sequenceStep so the marker number matches the timeline.
      const seq = compareSeq || o.step || (focusedRider || focusedKitchen ? (ordersToRender.indexOf(o) + 1) : 0);
      // Bumped from 22 → 32 so the step number reads at city-level zoom.
      const sz = 32;

      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 = L.divIcon({
        className: '',
        iconSize: [sz, sz],
        iconAnchor: [sz / 2, sz / 2],
        popupAnchor: [0, -28], // Lift popup above the flag, not just the larger 32px marker
        html: `<div class="cmark${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>`
      });

      return (
        <Marker
          key={o.orderid}
          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={{
            mouseover: (e) => e.target.openPopup(),
            mouseout: (e) => {
              if (!pinnedPopupsRef.current.has(String(o.orderid))) {
                e.target.closePopup();
              }
            },
            click: (e) => {
              const id = String(o.orderid);
              if (pinnedPopupsRef.current.has(id)) {
                pinnedPopupsRef.current.delete(id);
                e.target.closePopup();
              } else {
                pinnedPopupsRef.current.add(id);
                e.target.openPopup();
              }
            }
          }}
        >
          <Popup maxWidth={520} minWidth={460} className="dispatch-popup" autoPan={false}>
            {renderOrderPopupContent(o)}
          </Popup>
        </Marker>
      );
    });
  };

  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;
    riders.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;

      const rOrders = r.orders;
      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;

        const cacheKey = `${r.id}-${tNum}`;
        const roadPoints = osrmRoutes[cacheKey];
        const sorted = [...filteredTOrders].sort((a, b) => (a.step || 0) - (b.step || 0));

        // Cache values:
        //   Array  → OSRM road polyline (use it)
        //   false  → OSRM permanently failed (draw aerial fallback so user sees something)
        //   null   → request in-flight (DON'T draw anything yet — avoids the aerial flash)
        //   undefined → not yet requested (same as in-flight, wait)
        const hasRoad = Array.isArray(roadPoints) && roadPoints.length >= 2;
        const failed = roadPoints === false;
        if (!hasRoad && !failed) return; // still loading — don't show aerial flash

        const finalPoints = hasRoad ? roadPoints : 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]]);
              }
            }
          }
          // 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'
              }}
            />
          );
          segments.forEach((seg, i) => {
            if (!seg || seg.length < 2) return;
            const order = validDrops[i];
            const sequenceStep = order
              ? deliveryToStep.get(String(order.deliveryid))
              : null;
            const 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
                }}
              />
            );
          });
          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>

          {/* 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' : 'Profit'} (${activeStats.label})`}
                >
                  <span className="strat-stat-icon">{isLoss ? <MdTrendingDown /> : <MdTrendingUp />}</span>
                  <span className="strat-stat-label">{isLoss ? 'Loss' : 'Profit'}</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>
                )}
                <label className="live-date-label">
                  <span>Date</span>
                  <input
                    type="date"
                    value={selectedDate}
                    max={dayjs().format('YYYY-MM-DD')}
                    onChange={(e) => {
                      setSelectedDate(e.target.value);
                      handleRiderFocus(null);
                      setFocusedKitchen(null);
                      setFocusedZone(null);
                    }}
                  />
                </label>
              </>
            )}
          </div>

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

      <div id="strat-row">
        <button className={`sbt ${viewMode === 'kitchens' ? 'active' : ''}`} onClick={() => { setViewMode('kitchens'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPlace /></span> By Location</button>
        <button
          className={`sbt ${viewMode === 'zones' ? 'active' : ''}`}
          onClick={() => { setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}
        ><span className="sbt-icon"><MdMap /></span> By Zone</button>
        <button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button>
        <button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span> All Routes</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">Slot</span>
          {/* Dropdown to pick which timestamp drives slot bucketing. Mirrors
              the hub-location dropdown's look so it reads as the same kind of
              filter control. The chosen field reruns batchCounts +
              filteredLiveRows via selectedTimeField. */}
          <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 || 'Delivery'}</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 — lets the operator tweak start/end hours, add a new
              slot, remove an existing one, or reset to defaults. Persists via
              SLOTS_STORAGE_KEY in localStorage. */}
          <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). 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}
                          step={1}
                          value={s.startHour}
                          onChange={(e) => {
                            const v = Math.max(0, Math.min(23, parseInt(e.target.value, 10) || 0));
                            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={1}
                          max={24}
                          step={1}
                          value={s.endHour}
                          onChange={(e) => {
                            const v = Math.max(1, Math.min(24, parseInt(e.target.value, 10) || 1));
                            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 === '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: getRiderColor(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' : ''}`.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>
        <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 || 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 totalKm = focusedRider.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
                    const totalProfit = focusedRider.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0);
                    const isLoss = totalProfit < 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">{totalKm.toFixed(1)}<span className="rd-stat-unit">km</span></div>
                          <div className="rd-stat-label">Distance</div>
                        </div>
                        <div className={`rd-stat rd-stat-profit ${isLoss ? 'is-loss' : 'is-gain'}`}>
                          <div className="rd-stat-icon">{isLoss ? <MdTrendingDown /> : <MdTrendingUp />}</div>
                          <div className="rd-stat-value">
                            {isLoss ? '-' : ''}₹{Math.abs(totalProfit).toFixed(0)}
                          </div>
                          <div className="rd-stat-label">{isLoss ? 'Loss' : 'Profit'}</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;
                    let prevKitchenKey = null;
                    return Object.entries(trips)
                      .sort(([a], [b]) => Number(a) - Number(b))
                      .map(([tNum, tOrders]) => (
                      <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>
                        </div>
                        <div className="zone-order-grid">
                          {tOrders.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;
                            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' : ''}`}
                                  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">{o.step || idx + 1}</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);
                                      if (!o.orderstatus && !actual && !expected) 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>
                                          )}
                                        </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 || 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;
                          return (
                            <div
                              key={o.orderid}
                              className={`zone-order-card ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''}`}
                              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">{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>
                                {o.orderstatus && (
                                  <span
                                    className="zone-order-status"
                                    style={{ background: statusStyle.bg, color: statusStyle.fg }}
                                  >
                                    {statusStyle.label}
                                  </span>
                                )}
                              </div>

                              <div className="zone-order-customer">
                                <Ico><MdMarkunreadMailbox /></Ico>{o.deliverycustomer || '—'}
                              </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 || '-'}
                                </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;
                      return (
                        <div
                          key={o.orderid}
                          className={`zone-order-card ${canFocus ? 'clickable' : ''} ${isStopActive ? 'active' : ''}`}
                          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">
                              {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>
                            {o.orderstatus && (
                              <span
                                className="zone-order-status"
                                style={{ background: statusStyle.bg, color: statusStyle.fg }}
                              >
                                {statusStyle.label}
                              </span>
                            )}
                          </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 || '-'}
                            </span>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                )}
                
              </div>
            </div>
          ) : (
            <div id="riders-panel">
              <div className="ph">{
                viewMode === 'zones' ? 'Zone dispatch' :
                viewMode === 'kitchens' ? 'Kitchen 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>
                  ))
                ) : (
                  riders.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} />}
            <MapController focusedItem={compareFocusItem || ((focusedRider || focusedKitchen) && focusedStop) || focusedRider || focusedKitchen || focusedZone} viewMode={viewMode} orders={allOrders} kitchens={kitchens} locationKey={selectedAppLocationId} />
            {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={false}>
                    <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()}
            {!focusedKitchen && riderPositions
              .filter(p => activeRiders.has(p.id))
              .filter(p => !focusedRider || focusedRider.id === p.id)
              .map(p => {
                // Prefer the road polyline midpoint; fall back to aerial midpoint until OSRM responds.
                const segPolyline = osrmRoutes[`${p.id}-seg-${p.nextStep}`];
                const roadMid = polylineMidpoint(segPolyline);
                const pos = roadMid || [p.aerialLat, p.aerialLon];
                const onRoad = Boolean(roadMid);
                const bikeIcon = L.divIcon({
                  className: '',
                  iconSize: [44, 44],
                  iconAnchor: [22, 22],
                  popupAnchor: [0, -22],
                  html: `<div class="rider-bike${onRoad ? ' on-road' : ''}" style="--rider-color:${p.color}">
                           <div class="rider-bike-ring"></div>
                           <div class="rider-bike-svg">${MOTORBIKE_SVG}</div>
                           <div class="rider-bike-progress">${p.completedCount}/${p.totalCount}</div>
                         </div>`
                });
                return (
                  <Marker
                    key={`bike-${p.id}`}
                    position={pos}
                    icon={bikeIcon}
                    zIndexOffset={3000}
                    eventHandlers={{
                      click: () => handleRiderFocus(riders.find((r) => r.id === p.id) || null),
                      mouseover: (e) => e.target.openPopup(),
                      mouseout: (e) => e.target.closePopup()
                    }}
                  >
                    <Popup maxWidth={220} autoPan={false}>
                      <div className="pu-id">RIDER</div>
                      <div className="pu-rider" style={{ color: p.color }}>{p.riderName}</div>
                      <div className="pu-row"><span>Progress</span><span>{p.completedCount} / {p.totalCount} delivered</span></div>
                      <div className="pu-row"><span>Next stop</span><span>#{p.nextStep} · {p.nextCustomer || '—'}</span></div>
                      <div className="pu-row"><span>Position</span><span>{onRoad ? 'on road' : 'estimating…'}</span></div>
                    </Popup>
                  </Marker>
                );
              })}

            {/* 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) => 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';
                const pinColor = isActive ? '#16a34a' : '#dc2626';
                const liveIcon = 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">${(r.username || '').replace(/[<>&"']/g, '')}${r.orderid ? ` <span>#${String(r.orderid).replace(/[<>&"']/g, '')}</span>` : ''}</div>
                         </div>`
                });
                return (
                  <Marker
                    key={`live-${r.id}`}
                    position={[r.lat, r.lon]}
                    icon={liveIcon}
                    zIndexOffset={2500}
                    eventHandlers={{
                      click: () => {
                        const match = riders.find((rd) => String(rd.id) === String(r.id));
                        if (match) handleRiderFocus(match);
                      },
                      mouseover: (e) => e.target.openPopup(),
                      mouseout: (e) => e.target.closePopup()
                    }}
                  >
                    <Popup maxWidth={220} autoPan={false}>
                      <div className="pu-id">LIVE GPS</div>
                      <div className="pu-rider" style={{ color: pinColor }}>{r.username}</div>
                      <div className="pu-row"><span>Status</span><span>{r.status || 'unknown'}</span></div>
                      {r.orderid && <div className="pu-row"><span>Order</span><span>#{r.orderid}</span></div>}
                      {r.contactno && <div className="pu-row"><span>Phone</span><span>{r.contactno}</span></div>}
                      {r.logdate && <div className="pu-row"><span>Last seen</span><span>{r.logdate}</span></div>}
                      <div className="pu-row"><span>Position</span><span>{r.lat.toFixed(5)}, {r.lon.toFixed(5)}</span></div>
                    </Popup>
                  </Marker>
                );
              })}

            {/* 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;
              const color = stepColor(i);
              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 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();
                }
              };

              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'
                      }}
                    />
                  )}
                  {drawPolyline && (
                    <Polyline
                      positions={positions}
                      pathOptions={{
                        color,
                        weight: isFocusedStep ? 6.5 : 5,
                        opacity: isFocusedStep ? 1 : focusedCompareStep ? 0.55 : 0.95,
                        lineJoin: 'round',
                        lineCap: 'round'
                      }}
                    />
                  )}

                  {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
                        ? {
                            // Match the planned-route marker UX: hover opens
                            // the rich order popup, leaving it pinned while
                            // a step is focused (so click-to-focus keeps the
                            // modal visible after the cursor moves away). The
                            // ~120ms grace timer on mouseout lets the cursor
                            // travel onto the popup itself without flicker.
                            mouseover: (e) => {
                              if (compareHoverTimerRef.current) {
                                clearTimeout(compareHoverTimerRef.current);
                                compareHoverTimerRef.current = null;
                              }
                              e.target.openPopup();
                            },
                            mouseout: (e) => {
                              if (focusedCompareStep === t.sequenceStep) return;
                              const marker = e.target;
                              if (compareHoverTimerRef.current) {
                                clearTimeout(compareHoverTimerRef.current);
                              }
                              compareHoverTimerRef.current = setTimeout(() => {
                                marker.closePopup();
                                compareHoverTimerRef.current = null;
                              }, 120);
                            },
                            click: handleEndMarkerClick
                          }
                        : { 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>
                    )}
                    {orderForTrack && (
                      <Popup
                        maxWidth={520}
                        minWidth={460}
                        className="dispatch-popup"
                        autoPan={false}
                      >
                        {renderOrderPopupContent(orderForTrack)}
                      </Popup>
                    )}
                  </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>

          <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, #6366f1, #3b82f6)'
                  : '#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.anomaly ? ' · deviation flagged' : '')
                                  }
                                >
                                  <span className="compare-step-circle">
                                    {isLoading ? <span className="compare-step-spin" /> : plannedStepNum}
                                  </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. */}
                  {(() => {
                    // Both planned and actual share the same per-step palette
                    // — they're separated by stroke style on the unified map
                    // (planned = dashed, actual = solid). The swatch shows
                    // the focused step's color when one is focused, or a
                    // multi-hue gradient ("varies by step") when in overall.
                    const swatchBg = focusedDelta
                      ? stepColor(focusedDelta.sequenceStep - 1)
                      : `linear-gradient(90deg, ${STEP_PALETTE.slice(0, 6).join(', ')})`;
                    return (
                      <div className="compare-legend">
                        <span className="compare-legend-item">
                          <span
                            className="compare-legend-swatch is-step-color is-dashed"
                            style={{ background: swatchBg }}
                          />
                          Planned (dashed)
                        </span>
                        <span className="compare-legend-item">
                          <span
                            className="compare-legend-swatch is-step-color"
                            style={{ background: swatchBg }}
                          />
                          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>
      )}

    </div>
  );
};

export default Dispatch;
