import React, { useState, useMemo, useCallback } from 'react'; import PropTypes from 'prop-types'; import { MdTrendingUp, MdTrendingDown, MdExpandMore, MdReceipt, MdPayments, MdRoute, MdLocationOn, MdBarChart, MdPeopleAlt } from 'react-icons/md'; import './ProfitabilitySection.css'; // ───────────────────────────────────────────────────────────── // Constants // ───────────────────────────────────────────────────────────── /** Revenue rule: ₹30 base for ≤8 km, ₹6/km beyond. */ const BASE_REVENUE = 30; 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; /** Variable fuel / wear cost per km. */ const VARIABLE_RATE_KM = 2.5; /** Status display config keyed by normalised status string. */ const STATUS_MAP = { delivered: { label: 'Delivered', color: '#10b981', bg: '#ecfdf5', border: '#a7f3d0' }, active: { label: 'Active', color: '#0ea5e9', bg: '#f0f9ff', border: '#bae6fd' }, picked: { label: 'Picked up', color: '#8b5cf6', bg: '#f5f3ff', border: '#ddd6fe' }, assigned: { label: 'Assigned', color: '#f59e0b', bg: '#fffbeb', border: '#fde68a' }, cancelled: { label: 'Cancelled', color: '#ef4444', bg: '#fef2f2', border: '#fca5a5' }, skipped: { label: 'Skipped', color: '#f97316', bg: '#fff7ed', border: '#ffedd5' } }; const DEFAULT_STATUS = STATUS_MAP.assigned; // ───────────────────────────────────────────────────────────── // Pure helpers // ───────────────────────────────────────────────────────────── function getStatusConfig(raw) { return STATUS_MAP[String(raw ?? '').toLowerCase()] ?? DEFAULT_STATUS; } function orderRevenue(order) { const km = parseFloat(order.kms || order.actualkms || 0); return km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM; } function calcRiderMetrics(rider) { const orders = rider.orders ?? []; let revenue = 0; let kms = 0; for (const o of orders) { revenue += orderRevenue(o); kms += parseFloat(o.kms || o.actualkms || 0); } const varCost = kms * VARIABLE_RATE_KM; const fixedCost = 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 }; } function rupees(v, decimals = 0) { if (v == null) return '—'; return `₹${parseFloat(v).toFixed(decimals)}`; } function riderInitials(name) { if (!name) return '?'; return ( name .trim() .split(/\s+/) .slice(0, 2) .map((w) => w[0] ?? '') .join('') .toUpperCase() || '?' ); } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } // ───────────────────────────────────────────────────────────── // Sub-components // ───────────────────────────────────────────────────────────── /** Coloured pill with a status dot. */ function OrderStatusPill({ status }) { const cfg = getStatusConfig(status); return ( ); } /** Customer name + phone + location stacked cell. */ function CustomerCell({ order }) { const name = order.customername ?? order.customer_name ?? order.deliverycustomer ?? order.pickupcustomer ?? 'Unknown customer'; const phone = order.customerphone ?? order.phone ?? order.mobile ?? ''; const location = order.locationname ?? order.kitchenname ?? order.kitchenName ?? ''; return (
{name}
{phone &&
{phone}
} {location && (
)}
); } /** Orders table inside an expanded rider card. */ function OrdersBreakdownTable({ orders, getRevenue }) { if (!orders?.length) { return (
); } return (
{orders.map((order, idx) => { const km = parseFloat(order.kms ?? order.actualkms ?? 0); const rev = getRevenue(order); return ( ); })}
Customer Status Distance Revenue
{km.toFixed(1)} km {rupees(rev)}
); } /** Expanded cost breakdown + orders for one rider. */ function RiderDetailPanel({ rider, metrics }) { const { varCost, fixedCost, kms, net, margin } = metrics; const isProfit = net >= 0; const costTiles = [ { icon: , iconClass: 'cost-tile-icon--fixed', label: 'Fixed cost', amount: rupees(fixedCost, 2), amountClass: '', footnote: 'Salary (per slot)' }, { icon: , iconClass: 'cost-tile-icon--variable', label: 'Variable cost', amount: rupees(varCost, 2), amountClass: '', footnote: `${kms.toFixed(1)} km × ₹${VARIABLE_RATE_KM}/km` }, { icon: isProfit ? : , iconClass: isProfit ? 'cost-tile-icon--profit' : 'cost-tile-icon--loss', label: isProfit ? 'Net profit' : 'Net loss', amount: `${isProfit ? '+' : ''}${rupees(net, 2)}`, amountClass: isProfit ? 'cost-tile-amount--profit' : 'cost-tile-amount--loss', footnote: `${Math.abs(margin).toFixed(0)}% margin` } ]; return (
{/* Cost breakdown tiles */}
{costTiles.map((tile) => (
{tile.label} {tile.amount} {tile.footnote}
))}
{/* Revenue breakdown table */}
{rider.orders?.length ?? 0} order{(rider.orders?.length ?? 0) !== 1 ? 's' : ''}
); } /** Single expandable rider profitability card. */ function RiderProfitabilityCard({ rider, metrics, isExpanded, isFocused, onToggle, onFocus }) { const { revenue, kms, totalCost, net, margin } = metrics; const isProfit = net >= 0; const marginBarWidth = `${clamp(Math.abs(margin), 0, 100)}%`; const orderCount = rider.orders?.length ?? 0; const name = rider.riderName ?? rider.username ?? `Rider #${rider.id}`; const cardClasses = [ 'rider-profitability-card', isProfit ? 'rider-profitability-card--profitable' : 'rider-profitability-card--unprofitable', isFocused ? 'rider-profitability-card--selected' : '' ] .filter(Boolean) .join(' '); function handleActivate() { onToggle(rider.id); if (!isFocused && onFocus) onFocus(rider); } return (
{/* Clickable header row */}
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleActivate(); } }} > {/* Rider identity */}
{name}
{orderCount} order{orderCount !== 1 ? 's' : ''} · {kms.toFixed(1)} km total
{/* Summary metrics — hidden on mobile via CSS */} {/* Expanded detail panel */} {isExpanded && }
); } // ───────────────────────────────────────────────────────────── // Main export // ───────────────────────────────────────────────────────────── /** * ProfitabilitySection * * Props: * riders — Array of rider objects with .orders[] * totalDailyProfit — Number: daily aggregate across all slots * 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 }) { const [expanded, setExpanded] = useState({}); const [sortMode, setSortMode] = useState('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]); // 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 dailyIsProfit = totalDailyProfit >= 0; const slotIsProfit = slotNet >= 0; // Filter riders (filtering by search and status tabs removed) const filtered = enriched; // Sort riders based on selected sortMode const sortedAndFiltered = useMemo(() => { const list = [...filtered]; if (sortMode === 'profit-asc') { list.sort((a, b) => a._m.net - b._m.net); } else if (sortMode === 'profit-desc') { list.sort((a, b) => b._m.net - a._m.net); } else if (sortMode === 'name-asc') { list.sort((a, b) => { const nameA = (a.riderName ?? a.username ?? `Rider #${a.id}`).toLowerCase(); const nameB = (b.riderName ?? b.username ?? `Rider #${b.id}`).toLowerCase(); return nameA.localeCompare(nameB); }); } else if (sortMode === 'orders-desc') { list.sort((a, b) => (b.orders?.length ?? 0) - (a.orders?.length ?? 0)); } return list; }, [filtered, sortMode]); return (
{/* ── Header ── */}

