diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index 120ea67..036a6a9 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -1604,36 +1604,43 @@ const Dispatch = ({ const totalDailyProfit = useMemo(() => { let profit = 0; - const slotRiders = {}; + const riderDailyData = {}; liveRows.forEach(r => { + // Filter by selectedDate to align with daily slot aggregations + const dateStr = r.assigntime + ? dayjs(r.assigntime).format('YYYY-MM-DD') + : r.deliverydate + ? dayjs(r.deliverydate).format('YYYY-MM-DD') + : 'unknown'; + if (selectedDate && dateStr !== selectedDate) return; + const batch = getRowBatch(r, selectedTimeField, BATCHES); if (!batch || batch === 'all') return; const riderKey = String(r.userid || r.rider_id || 'unassigned'); if (riderKey === 'unassigned' || riderKey === '0') return; - if (!slotRiders[batch]) slotRiders[batch] = {}; - if (!slotRiders[batch][riderKey]) { - slotRiders[batch][riderKey] = { revenue: 0, kms: 0 }; + if (!riderDailyData[riderKey]) { + riderDailyData[riderKey] = { revenue: 0, kms: 0, batches: new Set() }; } const kms = parseFloat(r.kms || r.actualkms || 0); - slotRiders[batch][riderKey].kms += kms; - slotRiders[batch][riderKey].revenue += (kms <= 8 ? 30 : 30 + (kms - 8) * 6); + riderDailyData[riderKey].kms += kms; + riderDailyData[riderKey].revenue += (kms <= 8 ? 30 : 30 + (kms - 8) * 6); + riderDailyData[riderKey].batches.add(batch); }); - Object.values(slotRiders).forEach(riderMap => { - Object.values(riderMap).forEach(stats => { - const variableCost = stats.kms * 2.5; - const fixedCost = 166.67; - const totalCost = variableCost + fixedCost; - profit += (stats.revenue - totalCost); - }); + Object.values(riderDailyData).forEach(stats => { + const variableCost = stats.kms * 2.5; + const slotCount = Math.min(stats.batches.size, 3); + const fixedCost = slotCount * (500 / 3); + const totalCost = variableCost + fixedCost; + profit += (stats.revenue - totalCost); }); return profit; - }, [liveRows, selectedTimeField, BATCHES]); + }, [liveRows, selectedTimeField, BATCHES, selectedDate]); // Reshape flat delivery rows into the zones/riders/orders structure Dispatch consumes. const liveData = useMemo(() => { @@ -3830,7 +3837,14 @@ const Dispatch = ({ )} {viewMode === 'profitability' ? ( - + ) : viewMode === 'rider-info' ? (
diff --git a/src/pages/nearle/dispatch/ProfitabilitySection.js b/src/pages/nearle/dispatch/ProfitabilitySection.js index ab8f0dc..a8ea3da 100644 --- a/src/pages/nearle/dispatch/ProfitabilitySection.js +++ b/src/pages/nearle/dispatch/ProfitabilitySection.js @@ -1,5 +1,6 @@ import React, { useState, useMemo, useCallback } from 'react'; import PropTypes from 'prop-types'; +import dayjs from 'dayjs'; import { MdTrendingUp, MdTrendingDown, @@ -23,7 +24,7 @@ const BASE_KM_LIMIT = 8; const EXTRA_RATE_KM = 6; /** Fixed salary cost sliced per slot (₹5000 / 30 days / 1 slot). */ -const FIXED_COST_PER_SLOT = 166.67; +const FIXED_COST_PER_SLOT = 500 / 3; /** Variable fuel / wear cost per km. */ const VARIABLE_RATE_KM = 2.5; @@ -53,23 +54,75 @@ function orderRevenue(order) { return km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM; } -function calcRiderMetrics(rider) { - const orders = rider.orders ?? []; +const BATCHES_DEFAULT = [ + { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 }, + { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 }, + { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 } +]; + +const getBatchForHour = (h, batches = BATCHES_DEFAULT) => { + for (const b of batches) { + if (h >= b.startHour && h < b.endHour) return b.id; + } + return null; +}; + +const getRowBatch = (r, batches = BATCHES_DEFAULT) => { + const t = r?.assigntime || r?.deliverydate; + if (!t) return null; + const str = String(t).trim(); + if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null; + const d = dayjs(t); + if (!d.isValid()) return null; + return getBatchForHour(d.hour() + d.minute() / 60, batches); +}; + +function calcRiderMetrics(rider, selectedDate, batches) { + const orders = (rider.orders ?? []).filter((o) => { + if (!selectedDate) return true; + const dateStr = o.assigntime + ? dayjs(o.assigntime).format('YYYY-MM-DD') + : o.deliverydate + ? dayjs(o.deliverydate).format('YYYY-MM-DD') + : 'unknown'; + return dateStr === selectedDate; + }); let revenue = 0; let kms = 0; + const slotsByDate = {}; for (const o of orders) { revenue += orderRevenue(o); kms += parseFloat(o.kms || o.actualkms || 0); + + const slot = getRowBatch(o, batches); + if (!slot) continue; + + const dateStr = o.assigntime + ? dayjs(o.assigntime).format('YYYY-MM-DD') + : o.deliverydate + ? dayjs(o.deliverydate).format('YYYY-MM-DD') + : null; + if (!dateStr) continue; + + if (!slotsByDate[dateStr]) { + slotsByDate[dateStr] = new Set(); + } + slotsByDate[dateStr].add(slot); } + let slotCount = 0; + Object.values(slotsByDate).forEach((set) => { + slotCount += Math.min(set.size, 3); + }); + const varCost = kms * VARIABLE_RATE_KM; - const fixedCost = orders.length > 0 ? FIXED_COST_PER_SLOT : 0; + const fixedCost = slotCount * FIXED_COST_PER_SLOT; const totalCost = varCost + fixedCost; const net = revenue - totalCost; const margin = revenue > 0 ? (net / revenue) * 100 : 0; - return { revenue, kms, varCost, fixedCost, totalCost, net, margin }; + return { revenue, kms, varCost, fixedCost, totalCost, net, margin, orders }; } function rupees(v, decimals = 0) { @@ -184,7 +237,7 @@ function OrdersBreakdownTable({ orders, getRevenue }) { } /** Expanded cost breakdown + orders for one rider. */ -function RiderDetailPanel({ rider, metrics }) { +function RiderDetailPanel({ metrics }) { const { varCost, fixedCost, kms, net, margin } = metrics; const isProfit = net >= 0; @@ -242,10 +295,10 @@ function RiderDetailPanel({ rider, metrics }) { Revenue Breakdown
- {rider.orders?.length ?? 0} order{(rider.orders?.length ?? 0) !== 1 ? 's' : ''} + {metrics.orders?.length ?? 0} order{(metrics.orders?.length ?? 0) !== 1 ? 's' : ''}
- + @@ -254,10 +307,10 @@ function RiderDetailPanel({ rider, metrics }) { /** Single expandable rider profitability card. */ function RiderProfitabilityCard({ rider, metrics, isExpanded, isFocused, onToggle, onFocus }) { - const { revenue, kms, totalCost, net, margin } = metrics; + const { revenue, kms, totalCost, net, margin, orders } = metrics; const isProfit = net >= 0; const marginBarWidth = `${clamp(Math.abs(margin), 0, 100)}%`; - const orderCount = rider.orders?.length ?? 0; + const orderCount = orders?.length ?? 0; const name = rider.riderName ?? rider.username ?? `Rider #${rider.id}`; const cardClasses = [ @@ -357,29 +410,45 @@ function RiderProfitabilityCard({ rider, metrics, isExpanded, isFocused, onToggl * focusedRider — Rider object (or null) synced with map * handleRiderFocus — (rider) => void called when a card is clicked */ -export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0, focusedRider = null, handleRiderFocus }) { +export default function ProfitabilitySection({ + riders = [], + totalDailyProfit = 0, + focusedRider = null, + handleRiderFocus, + selectedDate, + batches +}) { const [expanded, setExpanded] = useState({}); - const [sortMode, setSortMode] = useState('profit-asc'); // 'profit-asc', 'profit-desc', 'name-asc', 'orders-desc' + const sortMode = 'profit-asc'; // 'profit-asc', 'profit-desc', 'name-asc', 'orders-desc' const toggleRider = useCallback((id) => { setExpanded((prev) => ({ ...prev, [id]: !prev[id] })); }, []); // Enrich riders with computed metrics - const enriched = useMemo(() => riders.map((r) => ({ ...r, _m: calcRiderMetrics(r) })), [riders]); + const enriched = useMemo( + () => riders.map((r) => ({ ...r, _m: calcRiderMetrics(r, selectedDate, batches) })), + [riders, selectedDate, batches] + ); + + // Filter out inactive riders (those with 0 orders in the selected date/slot) to ensure accurate KPI aggregations + const activeEnriched = useMemo( + () => enriched.filter((r) => r._m.orders.length > 0), + [enriched] + ); // Aggregate overall slot totals (pre-filtered for BI consistency) - const slotRevenue = enriched.reduce((s, r) => s + r._m.revenue, 0); - const slotCost = enriched.reduce((s, r) => s + r._m.totalCost, 0); - const slotNet = enriched.reduce((s, r) => s + r._m.net, 0); - const profitCount = enriched.filter((r) => r._m.net >= 0).length; - const lossCount = enriched.length - profitCount; + const slotRevenue = activeEnriched.reduce((s, r) => s + r._m.revenue, 0); + const slotCost = activeEnriched.reduce((s, r) => s + r._m.totalCost, 0); + const slotNet = activeEnriched.reduce((s, r) => s + r._m.net, 0); + const profitCount = activeEnriched.filter((r) => r._m.net >= 0).length; + const lossCount = activeEnriched.length - profitCount; const dailyIsProfit = totalDailyProfit >= 0; const slotIsProfit = slotNet >= 0; // Filter riders (filtering by search and status tabs removed) - const filtered = enriched; + const filtered = activeEnriched; // Sort riders based on selected sortMode const sortedAndFiltered = useMemo(() => { @@ -395,7 +464,7 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0 return nameA.localeCompare(nameB); }); } else if (sortMode === 'orders-desc') { - list.sort((a, b) => (b.orders?.length ?? 0) - (a.orders?.length ?? 0)); + list.sort((a, b) => (b._m.orders?.length ?? 0) - (a._m.orders?.length ?? 0)); } return list; }, [filtered, sortMode]); @@ -412,7 +481,7 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0

Profitability Overview

- {enriched.length} rider{enriched.length !== 1 ? 's' : ''} + {activeEnriched.length} rider{activeEnriched.length !== 1 ? 's' : ''} {profitCount} profitable @@ -451,7 +520,7 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0

Riders Active - {enriched.length} + {activeEnriched.length} {profitCount} in profit · {lossCount} at loss @@ -459,7 +528,7 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0
Slot Revenue {rupees(slotRevenue)} - From {enriched.reduce((s, r) => s + (r.orders?.length ?? 0), 0)} orders + From {enriched.reduce((s, r) => s + (r._m.orders?.length ?? 0), 0)} orders
Slot Cost @@ -558,5 +627,7 @@ ProfitabilitySection.propTypes = { riders: PropTypes.array, totalDailyProfit: PropTypes.number, focusedRider: PropTypes.object, - handleRiderFocus: PropTypes.func + handleRiderFocus: PropTypes.func, + selectedDate: PropTypes.string, + batches: PropTypes.array }; diff --git a/src/pages/nearle/reports/profitability.js b/src/pages/nearle/reports/profitability.js index f9a1376..2ad15fd 100644 --- a/src/pages/nearle/reports/profitability.js +++ b/src/pages/nearle/reports/profitability.js @@ -291,7 +291,7 @@ export default function ProfitabilityReport() { .map((r) => { let rRevenue = 0; let rKms = 0; - const activeSlots = new Set(); + const slotsByDate = {}; let ordersInSlots = 0; r.orders.forEach((o) => { @@ -301,8 +301,18 @@ export default function ProfitabilityReport() { const oKms = parseFloat(o.kms || o.actualkms || 0); rKms += oKms; rRevenue += oKms <= 8 ? 30 : 30 + (oKms - 8) * 6; - const dateStr = o.assigntime ? dayjs(o.assigntime).format('YYYY-MM-DD') : 'unknown'; - activeSlots.add(`${dateStr}_${slot}`); + + const dateStr = o.assigntime + ? dayjs(o.assigntime).format('YYYY-MM-DD') + : o.deliverydate + ? dayjs(o.deliverydate).format('YYYY-MM-DD') + : null; + if (!dateStr) return; + + if (!slotsByDate[dateStr]) { + slotsByDate[dateStr] = new Set(); + } + slotsByDate[dateStr].add(slot); ordersInSlots++; }); @@ -310,9 +320,14 @@ export default function ProfitabilityReport() { return null; } + // Sum unique slots per day, capping at 3 slots max per day + let slotCount = 0; + Object.values(slotsByDate).forEach((set) => { + slotCount += Math.min(set.size, 3); + }); + const rVarCost = rKms * 2.5; - const slotCount = activeSlots.size; - const rFixedCost = slotCount * 166.67; + const rFixedCost = slotCount * (500 / 3); const rTotalCost = rVarCost + rFixedCost; const rNet = rRevenue - rTotalCost; const rMargin = rRevenue > 0 ? (rNet / rRevenue) * 100 : 0;