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;