updates on the profitability page and added the new page of this one

This commit is contained in:
2026-06-16 19:49:57 +05:30
parent ffcd9440e6
commit b0bb2d63cf
5 changed files with 796 additions and 5 deletions

View File

@@ -149,6 +149,13 @@ const nearle = {
url: '/nearle/reports/riderslogs', url: '/nearle/reports/riderslogs',
icon: DirectionsBikeOutlinedIcon icon: DirectionsBikeOutlinedIcon
// target: true // target: true
},
{
id: 'profitability',
title: <FormattedMessage id="profitability" />,
type: 'item',
url: '/nearle/reports/profitability',
icon: icons.BarChartOutlined
} }
] ]
}, },

View File

@@ -49,7 +49,7 @@ function getStatusConfig(raw) {
} }
function orderRevenue(order) { function orderRevenue(order) {
const km = parseFloat(order.kms ?? order.actualkms ?? 0); const km = parseFloat(order.kms || order.actualkms || 0);
return km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM; return km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM;
} }
@@ -60,7 +60,7 @@ function calcRiderMetrics(rider) {
for (const o of orders) { for (const o of orders) {
revenue += orderRevenue(o); revenue += orderRevenue(o);
kms += parseFloat(o.kms ?? o.actualkms ?? 0); kms += parseFloat(o.kms || o.actualkms || 0);
} }
const varCost = kms * VARIABLE_RATE_KM; const varCost = kms * VARIABLE_RATE_KM;
@@ -472,8 +472,13 @@ export default function ProfitabilitySection({ riders = [], totalDailyProfit = 0
{slotIsProfit ? '+' : ''} {slotIsProfit ? '+' : ''}
{rupees(slotNet)} {rupees(slotNet)}
</span> </span>
<span className={`profitability-summary-detail ${slotRevenue > 0 ? (slotNet / slotRevenue >= 0 ? 'margin-positive' : 'margin-negative') : ''}`}> <span
{slotRevenue > 0 ? `${slotNet / slotRevenue >= 0 ? '+' : ''}${((slotNet / slotRevenue) * 100).toFixed(0)}% margin` : '0% margin'} className={`profitability-summary-detail ${slotRevenue > 0 ? (slotNet / slotRevenue >= 0 ? 'margin-positive' : 'margin-negative') : ''
}`}
>
{slotRevenue > 0
? `${slotNet / slotRevenue >= 0 ? '+' : ''}${((slotNet / slotRevenue) * 100).toFixed(0)}% margin`
: '0% margin'}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,773 @@
import React, { useState, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useInfiniteQuery } from '@tanstack/react-query';
import {
Avatar,
Box,
Chip,
Grid,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import {
MdMyLocation,
MdCalendarMonth,
MdPerson,
MdOutlineLocalShipping,
MdOutlineCurrencyRupee,
MdStraighten,
MdPayments,
MdRoute,
MdTrendingUp,
MdTrendingDown
} from 'react-icons/md';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { fetchDeliveries } from 'pages/api/api';
import Loader from 'components/Loader';
import DateFilterDialog from 'components/DateFilterDialog';
import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete';
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
const DT = {
radiusPill: 999,
radiusCard: 14,
shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)',
shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)',
textPrimary: '#0f172a',
textSecondary: '#64748b',
textMuted: '#94a3b8',
borderSubtle: '#e2e8f0',
divider: '#f1f5f9',
surface: '#ffffff',
surfaceAlt: '#f8fafc'
};
const aColor = (c, suffix) => `${c}${suffix}`;
const soft = (c) => aColor(c, '18');
const tint = (c) => aColor(c, '08');
const edge = (c) => aColor(c, '55');
const ring = (c) => aColor(c, '26');
const BRAND = '#662582';
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
SoftPaper.propTypes = {
children: PropTypes.node
};
const AccentAvatar = ({ color, selected, size = 24, children }) => (
<Avatar
sx={{
width: size,
height: size,
bgcolor: selected ? color : soft(color),
color: selected ? '#fff' : color,
transition: 'background-color 0.15s, color 0.15s'
}}
>
{children}
</Avatar>
);
AccentAvatar.propTypes = {
color: PropTypes.string.isRequired,
selected: PropTypes.bool,
size: PropTypes.number,
children: PropTypes.node
};
const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => (
<Tooltip title={tooltip || ''} placement="top">
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.375,
borderRadius: 999,
bgcolor: '#ffffff',
border: `1px solid ${edge(color)}`,
color,
fontSize: 11,
fontWeight: 800,
minWidth,
justifyContent: 'center',
whiteSpace: 'nowrap'
}}
>
{icon}
{label}
</Box>
</Tooltip>
);
MetricPill.propTypes = {
color: PropTypes.string.isRequired,
icon: PropTypes.node,
label: PropTypes.string.isRequired,
tooltip: PropTypes.string,
minWidth: PropTypes.number
};
const BATCHES = [
{ 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) => {
for (const b of batches) {
if (h >= b.startHour && h < b.endHour) return b.id;
}
return null;
};
const getRowBatch = (r, batches = BATCHES) => {
const t = r?.assigntime;
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 formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
}).format(Number(value) || 0);
}
export default function ProfitabilityReport() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [locaName, setLocoName] = useState('All');
const [open, setOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [appId, setAppId] = useState(0);
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
const liveUserid = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0;
// Load slots configuration from localStorage to match Dispatch page edits
const customBatches = useMemo(() => {
if (typeof window === 'undefined') return BATCHES;
try {
const raw = window.localStorage.getItem('dispatch.slots.v9');
if (!raw) return BATCHES;
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length !== BATCHES.length) return BATCHES;
return parsed.map((s, i) => {
const id = s.id || `slot-${i + 1}`;
const startHour = Number(s.startHour) || 0;
const endHour = Number(s.endHour) || 24;
return {
id,
name: s.name || BATCHES.find((b) => b.id === id)?.name || `Slot ${i + 1}`,
startHour,
endHour
};
});
} catch (e) {
return BATCHES;
}
}, []);
// Fetch all deliveries for the selected date range and zone
const {
data: deliveriesData,
isLoading: isLoadingDeliveries,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['fetchdeliveries', appId, liveUserid, 'all', startdate, enddate, 50, '', 0, 0, 0],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
refetchOnWindowFocus: false
});
// Auto-page through all results
useEffect(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
// Flatten and deduplicate deliveries by orderid
const liveRows = useMemo(() => {
const all = (deliveriesData?.pages || []).flatMap((p) => p.rows || []);
const seen = new Set();
const out = [];
for (const r of all) {
const key = r.orderid != null ? String(r.orderid) : null;
if (key && seen.has(key)) continue;
if (key) seen.add(key);
out.push(r);
}
return out;
}, [deliveriesData]);
// Group deliveries by rider
const ridersList = useMemo(() => {
const riderMap = {};
liveRows.forEach((r) => {
const key = String(r.userid || r.rider_id || '');
if (!key || key === 'unassigned' || key === '0') return;
if (!riderMap[key]) {
riderMap[key] = {
id: key,
riderName: r.ridername || r.rider_name || r.username || `Rider ${key}`,
orders: []
};
}
if (!riderMap[key].orders.some((existing) => existing.orderid === r.orderid)) {
riderMap[key].orders.push(r);
}
});
return Object.values(riderMap)
.map((r) => ({
...r,
orders: [...r.orders].sort((a, b) => {
const tA = a.trip_number || 1;
const tB = b.trip_number || 1;
if (tA !== tB) return tA - tB;
return (a.step || 0) - (b.step || 0);
})
}))
.sort((a, b) => b.orders.length - a.orders.length);
}, [liveRows]);
// Calculate profitability metrics for all riders
const stats = useMemo(() => {
let activeRiders = 0;
let totalOrders = 0;
let totalRevenue = 0;
let totalCost = 0;
let profitableRiders = 0;
let lossRiders = 0;
const list = ridersList
.map((r) => {
let rRevenue = 0;
let rKms = 0;
const activeSlots = new Set();
let ordersInSlots = 0;
r.orders.forEach((o) => {
const slot = getRowBatch(o, customBatches);
if (!slot) return;
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}`);
ordersInSlots++;
});
if (ordersInSlots === 0) {
return null;
}
const rVarCost = rKms * 2.5;
const slotCount = activeSlots.size;
const rFixedCost = slotCount * 166.67;
const rTotalCost = rVarCost + rFixedCost;
const rNet = rRevenue - rTotalCost;
const rMargin = rRevenue > 0 ? (rNet / rRevenue) * 100 : 0;
if (rNet >= 0) {
profitableRiders++;
} else {
lossRiders++;
}
totalOrders += ordersInSlots;
totalRevenue += rRevenue;
totalCost += rTotalCost;
activeRiders++;
return {
...r,
kms: rKms,
revenue: rRevenue,
varCost: rVarCost,
fixedCost: rFixedCost,
totalCost: rTotalCost,
net: rNet,
margin: rMargin
};
})
.filter(Boolean);
const totalNet = totalRevenue - totalCost;
const totalMargin = totalRevenue > 0 ? (totalNet / totalRevenue) * 100 : 0;
return {
activeRiders,
totalOrders,
totalRevenue,
totalCost,
totalNet,
totalMargin,
profitableRiders,
lossRiders,
enrichedRiders: list
};
}, [ridersList, customBatches]);
// Filter riders by search query
const filteredRiders = useMemo(() => {
if (!stats?.enrichedRiders || !Array.isArray(stats.enrichedRiders)) return [];
const baseList = stats.enrichedRiders.filter(Boolean);
if (!debouncedSearch) return baseList;
const q = debouncedSearch.toLowerCase().trim();
return baseList.filter(
(r) => r && [r.riderName, String(r.id)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q))
);
}, [stats?.enrichedRiders, debouncedSearch]);
const KPI_META = [
{
key: 'riders',
label: 'Riders Active',
color: BRAND,
icon: MdPerson,
value: stats?.activeRiders ?? 0,
detail: `${stats?.profitableRiders ?? 0} in profit · ${stats?.lossRiders ?? 0} at loss`
},
{
key: 'revenue',
label: 'Slot Revenue',
color: '#0ea5e9',
icon: MdOutlineLocalShipping,
value: formatNumberToRupees(stats?.totalRevenue ?? 0),
detail: `From ${stats?.totalOrders ?? 0} orders`
},
{
key: 'cost',
label: 'Slot Cost',
color: '#f59e0b',
icon: MdPayments,
value: formatNumberToRupees(stats?.totalCost ?? 0),
detail: 'Fixed + variable'
},
{
key: 'net',
label: 'Slot Net',
color: (stats?.totalNet ?? 0) >= 0 ? '#10b981' : '#ef4444',
icon: (stats?.totalNet ?? 0) >= 0 ? MdTrendingUp : MdTrendingDown,
value: `${(stats?.totalNet ?? 0) >= 0 ? '+' : ''}${formatNumberToRupees(stats?.totalNet ?? 0)}`,
detail: `${(stats?.totalRevenue ?? 0) > 0 ? ((stats?.totalNet ?? 0) / (stats?.totalRevenue ?? 1) >= 0 ? '+' : '') : ''}${(
stats?.totalMargin ?? 0
).toFixed(0)}% margin`
}
];
return (
<>
{(isLoadingDeliveries || isFetchingNextPage) && <Loader />}
{/* Page Header */}
<PageHeader
title="Profitability Report"
subtitle={`Live · ${locaName || 'All Zones'} · ${datestatus}`}
live
action={
<LocationAutocomplete
locaName={locaName}
setAppId={setAppId}
setLocoName={setLocoName}
pill
accentColor={BRAND}
icon={<MdMyLocation size={14} />}
placeholder="Select Zone"
paperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }}
/>
}
/>
{/* KPI Cards Grid */}
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
{KPI_META.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={6} md={3}>
<StatCard
title={item.label}
value={item.value ?? 0}
icon={<Icon size={20} />}
color={item.color}
loading={isLoadingDeliveries}
/>
<Typography variant="caption" sx={{ color: DT.textSecondary, display: 'block', mt: 0.5, px: 2, fontWeight: 500 }}>
{item.detail}
</Typography>
</Grid>
);
})}
</Grid>
{/* Filter Bar (date + search) */}
<Paper
elevation={0}
sx={{
mt: { xs: 1.5, md: 2 },
p: { xs: 1, md: 1.5 },
borderTopLeftRadius: DT.radiusCard / 8,
borderTopRightRadius: DT.radiusCard / 8,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: '1px solid',
borderColor: DT.borderSubtle,
borderBottom: 0,
background: '#fff'
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems={{ xs: 'stretch', sm: 'center' }}
justifyContent="space-between"
spacing={1.25}
>
<Stack direction="row" alignItems="center" spacing={1.25} flexWrap="wrap">
<AccentAvatar color={BRAND} size={32}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography
variant="caption"
sx={{ fontWeight: 800, color: DT.textSecondary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
Profitability Overview · {datestatus}
</Typography>
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
{filteredRiders.length} riders · {stats.profitableRiders} profitable · {stats.lossRiders} at loss
</Typography>
</Stack>
<Tooltip title="Date Filter" placement="top">
<Box
onClick={() => setOpen(true)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.75,
borderRadius: 999,
cursor: 'pointer',
bgcolor: tint('#f59e0b'),
border: `1.5px solid ${edge('#f59e0b')}`,
color: '#f59e0b',
fontWeight: 800,
fontSize: 12,
ml: 1,
transition: 'all 0.18s',
'&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` }
}}
>
<MdCalendarMonth size={14} />
{dayjs(startdate).format('DD/MM/YY')} {dayjs(enddate).format('DD/MM/YY')}
</Box>
</Tooltip>
</Stack>
<Box sx={{ width: { xs: '100%', sm: 280, lg: 340 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<DebounceSearchBar
value={searchword}
onChange={setSearchword}
onDebouncedChange={setDebouncedSearch}
placeholder="Search riders"
sx={{
m: 0,
width: '100%',
borderRadius: 999,
bgcolor: '#ffffff',
'& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 },
'&:hover fieldset': { borderColor: '#cbd5e1' },
'&.Mui-focused fieldset': { borderColor: '#662582', borderWidth: 1.5 },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
}}
/>
</Box>
</Stack>
</Paper>
{/* Table & Mobile List Container */}
<Paper
elevation={0}
sx={{
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomLeftRadius: DT.radiusCard / 8,
borderBottomRightRadius: DT.radiusCard / 8,
border: '1px solid',
borderColor: DT.borderSubtle,
overflow: 'hidden',
background: '#fff'
}}
>
{isMobile ? (
<MobileCardList scroll>
{!filteredRiders || filteredRiders.length === 0 ? (
<Stack alignItems="center" spacing={1.5} sx={{ py: 6 }}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdPerson size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
</Stack>
) : (
filteredRiders.map((row, index) => {
if (!row) return null;
const isProfit = (row.net ?? 0) >= 0;
return (
<MobileCard
key={row.id || index}
accent={isProfit ? '#10b981' : '#ef4444'}
header={
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={isProfit ? '#10b981' : '#ef4444'} size={36}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.riderName}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.id}
</Typography>
</Stack>
</Stack>
}
>
<MobileFieldGrid columns={2}>
<MobileField label="Orders" value={row.orders.length} />
<MobileField label="Distance" value={`${row.kms.toFixed(2)} km`} />
<MobileField label="Revenue" value={formatNumberToRupees(row.revenue)} />
<MobileField label="Fixed Cost" value={formatNumberToRupees(row.fixedCost)} />
<MobileField label="Variable Cost" value={formatNumberToRupees(row.varCost)} />
<MobileField label="Total Cost" value={formatNumberToRupees(row.totalCost)} />
<MobileField label="Net Profit" value={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net)}`} full />
<MobileField label="Margin" value={`${Math.abs(row.margin).toFixed(0)}%`} full />
</MobileFieldGrid>
</MobileCard>
);
})
)}
</MobileCardList>
) : (
<TableContainer
sx={{
maxHeight: 'calc(100vh - 280px)',
'&::-webkit-scrollbar': { width: 10, height: 10 },
'&::-webkit-scrollbar-thumb': {
backgroundColor: edge(BRAND),
borderRadius: 8,
'&:hover': { backgroundColor: BRAND }
},
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
}}
>
<Table stickyHeader sx={{ minWidth: 1000 }}>
<TableHead>
<TableRow
sx={{
'& th': {
backgroundColor: DT.surfaceAlt,
color: DT.textSecondary,
fontSize: 11,
fontWeight: 800,
letterSpacing: 0.6,
textTransform: 'uppercase',
whiteSpace: 'nowrap',
borderBottom: `1px solid ${DT.borderSubtle}`,
py: 1.25,
px: 2
}
}}
>
<TableCell>#</TableCell>
<TableCell>Rider</TableCell>
<TableCell align="center">Orders</TableCell>
<TableCell align="center">Distance</TableCell>
<TableCell align="center">Revenue</TableCell>
<TableCell align="center">Fixed Cost</TableCell>
<TableCell align="center">Variable Cost</TableCell>
<TableCell align="center">Total Cost</TableCell>
<TableCell align="center">Net Profit</TableCell>
<TableCell align="center">Margin</TableCell>
</TableRow>
</TableHead>
<TableBody>
{!filteredRiders || filteredRiders.length === 0 ? (
<TableRow>
<TableCell colSpan={10} sx={{ py: 6 }}>
<Stack alignItems="center" spacing={1.5}>
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
<MdPerson size={28} />
</Avatar>
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
No riders to show
</Typography>
</Stack>
</TableCell>
</TableRow>
) : (
filteredRiders.map((row, index) => {
if (!row) return null;
const isProfit = (row.net ?? 0) >= 0;
return (
<TableRow
key={row.id || index}
sx={{
transition: 'background-color 0.15s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: 1.5,
px: 2
},
'&:hover': { backgroundColor: DT.surfaceAlt }
}}
>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 700, color: DT.textMuted }}>
{String(index + 1).padStart(2, '0')}
</Typography>
</TableCell>
<TableCell>
<Stack direction="row" alignItems="center" spacing={1}>
<AccentAvatar color={BRAND} size={36}>
<MdPerson size={18} />
</AccentAvatar>
<Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
{row.riderName}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
ID #{row.id}
</Typography>
</Stack>
</Stack>
</TableCell>
<TableCell align="center">
<Typography variant="body2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
{row.orders.length}
</Typography>
</TableCell>
<TableCell align="center">
<MetricPill color="#10b981" icon={<MdStraighten size={11} />} label={`${row.kms.toFixed(2)} km`} tooltip="KMS" />
</TableCell>
<TableCell align="center">
<MetricPill
color={BRAND}
icon={<MdOutlineCurrencyRupee size={11} />}
label={formatNumberToRupees(row.revenue).replace('₹', '').trim()}
tooltip="Revenue"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#6366f1"
icon={<MdPayments size={11} />}
label={formatNumberToRupees(row.fixedCost).replace('₹', '').trim()}
tooltip="Fixed Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#f59e0b"
icon={<MdRoute size={11} />}
label={formatNumberToRupees(row.varCost).replace('₹', '').trim()}
tooltip="Variable Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color="#94a3b8"
icon={<MdPayments size={11} />}
label={formatNumberToRupees(row.totalCost).replace('₹', '').trim()}
tooltip="Total Cost"
/>
</TableCell>
<TableCell align="center">
<MetricPill
color={isProfit ? '#10b981' : '#ef4444'}
icon={isProfit ? <MdTrendingUp size={11} /> : <MdTrendingDown size={11} />}
label={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net).replace('₹', '').trim()}`}
tooltip="Net Profit"
/>
</TableCell>
<TableCell align="center">
<Chip
label={`${Math.abs(row.margin).toFixed(0)}%`}
color={isProfit ? 'success' : 'error'}
size="small"
sx={{ fontWeight: 700, minWidth: 60 }}
/>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</TableContainer>
)}
</Paper>
{/* Date Filter Dialog */}
<DateFilterDialog
open={open}
onClose={() => setOpen(false)}
onSelect={(range) => {
setStartdate(range.startDate);
setEnddate(range.endDate);
setDatestatus(range.label);
}}
/>
</>
);
}

View File

@@ -44,6 +44,7 @@ const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSum
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails'))); const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary'))); const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary')));
const RidersLogs = Loadable(lazy(() => import('pages/nearle/reports/ridersLogs'))); const RidersLogs = Loadable(lazy(() => import('pages/nearle/reports/ridersLogs')));
const Profitability = Loadable(lazy(() => import('pages/nearle/reports/profitability')));
const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders'))); const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders')));
const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider'))); const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider')));
@@ -165,6 +166,10 @@ const MainRoutes = {
{ {
path: 'riderslogs', path: 'riderslogs',
element: <RidersLogs /> element: <RidersLogs />
},
{
path: 'profitability',
element: <Profitability />
} }
] ]
}, },

View File

@@ -17,5 +17,6 @@
"riderssummary": "Riders Summary", "riderssummary": "Riders Summary",
"riderslogs": "Riders Logs", "riderslogs": "Riders Logs",
"invoice": "Invoice", "invoice": "Invoice",
"dispatch": "Dispatch" "dispatch": "Dispatch",
"profitability": "Profitability"
} }