diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css
index 44ad8ee..f483884 100644
--- a/src/pages/nearle/dispatch/Dispatch.css
+++ b/src/pages/nearle/dispatch/Dispatch.css
@@ -10986,4 +10986,87 @@
.dispatch-container #dispatch-analysis {
overflow-y: visible;
}
+}
+
+/* ── Live Rider Bike Marker (Swiggy/Zomato style) ── */
+.dispatch-container .live-rider-bike {
+ --pin-color: #16a34a;
+ position: relative;
+ width: 160px;
+ height: 44px;
+ pointer-events: none;
+}
+
+.dispatch-container .live-rider-bike-badge {
+ position: absolute;
+ left: 4px;
+ top: 4px;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--pin-color);
+ border: 3px solid #fff;
+ border-radius: 50%;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
+ pointer-events: auto;
+ z-index: 2;
+}
+
+.dispatch-container .live-rider-bike-pulse {
+ position: absolute;
+ left: 4px;
+ top: 4px;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: var(--pin-color);
+ opacity: 0.45;
+ z-index: 1;
+}
+
+.dispatch-container .live-rider-bike.is-active .live-rider-bike-pulse {
+ animation: live-rider-bike-pulse 1.6s ease-out infinite;
+}
+
+.dispatch-container .live-rider-bike.is-idle .live-rider-bike-badge {
+ opacity: 0.85;
+}
+
+.dispatch-container .live-rider-bike-label {
+ position: absolute;
+ left: 46px;
+ top: 9px;
+ background: var(--pin-color);
+ color: #fff;
+ font-size: 11px;
+ font-weight: 700;
+ padding: 3px 8px;
+ border-radius: 4px;
+ white-space: nowrap;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
+ line-height: 1.2;
+ pointer-events: auto;
+}
+
+.dispatch-container .live-rider-bike-label span {
+ font-weight: 500;
+ opacity: 0.85;
+ margin-left: 4px;
+}
+
+@keyframes live-rider-bike-pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 0.5;
+ }
+ 70% {
+ transform: scale(2.2);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(2.2);
+ opacity: 0;
+ }
}
\ No newline at end of file
diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js
index 913583e..120ea67 100644
--- a/src/pages/nearle/dispatch/Dispatch.js
+++ b/src/pages/nearle/dispatch/Dispatch.js
@@ -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 '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 (
+ {
+ 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}
+
+ );
+};
+
// 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 (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: `
-
-
${(r.username || '').replace(/[<>&"']/g, '')}${r.orderid ? ` #${String(r.orderid).replace(/[<>&"']/g, '')}` : ''}
+ // 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: `
+
+
+
+
+
${safeName}${safeOrder ? ` #${safeOrder}` : ''}
`
- });
+ })
+ : L.divIcon({
+ className: '',
+ iconSize: [140, 56],
+ iconAnchor: [12, 41],
+ popupAnchor: [58, -40],
+ html: `
+
+
${safeName}${safeOrder ? ` #${safeOrder}` : ''}
+
`
+ });
+ 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 (
-
{
- 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}
>
@@ -4862,7 +5108,7 @@ const Dispatch = ({
-
+
);
})}
diff --git a/src/pages/nearle/dispatch/ProfitabilitySection.css b/src/pages/nearle/dispatch/ProfitabilitySection.css
index 201047f..4bdfd39 100644
--- a/src/pages/nearle/dispatch/ProfitabilitySection.css
+++ b/src/pages/nearle/dispatch/ProfitabilitySection.css
@@ -100,7 +100,8 @@ body {
line-height: 1.5;
color: var(--text-primary);
- min-height: 100vh;
+ min-height: 0;
+ flex: 1;
}
/* Scrollbar styling */
@@ -134,9 +135,9 @@ body {
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
- gap: var(--space-2xl);
+ gap: var(--space-md);
- padding: var(--space-2xl);
+ padding: 10px 16px;
background: linear-gradient(135deg, rgba(108, 76, 241, 0.04) 0%, rgba(108, 76, 241, 0.01) 100%);
border: 1px solid var(--border-light);
@@ -149,17 +150,17 @@ body {
.profitability-header-left {
display: flex;
align-items: center;
- gap: var(--space-2xl);
+ gap: var(--space-md);
min-width: 0;
flex: 1 1 auto;
}
.profitability-header-icon {
- width: 48px;
- height: 48px;
- min-width: 48px;
+ width: 36px;
+ height: 36px;
+ min-width: 36px;
- border-radius: var(--radius-md);
+ border-radius: var(--radius-sm);
background: linear-gradient(135deg, var(--primary), #7C58E8);
display: flex;
@@ -167,9 +168,9 @@ body {
justify-content: center;
color: white;
- font-size: 24px;
+ font-size: 18px;
- box-shadow: var(--shadow-md);
+ box-shadow: var(--shadow-sm);
flex-shrink: 0;
}
@@ -177,35 +178,35 @@ body {
.profitability-header-content {
display: flex;
flex-direction: column;
- gap: var(--space-sm);
+ gap: var(--space-xs);
min-width: 0;
}
.profitability-header-content h2 {
- font-size: 24px;
+ font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
- letter-spacing: -0.02em;
+ letter-spacing: -0.01em;
line-height: 1.2;
}
.profitability-header-title {
- font-size: 24px;
+ font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
- letter-spacing: -0.02em;
+ letter-spacing: -0.01em;
line-height: 1.2;
}
.profitability-header-subtitle {
- font-size: 13px;
+ font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
display: flex;
align-items: center;
- gap: var(--space-lg);
+ gap: var(--space-sm);
flex-wrap: wrap;
margin: 0;
}
@@ -232,7 +233,7 @@ body {
.header-divider {
width: 1px;
- height: 20px;
+ height: 16px;
background: var(--border);
}
@@ -242,12 +243,12 @@ body {
border-radius: 50%;
background: var(--text-tertiary);
display: inline-block;
- margin: 0 6px;
+ margin: 0 4px;
}
.profitability-kpi-group {
display: flex;
- gap: var(--space-md);
+ gap: var(--space-sm);
flex-wrap: wrap;
flex-shrink: 0;
}
@@ -255,11 +256,11 @@ body {
.profitability-dashboard .profitability-kpi-chip {
display: inline-flex;
align-items: center;
- gap: var(--space-md);
- padding: var(--space-md) var(--space-lg);
+ gap: var(--space-sm);
+ padding: 6px 12px;
border-radius: var(--radius-md);
border: 1px solid;
- font-size: 12px;
+ font-size: 11px;
font-weight: 600;
white-space: nowrap;
transition: all var(--duration-base) var(--easing);
@@ -278,12 +279,12 @@ body {
}
.profitability-kpi-chip:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-sm);
}
.profitability-kpi-chip-icon {
- font-size: 16px;
+ font-size: 14px;
display: flex;
align-items: center;
flex-shrink: 0;
@@ -292,20 +293,20 @@ body {
.profitability-kpi-chip-content {
display: flex;
flex-direction: column;
- gap: 2px;
+ gap: 1px;
}
.profitability-kpi-chip-label {
- font-size: 10px;
+ font-size: 9px;
font-weight: 700;
text-transform: uppercase;
- letter-spacing: 0.08em;
+ letter-spacing: 0.05em;
opacity: 0.8;
}
.profitability-kpi-chip-amount {
- font-size: 16px;
- font-weight: 800;
+ font-size: 13px;
+ font-weight: 700;
letter-spacing: -0.01em;
}
diff --git a/src/pages/nearle/dispatch/ProfitabilitySection.js b/src/pages/nearle/dispatch/ProfitabilitySection.js
index ca80736..5c5ca71 100644
--- a/src/pages/nearle/dispatch/ProfitabilitySection.js
+++ b/src/pages/nearle/dispatch/ProfitabilitySection.js
@@ -9,8 +9,7 @@ import {
MdRoute,
MdLocationOn,
MdBarChart,
- MdPeopleAlt,
- MdSearch
+ MdPeopleAlt
} from 'react-icons/md';
import './ProfitabilitySection.css';
@@ -161,12 +160,12 @@ function OrdersBreakdownTable({ orders, getRevenue }) {
const km = parseFloat(order.kms ?? order.actualkms ?? 0);
const rev = getRevenue(order);
return (
-
+
|
|
-
+
|
{km.toFixed(1)}
@@ -360,8 +359,6 @@ function RiderProfitabilityCard({ rider, metrics, isExpanded, isFocused, onToggl
*/
export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0, focusedRider = null, handleRiderFocus }) {
const [expanded, setExpanded] = useState({});
- const [searchQuery, setSearchQuery] = useState('');
- const [filterTab, setFilterTab] = useState('all'); // 'all', 'profitable', 'loss'
const [sortMode, setSortMode] = useState('profit-asc'); // 'profit-asc', 'profit-desc', 'name-asc', 'orders-desc'
const toggleRider = useCallback((id) => {
@@ -381,23 +378,8 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0
const dailyIsProfit = totalDailyProfit >= 0;
const slotIsProfit = slotNet >= 0;
- // Filter riders based on search query and filter tabs
- const filtered = useMemo(() => {
- return enriched.filter((r) => {
- const name = r.riderName ?? r.username ?? `Rider #${r.id}`;
- const matchesSearch = name.toLowerCase().includes(searchQuery.toLowerCase()) || String(r.id).includes(searchQuery);
-
- const isProfitable = r._m.net >= 0;
- let matchesTab = true;
- if (filterTab === 'profitable') {
- matchesTab = isProfitable;
- } else if (filterTab === 'loss') {
- matchesTab = !isProfitable;
- }
-
- return matchesSearch && matchesTab;
- });
- }, [enriched, searchQuery, filterTab]);
+ // Filter riders (filtering by search and status tabs removed)
+ const filtered = enriched;
// Sort riders based on selected sortMode
const sortedAndFiltered = useMemo(() => {
@@ -424,7 +406,7 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0
-
+
Profitability Overview
@@ -496,58 +478,7 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0
- {/* ── Controls (Search, Filter, Sort) ── */}
-
-
-
- setSearchQuery(e.target.value)}
- />
-
-
-
-
-
-
-
-
-
- Sort:
-
-
-
+ {/* ── Controls (Search) (removed) ── */}
{/* ── Rider feed ── */}
|