updates on the active page and the profitability section
This commit is contained in:
@@ -779,6 +779,125 @@ const MapController = ({ focusedItem, viewMode, orders, kitchens, locationKey })
|
||||
return null;
|
||||
};
|
||||
|
||||
// Smoothly-moving rider marker — the Swiggy/Zomato/Rapido style "bike gliding
|
||||
// down the road" effect. Instead of letting react-leaflet snap the marker to
|
||||
// each new GPS fix, we keep the <Marker>'s `position` prop frozen at its mount
|
||||
// coordinate (a stable ref, so react-leaflet never repositions it) and drive
|
||||
// every subsequent move imperatively with marker.setLatLng() inside a
|
||||
// requestAnimationFrame loop. Each time `target` changes we ease from the
|
||||
// marker's current on-screen latlng to the new fix, so the rider visibly
|
||||
// travels between points rather than teleporting. A large jump (GPS glitch /
|
||||
// first real fix after a placeholder) snaps instead of crawling across the map.
|
||||
//
|
||||
// ADAPTIVE GLIDE — the critical bit for "live" feel:
|
||||
// We poll the GPS feed every 1s, but the backend only emits a fresh coordinate
|
||||
// every ~30s, so the same fix repeats for ~30 polls and then jumps. With a
|
||||
// fixed glide we'd animate for ~1s and then sit frozen for ~29s — the bike
|
||||
// would look like it teleports every 30s. Instead we MEASURE the real wall-time
|
||||
// between distinct fixes and stretch the glide across that whole interval
|
||||
// (clamped). So when fixes are 30s apart the bike eases continuously for the
|
||||
// full 30s; if the backend ever speeds up to 1s the glide tightens to 1s
|
||||
// automatically. Either way motion is smooth and never freezes mid-trip.
|
||||
// `duration` is only the seed used for the very first segment (no prior fix to
|
||||
// measure against yet).
|
||||
const MIN_GLIDE_MS = 800; // floor: don't animate faster than this even on rapid fixes
|
||||
const MAX_GLIDE_MS = 32_000; // ceiling: cover the ~30s backend cadence + small buffer
|
||||
const AnimatedRiderMarker = ({ target, icon, duration = 950, zIndexOffset, eventHandlers, children, markerRef: externalRef }) => {
|
||||
const markerRef = useRef(null);
|
||||
const rafRef = useRef(null);
|
||||
// Frozen mount position — never handed back to react-leaflet again, so it
|
||||
// can't fight the imperative animation below.
|
||||
const mountPosRef = useRef(target);
|
||||
// performance.now() timestamp of the last DISTINCT fix we glided to. Lets us
|
||||
// measure the true inter-fix interval and size the next glide to match it.
|
||||
const prevFixTsRef = useRef(null);
|
||||
|
||||
// CRITICAL: depend on the primitive lat/lon, NOT the `target` array. The
|
||||
// parent re-renders every second (clock tick + 1s GPS poll) and hands us a
|
||||
// brand-new `[lat, lon]` array each time. If the effect keyed off that array
|
||||
// it would cancel + restart the glide on every render — the ease keeps
|
||||
// resetting to zero velocity and the bike visibly stutters/lags. Keying off
|
||||
// the numbers means the glide only (re)starts when the rider's coordinate
|
||||
// genuinely changes, so each 1s segment plays out uninterrupted.
|
||||
const lat = Array.isArray(target) ? Number(target[0]) : NaN;
|
||||
const lon = Array.isArray(target) ? Number(target[1]) : NaN;
|
||||
useEffect(() => {
|
||||
const marker = markerRef.current;
|
||||
if (!marker || !Number.isFinite(lat) || !Number.isFinite(lon)) return undefined;
|
||||
|
||||
const to = L.latLng(lat, lon);
|
||||
const from = marker.getLatLng();
|
||||
if (!from) {
|
||||
marker.setLatLng(to);
|
||||
return undefined;
|
||||
}
|
||||
const dLat = to.lat - from.lat;
|
||||
const dLng = to.lng - from.lng;
|
||||
// No meaningful move (~<0.1m) — snap and skip the rAF.
|
||||
if (Math.abs(dLat) < 1e-6 && Math.abs(dLng) < 1e-6) {
|
||||
marker.setLatLng(to);
|
||||
return undefined;
|
||||
}
|
||||
// Teleport on big jumps (>2km) so a bad fix doesn't drag the icon across town.
|
||||
let bigJump = false;
|
||||
try {
|
||||
bigJump = from.distanceTo(to) > 2000;
|
||||
} catch {
|
||||
bigJump = false;
|
||||
}
|
||||
if (bigJump) {
|
||||
marker.setLatLng(to);
|
||||
prevFixTsRef.current = performance.now();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
const startTs = performance.now();
|
||||
// Size this glide to the real gap since the previous fix so the bike keeps
|
||||
// moving for the whole interval instead of darting then freezing. First
|
||||
// segment has no prior timestamp, so fall back to the `duration` seed.
|
||||
const gap = prevFixTsRef.current == null ? duration : startTs - prevFixTsRef.current;
|
||||
const segMs = Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, gap));
|
||||
prevFixTsRef.current = startTs;
|
||||
const startLat = from.lat;
|
||||
const startLng = from.lng;
|
||||
const step = (now) => {
|
||||
const t = Math.min(1, (now - startTs) / segMs);
|
||||
// Linear interpolation → constant speed. Spanning the glide across the
|
||||
// full inter-fix interval chains consecutive segments into one continuous
|
||||
// motion (a vehicle moving down the road) rather than the accelerate/brake
|
||||
// feel an easing curve gives, or the dart-then-freeze of a fixed duration.
|
||||
marker.setLatLng([startLat + dLat * t, startLng + dLng * t]);
|
||||
if (t < 1) rafRef.current = requestAnimationFrame(step);
|
||||
};
|
||||
rafRef.current = requestAnimationFrame(step);
|
||||
return () => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [lat, lon, duration]);
|
||||
|
||||
// Cleanup any in-flight animation on unmount.
|
||||
useEffect(() => () => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Marker
|
||||
ref={(inst) => {
|
||||
markerRef.current = inst;
|
||||
if (typeof externalRef === 'function') externalRef(inst);
|
||||
else if (externalRef) externalRef.current = inst;
|
||||
}}
|
||||
position={mountPosRef.current}
|
||||
icon={icon}
|
||||
zIndexOffset={zIndexOffset}
|
||||
eventHandlers={eventHandlers}
|
||||
>
|
||||
{children}
|
||||
</Marker>
|
||||
);
|
||||
};
|
||||
|
||||
// Inline-icon wrapper used wherever a Material icon precedes some text — keeps the
|
||||
// SVG vertically centered with the adjacent text and inherits the parent color.
|
||||
const Ico = ({ children }) => (
|
||||
@@ -972,6 +1091,8 @@ const Dispatch = ({
|
||||
// popups use leaflet's marker-attached <Popup> (openPopup/closePopup) rather
|
||||
// than the centered overlay used for order popups.
|
||||
const pinnedLivePopupsRef = useRef(new Set());
|
||||
// Cache for live GPS rider Leaflet divIcon instances.
|
||||
const liveIconCacheRef = useRef(new Map());
|
||||
// Short-lived close timer for the general map order/marker popups.
|
||||
// Gives the cursor a ~200ms window to travel from the marker onto the popup
|
||||
// or vice versa without immediately triggering a close.
|
||||
@@ -1334,7 +1455,7 @@ const Dispatch = ({
|
||||
// hub (latitude/longitude/logdate/status). We render those positions as
|
||||
// markers on the main dispatch map so the operator sees where each rider
|
||||
// actually is — matching the Reports → Riders Logs page.
|
||||
const RIDER_LOG_POLL_MS = viewMode === 'all' ? 1_000 : 15_000;
|
||||
const RIDER_LOG_POLL_MS = 15000;
|
||||
const { data: ridersLocationLogs } = useQuery({
|
||||
queryKey: [selectedAppLocationId, selectedDate, ''],
|
||||
queryFn: fetchRidersLogs,
|
||||
@@ -1484,24 +1605,24 @@ const Dispatch = ({
|
||||
const totalDailyProfit = useMemo(() => {
|
||||
let profit = 0;
|
||||
const slotRiders = {};
|
||||
|
||||
|
||||
liveRows.forEach(r => {
|
||||
const batch = getRowBatch(r, selectedTimeField, BATCHES);
|
||||
if (!batch || batch === 'all') return;
|
||||
|
||||
|
||||
const riderKey = String(r.userid || r.rider_id || 'unassigned');
|
||||
if (riderKey === 'unassigned' || riderKey === '0') return;
|
||||
|
||||
|
||||
if (!slotRiders[batch]) slotRiders[batch] = {};
|
||||
if (!slotRiders[batch][riderKey]) {
|
||||
slotRiders[batch][riderKey] = { revenue: 0, kms: 0 };
|
||||
}
|
||||
|
||||
|
||||
const kms = parseFloat(r.kms || r.actualkms || 0);
|
||||
slotRiders[batch][riderKey].kms += kms;
|
||||
slotRiders[batch][riderKey].revenue += (kms <= 8 ? 30 : 30 + (kms - 8) * 6);
|
||||
});
|
||||
|
||||
|
||||
Object.values(slotRiders).forEach(riderMap => {
|
||||
Object.values(riderMap).forEach(stats => {
|
||||
const variableCost = stats.kms * 2.5;
|
||||
@@ -1510,7 +1631,7 @@ const Dispatch = ({
|
||||
profit += (stats.revenue - totalCost);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return profit;
|
||||
}, [liveRows, selectedTimeField, BATCHES]);
|
||||
|
||||
@@ -1636,8 +1757,27 @@ const Dispatch = ({
|
||||
});
|
||||
|
||||
const riderMap = {};
|
||||
|
||||
// First, populate riderMap with all riders present in the zones to capture those with 0 orders
|
||||
(source.zones || []).forEach(z => {
|
||||
(z.riders || []).forEach(r => {
|
||||
const key = r.rider_id;
|
||||
if (!key || key === 'unassigned') return;
|
||||
if (!riderMap[key]) {
|
||||
riderMap[key] = {
|
||||
id: key,
|
||||
riderName: r.rider_name || r.username || key,
|
||||
orders: [],
|
||||
color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length]
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Next, map the flat orders list to the riders
|
||||
orders.forEach(o => {
|
||||
const key = o.rider_id || o.userid || 'unknown';
|
||||
if (key === 'unassigned') return;
|
||||
if (!riderMap[key]) {
|
||||
riderMap[key] = {
|
||||
id: key,
|
||||
@@ -1646,7 +1786,9 @@ const Dispatch = ({
|
||||
color: RIDER_COLORS[Object.keys(riderMap).length % RIDER_COLORS.length]
|
||||
};
|
||||
}
|
||||
riderMap[key].orders.push(o);
|
||||
if (!riderMap[key].orders.some(existing => existing.orderid === o.orderid)) {
|
||||
riderMap[key].orders.push(o);
|
||||
}
|
||||
});
|
||||
|
||||
const kitchenMap = {};
|
||||
@@ -1846,7 +1988,7 @@ const Dispatch = ({
|
||||
// is already gated to active-order riders) so the tiles can't disagree with
|
||||
// the list/map below.
|
||||
if (isAllActiveView) {
|
||||
const activeOrders = allViewOrders.filter(isActiveDelivery);
|
||||
const activeOrders = allOrders.filter(isActiveDelivery);
|
||||
return {
|
||||
orders: activeOrders.length,
|
||||
riders: visibleRiders.length,
|
||||
@@ -2299,8 +2441,30 @@ const Dispatch = ({
|
||||
const pts = buildTripPoints(sorted);
|
||||
if (pts.length >= 2) fetchRoute(r.id, tNum, pts);
|
||||
});
|
||||
|
||||
// Active view: fetch a road-following route scoped to the rider's single
|
||||
// in-progress leg — current live GPS position → drop (pickup as fallback
|
||||
// when no live fix). Cached under a per-order key so renderRoutes() can
|
||||
// draw the road polyline for that leg instead of a straight line, and so
|
||||
// it never collides with the per-trip route fetched above.
|
||||
if (isAllActiveView) {
|
||||
const activeOrder = getActiveOrder(r.orders);
|
||||
if (activeOrder) {
|
||||
const lp = liveRiderLocations.find((l) => String(l.id) === String(r.id));
|
||||
const start = (lp && Number.isFinite(lp.lat) && Number.isFinite(lp.lon))
|
||||
? [lp.lat, lp.lon]
|
||||
: (hasValidPickup(activeOrder)
|
||||
? [toNum(pickupLat(activeOrder)), toNum(pickupLon(activeOrder))]
|
||||
: null);
|
||||
const dLat = toNum(activeOrder.droplat || activeOrder.deliverylat);
|
||||
const dLon = toNum(activeOrder.droplon || activeOrder.deliverylong);
|
||||
if (start && Number.isFinite(dLat) && Number.isFinite(dLon)) {
|
||||
fetchRoute(r.id, `active-${activeOrder.orderid}`, [start, [dLat, dLon]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [riders, activeRiders, focusedRider, fetchRoute]);
|
||||
}, [riders, activeRiders, focusedRider, isAllActiveView, liveRiderLocations, fetchRoute]);
|
||||
|
||||
// Auto-advance the selected slot when the wall-clock moves into a new slot's
|
||||
// window — BUT only if the user is still sitting on the slot that's just been
|
||||
@@ -2353,6 +2517,8 @@ const Dispatch = ({
|
||||
// (renderOrderPopupContent needs the rich timeline + status fields).
|
||||
// Wait ~350ms so MapController has a chance to recenter first (matches
|
||||
// the prior delay before openPopup was called).
|
||||
// COMMENTED OUT: as per user request to only show the popup when clicking the map number plots/markers.
|
||||
/*
|
||||
useEffect(() => {
|
||||
if (!focusedStop) return;
|
||||
const t = setTimeout(() => {
|
||||
@@ -2361,6 +2527,7 @@ const Dispatch = ({
|
||||
}, 350);
|
||||
return () => clearTimeout(t);
|
||||
}, [focusedStop, allOrders]);
|
||||
*/
|
||||
|
||||
const startAnimation = () => {
|
||||
if (isAnimating) {
|
||||
@@ -2902,13 +3069,20 @@ const Dispatch = ({
|
||||
const routes = [];
|
||||
const zoneRiderIds = focusedZone ? new Set(focusedZone.riders.map((zr) => String(zr.rider_id))) : null;
|
||||
if (hidePlanned) return routes;
|
||||
riders.forEach(r => {
|
||||
// visibleRiders === riders in every view except "All Active Routes", where
|
||||
// it's pre-filtered to riders whose live GPS is currently active.
|
||||
visibleRiders.forEach(r => {
|
||||
const isActive = activeRiders.has(r.id);
|
||||
if (focusedRider && focusedRider.id !== r.id) return;
|
||||
if (focusedKitchen && !focusedKitchen.riders.has(r.id)) return;
|
||||
if (zoneRiderIds && !zoneRiderIds.has(String(r.id))) return;
|
||||
|
||||
const rOrders = r.orders;
|
||||
// In the Active view, collapse the rider down to just their in-progress
|
||||
// delivery so the map draws only that one leg's polyline instead of the
|
||||
// whole day's route. Every other view keeps the full order list.
|
||||
const activeOrder = isAllActiveView ? getActiveOrder(r.orders) : null;
|
||||
const rOrders = isAllActiveView ? (activeOrder ? [activeOrder] : []) : r.orders;
|
||||
if (isAllActiveView && rOrders.length === 0) return;
|
||||
const trips = {};
|
||||
rOrders.forEach(o => {
|
||||
const t = o.trip_number || 1;
|
||||
@@ -2933,16 +3107,46 @@ const Dispatch = ({
|
||||
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
|
||||
// Active view: build a route scoped to just the in-progress leg and FOLLOW THE ROADS.
|
||||
//
|
||||
// Leg endpoints: the rider's CURRENT live GPS position → the drop. The
|
||||
// live position is preferred because (a) active riders always have a
|
||||
// live GPS fix in this view, so the leg always has 2 valid points even
|
||||
// when the order carries no pickup coordinates — the previous cause of a
|
||||
// completely missing line — and (b) it shows the rider's REMAINING route
|
||||
// to the customer. Falls back to the pickup when there's no live fix.
|
||||
const activeLegOrder = isAllActiveView ? sorted[0] : null;
|
||||
let activeStraightLeg = null;
|
||||
if (isAllActiveView && activeLegOrder) {
|
||||
const lp = liveRiderLocations.find((l) => String(l.id) === String(r.id));
|
||||
const start = (lp && Number.isFinite(lp.lat) && Number.isFinite(lp.lon))
|
||||
? [lp.lat, lp.lon]
|
||||
: (hasValidPickup(activeLegOrder)
|
||||
? [toNum(pickupLat(activeLegOrder)), toNum(pickupLon(activeLegOrder))]
|
||||
: null);
|
||||
const dLat = toNum(activeLegOrder.droplat || activeLegOrder.deliverylat);
|
||||
const dLon = toNum(activeLegOrder.droplon || activeLegOrder.deliverylong);
|
||||
if (start && Number.isFinite(dLat) && Number.isFinite(dLon)) {
|
||||
activeStraightLeg = [start, [dLat, dLon]];
|
||||
}
|
||||
}
|
||||
// Prefer the OSRM road-following route for the active leg (fetched as
|
||||
// `${r.id}-active-${orderid}` in the route-fetch effect); other views
|
||||
// keep the per-trip route.
|
||||
const roadToUse = isAllActiveView
|
||||
? (activeLegOrder && activeStraightLeg ? osrmRoutes[getTripCacheKey(r.id, `active-${activeLegOrder.orderid}`, activeStraightLeg)] : undefined)
|
||||
: roadPoints;
|
||||
const hasRoad = Array.isArray(roadToUse) && roadToUse.length >= 2;
|
||||
const failed = roadToUse === false;
|
||||
// Other views: wait for OSRM so we don't flash an aerial line before the
|
||||
// road polyline lands. Active view: NEVER wait — draw the straight leg
|
||||
// immediately and let it upgrade to the road-following route, so the
|
||||
// line is never invisible while OSRM is in flight.
|
||||
if (!isAllActiveView && !hasRoad && !failed) return;
|
||||
|
||||
const finalPoints = hasRoad ? roadPoints : buildTripPoints(sorted);
|
||||
const finalPoints = hasRoad
|
||||
? roadToUse
|
||||
: (isAllActiveView ? activeStraightLeg : buildTripPoints(sorted));
|
||||
if (!finalPoints || finalPoints.length < 2) return;
|
||||
|
||||
const isKitchenView = (viewMode === 'kitchens' || focusedKitchen);
|
||||
@@ -4720,7 +4924,11 @@ const Dispatch = ({
|
||||
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) =>
|
||||
isAllActiveView
|
||||
? (r.status === 'active' && activeOrderRiderIdSet.has(String(r.id)))
|
||||
: riders.some((rd) => String(rd.id) === String(r.id))
|
||||
)
|
||||
.filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))
|
||||
.map((r) => {
|
||||
const isActive = r.status === 'active';
|
||||
@@ -4744,39 +4952,77 @@ const Dispatch = ({
|
||||
const nextDropArea = nextOrder
|
||||
? (nextOrder.deliverysuburb || extractArea(nextOrder.deliveryaddress))
|
||||
: null;
|
||||
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>
|
||||
// Marker icon. ONLY the "All Active Routes" view gets the
|
||||
// Swiggy/Zomato/Rapido-style live bike badge (rounded glyph
|
||||
// + pulsing ring) that smoothly glides between GPS fixes.
|
||||
// Every other view keeps the original teardrop pin exactly
|
||||
// as before. Icons are cached per rider (liveIconCacheRef);
|
||||
// the sig includes the view so switching modes rebuilds it.
|
||||
const safeName = (r.username || '').replace(/[<>&"']/g, '');
|
||||
const safeOrder = r.orderid ? String(r.orderid).replace(/[<>&"']/g, '') : '';
|
||||
const iconSig = `${isAllActiveView ? 'bike' : 'pin'}|${pinColor}|${safeName}|${safeOrder}|${isActive ? 1 : 0}`;
|
||||
let iconEntry = liveIconCacheRef.current.get(r.id);
|
||||
if (!iconEntry || iconEntry.sig !== iconSig) {
|
||||
const icon = isAllActiveView
|
||||
? L.divIcon({
|
||||
className: '',
|
||||
iconSize: [160, 44],
|
||||
iconAnchor: [22, 22],
|
||||
popupAnchor: [0, -22],
|
||||
html: `<div class="live-rider-bike ${isActive ? 'is-active' : 'is-idle'}" style="--pin-color:${pinColor}">
|
||||
<span class="live-rider-bike-pulse"></span>
|
||||
<span class="live-rider-bike-badge">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#fff" aria-hidden="true"><path d="M19.44 9.03 15.41 5H11v2h3.59l2 2H5c-2.8 0-5 2.2-5 5s2.2 5 5 5c2.46 0 4.45-1.69 4.9-4h1.65l2.77-2.77c-.21.54-.32 1.14-.32 1.77 0 2.76 2.24 5 5 5s5-2.24 5-5c0-2.65-2.06-4.77-4.66-4.97ZM7.82 15C7.4 16.15 6.28 17 5 17c-1.63 0-3-1.37-3-3s1.37-3 3-3c1.28 0 2.4.85 2.82 2H5v2h2.82ZM19 17c-1.63 0-3-1.37-3-3s1.37-3 3-3 3 1.37 3 3-1.37 3-3 3Z"/></svg>
|
||||
</span>
|
||||
<span class="live-rider-bike-label">${safeName}${safeOrder ? ` <span>#${safeOrder}</span>` : ''}</span>
|
||||
</div>`
|
||||
});
|
||||
})
|
||||
: L.divIcon({
|
||||
className: '',
|
||||
iconSize: [140, 56],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [58, -40],
|
||||
html: `<div class="live-rider-pin" style="--pin-color:${pinColor}">
|
||||
<div class="live-rider-pin-marker"></div>
|
||||
<div class="live-rider-pin-label">${safeName}${safeOrder ? ` <span>#${safeOrder}</span>` : ''}</div>
|
||||
</div>`
|
||||
});
|
||||
iconEntry = { sig: iconSig, icon };
|
||||
liveIconCacheRef.current.set(r.id, iconEntry);
|
||||
}
|
||||
const liveIcon = iconEntry.icon;
|
||||
// Shared interaction handlers — identical for both marker types.
|
||||
const liveEventHandlers = {
|
||||
click: (e) => {
|
||||
const idStr = String(r.id);
|
||||
if (pinnedLivePopupsRef.current.has(idStr)) {
|
||||
pinnedLivePopupsRef.current.delete(idStr);
|
||||
e.target.closePopup();
|
||||
} else {
|
||||
pinnedLivePopupsRef.current.add(idStr);
|
||||
e.target.openPopup();
|
||||
}
|
||||
const match = riders.find((rd) => String(rd.id) === idStr);
|
||||
if (match) handleRiderFocus(match);
|
||||
},
|
||||
popupclose: () => {
|
||||
pinnedLivePopupsRef.current.delete(String(r.id));
|
||||
}
|
||||
};
|
||||
// Animated bike (with imperative gliding) only in the active
|
||||
// view; the plain react-leaflet Marker everywhere else so
|
||||
// existing views behave exactly as they did before.
|
||||
const LiveMarker = isAllActiveView ? AnimatedRiderMarker : Marker;
|
||||
const positionProps = isAllActiveView
|
||||
? { target: [r.lat, r.lon], duration: 15000 }
|
||||
: { position: [r.lat, r.lon] };
|
||||
return (
|
||||
<Marker
|
||||
<LiveMarker
|
||||
key={`live-${r.id}`}
|
||||
position={[r.lat, r.lon]}
|
||||
{...positionProps}
|
||||
icon={liveIcon}
|
||||
zIndexOffset={2500}
|
||||
eventHandlers={{
|
||||
click: (e) => {
|
||||
const idStr = String(r.id);
|
||||
if (pinnedLivePopupsRef.current.has(idStr)) {
|
||||
pinnedLivePopupsRef.current.delete(idStr);
|
||||
e.target.closePopup();
|
||||
} else {
|
||||
pinnedLivePopupsRef.current.add(idStr);
|
||||
e.target.openPopup();
|
||||
}
|
||||
const match = riders.find((rd) => String(rd.id) === idStr);
|
||||
if (match) handleRiderFocus(match);
|
||||
},
|
||||
popupclose: () => {
|
||||
pinnedLivePopupsRef.current.delete(String(r.id));
|
||||
}
|
||||
}}
|
||||
eventHandlers={liveEventHandlers}
|
||||
>
|
||||
<Popup maxWidth={260} autoPan={true} autoPanPadding={[20, 20]} className="dispatch-popup live-rider-popup">
|
||||
<div className="pu-hdr-live">
|
||||
@@ -4862,7 +5108,7 @@ const Dispatch = ({
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</Marker>
|
||||
</LiveMarker>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user