From ffcd9440e6bac4e47cbb82f2e5f8a473c4b86133 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Tue, 16 Jun 2026 15:33:06 +0530 Subject: [PATCH] updates on the active page and the profitability section --- src/pages/nearle/dispatch/Dispatch.css | 83 +++++ src/pages/nearle/dispatch/Dispatch.js | 348 +++++++++++++++--- .../nearle/dispatch/ProfitabilitySection.css | 63 ++-- .../nearle/dispatch/ProfitabilitySection.js | 83 +---- 4 files changed, 419 insertions(+), 158 deletions(-) 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 ── */}