Profitability Overview

{enriched.length} rider{enriched.length !== 1 ? 's' : ''} {profitCount} profitable {lossCount} at loss

{/* Daily profit / loss */}
Daily {dailyIsProfit ? 'profit' : 'loss'} {rupees(Math.abs(totalDailyProfit))}
{/* Slot profit / loss */}
Slot {slotIsProfit ? 'profit' : 'loss'} {rupees(Math.abs(slotNet))}
{/* ── Summary statistics row ── */}
Riders Active {enriched.length} {profitCount} in profit · {lossCount} at loss
Slot Revenue {rupees(slotRevenue)} From {enriched.reduce((s, r) => s + (r.orders?.length ?? 0), 0)} orders
Slot Cost {rupees(slotCost)} Fixed + variable
Slot Net {slotIsProfit ? '+' : ''} {rupees(slotNet)} 0 ? (slotNet / slotRevenue >= 0 ? 'margin-positive' : 'margin-negative') : '' }`} > {slotRevenue > 0 ? `${slotNet / slotRevenue >= 0 ? '+' : ''}${((slotNet / slotRevenue) * 100).toFixed(0)}% margin` : '0% margin'}
{/* ── Controls (Search) (removed) ── */} {/* ── Rider feed ── */}
{sortedAndFiltered.length === 0 ? (
) : ( sortedAndFiltered.map((rider) => (
)) )}
); } OrderStatusPill.propTypes = { status: PropTypes.string }; CustomerCell.propTypes = { order: PropTypes.object.isRequired }; OrdersBreakdownTable.propTypes = { orders: PropTypes.array, getRevenue: PropTypes.func.isRequired }; RiderDetailPanel.propTypes = { rider: PropTypes.object.isRequired, metrics: PropTypes.shape({ varCost: PropTypes.number.isRequired, fixedCost: PropTypes.number.isRequired, kms: PropTypes.number.isRequired, net: PropTypes.number.isRequired, margin: PropTypes.number.isRequired }).isRequired }; RiderProfitabilityCard.propTypes = { rider: PropTypes.object.isRequired, metrics: PropTypes.shape({ revenue: PropTypes.number.isRequired, kms: PropTypes.number.isRequired, totalCost: PropTypes.number.isRequired, net: PropTypes.number.isRequired, margin: PropTypes.number.isRequired }).isRequired, isExpanded: PropTypes.bool.isRequired, isFocused: PropTypes.bool, onToggle: PropTypes.func.isRequired, onFocus: PropTypes.func }; ProfitabilitySection.propTypes = { riders: PropTypes.array, totalDailyProfit: PropTypes.number, focusedRider: PropTypes.object, handleRiderFocus: PropTypes.func };