update ui admin

This commit is contained in:
2026-06-11 20:17:18 +05:30
parent 4ad40b2c6d
commit 0736712464
51 changed files with 5466 additions and 1445 deletions

View File

@@ -17,8 +17,11 @@
"@mui/material": "^5.15.20", "@mui/material": "^5.15.20",
"@mui/x-date-pickers": "^6.20.2", "@mui/x-date-pickers": "^6.20.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-leaflet": "^4.2.1",
"react-router-dom": "^6.23.1" "react-router-dom": "^6.23.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -30,7 +30,7 @@ export default function App() {
<Route path="/orders" element={load(() => import('@/pages/orders/OrdersList'))} /> <Route path="/orders" element={load(() => import('@/pages/orders/OrdersList'))} />
<Route path="/orders/create" element={load(() => import('@/pages/orders/CreateOrder'))} /> <Route path="/orders/create" element={load(() => import('@/pages/orders/CreateOrder'))} />
<Route path="/orders/create-multiple" element={load(() => import('@/pages/orders/CreateMultipleOrders'))} /> <Route path="/orders/create-multiple" element={load(() => import('@/pages/orders/CreateMultipleOrders'))} />
<Route path="/orders/assign" element={load(() => import('@/pages/orders/AssignOrders'))} /> <Route path="/orders/assign" element={load(() => import('@/pages/dispatch/DispatchBoard'))} />
<Route path="/orders/:id" element={load(() => import('@/pages/orders/OrderDetails'))} /> <Route path="/orders/:id" element={load(() => import('@/pages/orders/OrderDetails'))} />
<Route path="/deliveries" element={load(() => import('@/pages/Deliveries'))} /> <Route path="/deliveries" element={load(() => import('@/pages/Deliveries'))} />
@@ -42,7 +42,8 @@ export default function App() {
<Route path="/hubs" element={load(() => import('@/pages/hubs/HubNetwork'))} /> <Route path="/hubs" element={load(() => import('@/pages/hubs/HubNetwork'))} />
<Route path="/fleet" element={load(() => import('@/pages/fleet/Fleet'))} /> <Route path="/fleet" element={load(() => import('@/pages/fleet/Fleet'))} />
<Route path="/dispatch" element={load(() => import('@/pages/dispatch/AiDispatch'))} /> <Route path="/dispatch" element={load(() => import('@/pages/dispatch/AiDispatch'))} />
<Route path="/tracking" element={load(() => import('@/pages/tracking/LiveTracking'))} /> <Route path="/tracking" element={load(() => import('@/pages/operations/DispatchTracking'))} />
<Route path="/tracking/live" element={load(() => import('@/pages/tracking/LiveTracking'))} />
<Route path="/tracking/journey" element={load(() => import('@/pages/tracking/ShipmentJourney'))} /> <Route path="/tracking/journey" element={load(() => import('@/pages/tracking/ShipmentJourney'))} />
<Route path="/tracking/journey/:id" element={load(() => import('@/pages/tracking/ShipmentJourney'))} /> <Route path="/tracking/journey/:id" element={load(() => import('@/pages/tracking/ShipmentJourney'))} />
<Route path="/analytics" element={load(() => import('@/pages/analytics/Analytics'))} /> <Route path="/analytics" element={load(() => import('@/pages/analytics/Analytics'))} />
@@ -51,7 +52,7 @@ export default function App() {
<Route path="/tenants" element={load(() => import('@/pages/tenants/Tenants'))} /> <Route path="/tenants" element={load(() => import('@/pages/tenants/Tenants'))} />
<Route path="/tenants/create" element={load(() => import('@/pages/tenants/CreateClient'))} /> <Route path="/tenants/create" element={load(() => import('@/pages/tenants/CreateClient'))} />
<Route path="/customers" element={load(() => import('@/pages/customers/Customers'))} /> <Route path="/customers" element={load(() => import('@/pages/business/CustomersHub'))} />
<Route path="/customers/create" element={load(() => import('@/pages/customers/CreateCustomer'))} /> <Route path="/customers/create" element={load(() => import('@/pages/customers/CreateCustomer'))} />
<Route path="/pricing" element={load(() => import('@/pages/Pricing'))} /> <Route path="/pricing" element={load(() => import('@/pages/Pricing'))} />
@@ -60,12 +61,13 @@ export default function App() {
<Route path="/riders/create" element={load(() => import('@/pages/riders/CreateRider'))} /> <Route path="/riders/create" element={load(() => import('@/pages/riders/CreateRider'))} />
<Route path="/riders/:id/edit" element={load(() => import('@/pages/riders/EditRider'))} /> <Route path="/riders/:id/edit" element={load(() => import('@/pages/riders/EditRider'))} />
<Route path="/reports" element={load(() => import('@/pages/reports/ReportsHub'))} />
<Route path="/reports/orders-summary" element={load(() => import('@/pages/reports/OrdersSummary'))} /> <Route path="/reports/orders-summary" element={load(() => import('@/pages/reports/OrdersSummary'))} />
<Route path="/reports/orders-details" element={load(() => import('@/pages/reports/OrdersDetails'))} /> <Route path="/reports/orders-details" element={load(() => import('@/pages/reports/OrdersDetails'))} />
<Route path="/reports/riders-summary" element={load(() => import('@/pages/reports/RidersSummary'))} /> <Route path="/reports/riders-summary" element={load(() => import('@/pages/reports/RidersSummary'))} />
<Route path="/reports/riders-logs" element={load(() => import('@/pages/reports/RidersLogs'))} /> <Route path="/reports/riders-logs" element={load(() => import('@/pages/reports/RidersLogs'))} />
<Route path="/invoice" element={load(() => import('@/pages/invoice/Invoices'))} /> <Route path="/invoice" element={load(() => import('@/pages/business/FinanceHub'))} />
<Route path="/invoice/:id" element={load(() => import('@/pages/invoice/InvoicePreview'))} /> <Route path="/invoice/:id" element={load(() => import('@/pages/invoice/InvoicePreview'))} />
<Route path="/profile" element={load(() => import('@/pages/Profile'))} /> <Route path="/profile" element={load(() => import('@/pages/Profile'))} />

View File

@@ -0,0 +1,50 @@
import { Card, Grid, Stack, Box, Typography, Avatar } from '@mui/material';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined';
import LocalGasStationOutlinedIcon from '@mui/icons-material/LocalGasStationOutlined';
import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined';
import SpeedOutlinedIcon from '@mui/icons-material/SpeedOutlined';
import { aiImpact } from '@/data/mock';
// ==============================|| AI IMPACT ||============================== //
// What MileTruth AI delivered — six contribution metrics. Read-only, no action lists.
const ITEMS = [
{ icon: RouteOutlinedIcon, value: `${aiImpact.routeSavings}%`, label: 'Route Savings', color: '#1D4ED8' },
{ icon: ScheduleOutlinedIcon, value: aiImpact.delaysPrevented, label: 'Delays Prevented', color: '#00A2AE' },
{ icon: TuneOutlinedIcon, value: aiImpact.optimizationActions, label: 'Optimization Actions', color: '#EA580C' },
{ icon: LocalGasStationOutlinedIcon, value: `${aiImpact.fuelSavedL.toLocaleString('en-IN')} L`, label: 'Fuel Saved', color: '#8A6500' },
{ icon: EnergySavingsLeafOutlinedIcon, value: `${(aiImpact.co2ReducedKg / 1000).toFixed(1)} t`, label: 'CO₂ Reduction', color: '#00A854' },
{ icon: SpeedOutlinedIcon, value: `${aiImpact.networkEfficiency}%`, label: 'Network Efficiency', color: '#7C3AED' }
];
export default function AiImpactSummary() {
return (
<Card sx={{ p: 2.5 }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 2 }}>
<AutoAwesomeOutlinedIcon fontSize="small" sx={{ color: '#EA580C' }} />
<Typography variant="h5" sx={{ fontWeight: 700 }}>AI Impact</Typography>
<Typography variant="caption" color="text.secondary">MileTruth AI · last 30 days</Typography>
</Stack>
<Grid container spacing={2.5}>
{ITEMS.map((it) => (
<Grid key={it.label} item xs={6} sm={4} lg={2}>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar variant="rounded" sx={{ bgcolor: 'grey.100', color: it.color, width: 42, height: 42 }}>
<it.icon fontSize="small" />
</Avatar>
<Box sx={{ minWidth: 0 }}>
<Typography variant="h4" sx={{ fontWeight: 800, color: 'grey.900', lineHeight: 1.05 }}>{it.value}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>{it.label}</Typography>
</Box>
</Stack>
</Grid>
))}
</Grid>
</Card>
);
}

View File

@@ -0,0 +1,63 @@
import { useNavigate } from 'react-router-dom';
import { Grid, Card, Stack, Box, Typography, Avatar } from '@mui/material';
import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
import { attentionItems } from '@/data/mock';
// ==============================|| ATTENTION REQUIRED — DECISION CARDS ||============================== //
// Visual, single-click cards. Answers "what needs action now?" — no tables, no detail dumps.
const ICON = { delay: ScheduleOutlinedIcon, alert: WarningAmberRoundedIcon, hub: HubOutlinedIcon, route: AltRouteOutlinedIcon };
const SEV = {
high: { fg: '#A82216', bg: '#FEEAE9', label: 'High', accent: '#F04134' },
medium: { fg: '#8A6500', bg: '#FFF7E0', label: 'Medium', accent: '#FFBF00' },
low: { fg: '#00727B', bg: '#E0F7F8', label: 'Low', accent: '#00A2AE' }
};
export default function AttentionCards() {
const navigate = useNavigate();
return (
<Grid container spacing={2.5}>
{attentionItems.map((it) => {
const Icon = ICON[it.icon] || WarningAmberRoundedIcon;
const sev = SEV[it.severity] || SEV.low;
return (
<Grid key={it.key} item xs={12} sm={6} lg={3}>
<Card
onClick={() => navigate(it.to)}
sx={{
cursor: 'pointer',
height: '100%',
borderLeft: '3px solid',
borderLeftColor: sev.accent,
transition: 'box-shadow .15s ease, transform .15s ease',
'&:hover': { boxShadow: '0 4px 16px rgba(16,24,40,0.10)', transform: 'translateY(-1px)' }
}}
>
<Stack direction="row" alignItems="center" spacing={1.75} sx={{ p: 2 }}>
<Avatar variant="rounded" sx={{ bgcolor: sev.bg, color: sev.fg, width: 46, height: 46, flexShrink: 0 }}>
<Icon />
</Avatar>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Stack direction="row" alignItems="baseline" spacing={1}>
<Typography variant="h3" sx={{ fontWeight: 800, color: 'grey.900', lineHeight: 1 }}>{it.count}</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: sev.fg }}>{sev.label}</Typography>
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600, mt: 0.25 }} noWrap>
{it.label}
</Typography>
</Box>
<ChevronRightRoundedIcon sx={{ color: 'grey.400', flexShrink: 0 }} />
</Stack>
</Card>
</Grid>
);
})}
</Grid>
);
}

View File

@@ -0,0 +1,62 @@
import { useState } from 'react';
import { Button, Menu, MenuItem, Divider, Stack } from '@mui/material';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs from 'dayjs';
import FormDialog from '@/components/FormDialog';
import { useFilters, buildRange, customRange, DATA_TODAY } from '@/store/Filters';
// ==============================|| DATE RANGE FILTER ||============================== //
// A real, functional range picker bound to the global filter store. Presets + custom range;
// every report that renders it shares (and persists) the same selection.
const PRESETS = [
['today', 'Today'],
['yesterday', 'Yesterday'],
['last7', 'Last 7 Days'],
['last30', 'Last 30 Days'],
['thisMonth', 'This Month'],
['prevMonth', 'Previous Month']
];
export default function DateRangeFilter() {
const { range, setRange } = useFilters();
const [anchor, setAnchor] = useState(null);
const [customOpen, setCustomOpen] = useState(false);
const [from, setFrom] = useState(range.start || DATA_TODAY.subtract(6, 'day'));
const [to, setTo] = useState(range.end || DATA_TODAY);
const pick = (key) => { setRange(buildRange(key)); setAnchor(null); };
const openCustom = () => { setAnchor(null); setFrom(range.start || DATA_TODAY.subtract(6, 'day')); setTo(range.end || DATA_TODAY); setCustomOpen(true); };
const applyCustom = () => { if (from && to) setRange(customRange(dayjs(from), dayjs(to))); setCustomOpen(false); };
return (
<>
<Button
variant="outlined"
startIcon={<CalendarTodayOutlinedIcon />}
endIcon={<KeyboardArrowDownIcon />}
onClick={(e) => setAnchor(e.currentTarget)}
sx={{ color: 'text.secondary', borderColor: 'grey.300', fontWeight: 600 }}
>
{range.label}
</Button>
<Menu anchorEl={anchor} open={Boolean(anchor)} onClose={() => setAnchor(null)} PaperProps={{ sx: { minWidth: 180 } }}>
{PRESETS.map(([k, l]) => (
<MenuItem key={k} selected={range.key === k} onClick={() => pick(k)}>{l}</MenuItem>
))}
<Divider />
<MenuItem selected={range.key === 'custom'} onClick={openCustom}>Custom Range</MenuItem>
</Menu>
<FormDialog open={customOpen} onClose={() => setCustomOpen(false)} title="Custom Date Range" onSubmit={applyCustom} submitLabel="Apply" maxWidth="xs">
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mt: 0.5 }}>
<DatePicker label="From" value={from} onChange={setFrom} maxDate={to || undefined} slotProps={{ textField: { size: 'small', fullWidth: true } }} />
<DatePicker label="To" value={to} onChange={setTo} minDate={from || undefined} slotProps={{ textField: { size: 'small', fullWidth: true } }} />
</Stack>
</FormDialog>
</>
);
}

View File

@@ -0,0 +1,22 @@
import { Stack, Typography, Chip } from '@mui/material';
import { useFilters } from '@/store/Filters';
// ==============================|| FILTER SUMMARY ||============================== //
// "Showing data for: <range> · Location: <location> · <count> records" — confirms exactly which
// filters are active so the data on screen is never ambiguous.
export default function FilterSummary({ count }) {
const { range, location } = useFilters();
return (
<Stack direction="row" spacing={1} alignItems="center" sx={{ flexWrap: 'wrap', gap: 0.75, color: 'text.secondary' }}>
<Typography variant="caption">Showing data for</Typography>
<Chip size="small" label={range.label} sx={{ height: 22, fontWeight: 700, bgcolor: 'grey.100', color: 'grey.800' }} />
<Typography variant="caption">· Location</Typography>
<Chip size="small" label={location === 'all' ? 'All Locations' : location} sx={{ height: 22, fontWeight: 700, bgcolor: 'grey.100', color: 'grey.800' }} />
{typeof count === 'number' && (
<Typography variant="caption">· {count} record{count === 1 ? '' : 's'}</Typography>
)}
</Stack>
);
}

View File

@@ -0,0 +1,72 @@
import { Card, CardContent, Box, Typography, Avatar, Stack, Divider } from '@mui/material';
import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded';
import ArrowDownwardRoundedIcon from '@mui/icons-material/ArrowDownwardRounded';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
// ==============================|| EXECUTIVE KPI CARD ||============================== //
// Taller operational KPI tile: headline metric + trend + an AI insight line, plus a secondary
// breakdown strip so each card carries real context. `details` = [{ label, value }].
export default function KpiCard({ title, value, icon: Icon, trend, comparison, insight, details = [] }) {
const trendUp = typeof trend === 'number' ? trend >= 0 : null;
return (
<Card sx={{ height: '100%' }}>
<CardContent sx={{ p: 2.5, minHeight: 168, display: 'flex', flexDirection: 'column' }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Box>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
{title}
</Typography>
<Typography variant="h2" sx={{ mt: 1, fontWeight: 700, color: 'grey.900', lineHeight: 1.05 }}>
{value}
</Typography>
{(trendUp !== null || comparison) && (
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ mt: 1 }}>
{trendUp !== null && (
<Stack
direction="row"
alignItems="center"
spacing={0.25}
sx={{ px: 0.75, py: 0.125, borderRadius: 1, bgcolor: trendUp ? 'success.lighter' : 'error.lighter', color: trendUp ? 'success.dark' : 'error.dark' }}
>
{trendUp ? <ArrowUpwardRoundedIcon sx={{ fontSize: 14 }} /> : <ArrowDownwardRoundedIcon sx={{ fontSize: 14 }} />}
<Typography variant="caption" sx={{ fontWeight: 700 }}>{Math.abs(trend)}%</Typography>
</Stack>
)}
{comparison && <Typography variant="caption" color="text.secondary">{comparison}</Typography>}
</Stack>
)}
</Box>
{Icon && (
<Avatar variant="rounded" sx={{ bgcolor: 'grey.100', color: 'grey.600', width: 42, height: 42 }}>
<Icon fontSize="small" />
</Avatar>
)}
</Stack>
{insight && (
<Stack direction="row" spacing={0.5} alignItems="flex-start" sx={{ mt: 1.25 }}>
<AutoAwesomeOutlinedIcon sx={{ fontSize: 13, color: '#EA580C', mt: 0.15, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: 'grey.600', lineHeight: 1.3 }}>{insight}</Typography>
</Stack>
)}
{details.length > 0 && (
<>
<Box sx={{ flexGrow: 1 }} />
<Divider sx={{ mt: 1.5, mb: 1.25 }} />
<Stack direction="row" spacing={2} divider={<Divider orientation="vertical" flexItem />}>
{details.map((d) => (
<Box key={d.label} sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.2 }} noWrap>{d.value}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>{d.label}</Typography>
</Box>
))}
</Stack>
</>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,57 @@
import { Card, Box, Stack, Typography, Divider } from '@mui/material';
// ==============================|| KPI STRIP ||============================== //
// Operational KPI row shared across dashboard & table pages (Orders, Deliveries…).
// Premium analytics styling: tall cells, prominent values, subtle separators + hover.
// items: [{ label, value, color, icon }].
export default function KpiStrip({ items = [], sx }) {
return (
<Card sx={{ mb: 2, borderRadius: 3, ...sx }}>
<Stack
direction="row"
alignItems="stretch"
divider={<Divider orientation="vertical" flexItem sx={{ borderColor: 'grey.100' }} />}
sx={{ overflowX: 'auto', flexWrap: { xs: 'wrap', lg: 'nowrap' } }}
>
{items.map((it) => {
const Icon = it.icon;
// Keep short metrics at full prominence; step long values (e.g. ₹3,84,200) down so
// nothing truncates while six cards share one full-width row on laptop screens.
const len = String(it.value).length;
const valueSize = len <= 5 ? '2.125rem' : len <= 7 ? '1.85rem' : '1.6rem';
return (
<Stack
key={it.label}
sx={{
flex: 1,
minWidth: 180,
minHeight: 100,
px: 3,
py: '18px',
justifyContent: 'center',
gap: 1,
transition: 'background-color .15s ease',
'&:hover': { bgcolor: 'grey.50' }
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
<Typography sx={{ fontSize: '0.9375rem', fontWeight: 600, color: 'text.secondary', lineHeight: 1.25 }}>
{it.label}
</Typography>
{Icon && (
<Box sx={{ width: 34, height: 34, borderRadius: 1.5, bgcolor: 'grey.100', color: it.color || 'grey.600', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Icon sx={{ fontSize: 20 }} />
</Box>
)}
</Stack>
<Typography sx={{ fontSize: valueSize, fontWeight: 800, lineHeight: 1.1, color: it.color || 'grey.900' }} noWrap>
{it.value}
</Typography>
</Stack>
);
})}
</Stack>
</Card>
);
}

View File

@@ -1,11 +1,13 @@
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
// ==============================|| DOORMILE WORDMARK LOGO ||============================== // // ==============================|| DOORMILE WORDMARK LOGO ||============================== //
// Uses the brand wordmark asset (white PNG). `onDark` shows it as-is on dark/red // The asset is the Doormile wordmark as a transparent-background mask. We paint it with the
// surfaces; on light surfaces it is recoloured to near-black. `compact` (e.g. the // brand red on light surfaces and white on dark/red surfaces using a CSS mask, so a single
// asset renders crisply on both the white header and the red sidebar. `compact` (e.g. the
// collapsed sidebar) renders just the square "D" badge, since the wordmark won't fit. // collapsed sidebar) renders just the square "D" badge, since the wordmark won't fit.
const LOGO_SRC = '/Doormile-logo.png'; const LOGO_SRC = '/Doormile-logo.png';
const ASPECT = 748 / 100; // intrinsic ratio of the wordmark asset
export default function Logo({ onDark = false, compact = false, height = 26, sx }) { export default function Logo({ onDark = false, compact = false, height = 26, sx }) {
if (compact) { if (compact) {
@@ -35,15 +37,21 @@ export default function Logo({ onDark = false, compact = false, height = 26, sx
return ( return (
<Box sx={{ display: 'flex', alignItems: 'center', ...sx }}> <Box sx={{ display: 'flex', alignItems: 'center', ...sx }}>
<Box <Box
component="img" aria-label="Doormile"
src={LOGO_SRC} role="img"
alt="Doormile"
sx={{ sx={{
height, height,
width: 'auto', width: height * ASPECT,
display: 'block', flexShrink: 0,
// The asset is white; on light surfaces recolour it to near-black so it stays visible. bgcolor: onDark ? '#FFFFFF' : '#C01227',
filter: onDark ? 'none' : 'brightness(0) saturate(100%)' WebkitMaskImage: `url(${LOGO_SRC})`,
maskImage: `url(${LOGO_SRC})`,
WebkitMaskRepeat: 'no-repeat',
maskRepeat: 'no-repeat',
WebkitMaskSize: 'contain',
maskSize: 'contain',
WebkitMaskPosition: 'left center',
maskPosition: 'left center'
}} }}
/> />
</Box> </Box>

View File

@@ -0,0 +1,91 @@
import { Card, Box, Stack, Typography, LinearProgress } from '@mui/material';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
import WarningAmberRoundedIcon from '@mui/icons-material/WarningAmberRounded';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import { threeMile } from '@/data/mock';
// ==============================|| OPERATIONS LAYER — FIRST → MID → LAST ||============================== //
// Each stage: throughput · utilization · exception count · bottleneck · AI recommendation.
const statusFor = (onTime) =>
onTime >= 98 ? { label: 'On Track', color: 'success.main' } : onTime >= 95 ? { label: 'Watch', color: 'warning.main' } : { label: 'At Risk', color: 'error.main' };
function FlowNode({ stage }) {
const status = statusFor(stage.onTime);
const utilColor = stage.utilization > 85 ? 'error' : stage.utilization > 70 ? 'warning' : 'success';
return (
<Card variant="outlined" sx={{ flex: 1, p: 2, border: '1px solid', borderColor: 'grey.200', boxShadow: 'none', height: '100%' }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.900' }}>{stage.title}</Typography>
<Typography variant="caption" color="text.secondary">{stage.subtitle}</Typography>
</Box>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ flexShrink: 0 }}>
<Box sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: status.color }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: status.color }}>{status.label}</Typography>
</Stack>
</Stack>
<Stack direction="row" spacing={2.5} alignItems="flex-end" sx={{ mt: 1.5 }}>
<Box>
<Typography variant="h4" sx={{ fontWeight: 800, color: 'grey.900', lineHeight: 1 }}>{stage.metric}</Typography>
<Typography variant="caption" color="text.secondary">{stage.metricLabel}</Typography>
</Box>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.1 }}>{stage.throughput.toLocaleString('en-IN')}</Typography>
<Typography variant="caption" color="text.secondary">throughput</Typography>
</Box>
<Box sx={{ ml: 'auto', textAlign: 'right' }}>
<Stack direction="row" spacing={0.4} alignItems="center" justifyContent="flex-end" sx={{ color: stage.exceptions > 0 ? 'error.main' : 'success.main' }}>
<WarningAmberRoundedIcon sx={{ fontSize: 15 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{stage.exceptions}</Typography>
</Stack>
<Typography variant="caption" color="text.secondary">exceptions</Typography>
</Box>
</Stack>
{/* utilization */}
<Box sx={{ mt: 1.5 }}>
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.25 }}>
<Typography variant="caption" color="text.secondary">Utilization</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.800' }}>{stage.utilization}%</Typography>
</Stack>
<LinearProgress variant="determinate" value={stage.utilization} color={utilColor} sx={{ height: 6, borderRadius: 3 }} />
</Box>
{/* bottleneck */}
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 1 }}>
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: stage.bottleneck ? 'error.main' : 'success.main', flexShrink: 0 }} />
<Typography variant="caption" color="text.secondary" noWrap>
{stage.bottleneck ? `Bottleneck · ${stage.bottleneck}` : 'No active bottleneck'}
</Typography>
</Stack>
{/* AI recommendation */}
<Stack direction="row" spacing={0.75} alignItems="flex-start" sx={{ mt: 1, p: 1, borderRadius: 1.5, bgcolor: 'grey.50' }}>
<AutoAwesomeOutlinedIcon sx={{ fontSize: 15, color: '#EA580C', mt: 0.1, flexShrink: 0 }} />
<Typography variant="caption" sx={{ color: 'grey.700', lineHeight: 1.35 }}>{stage.aiRec}</Typography>
</Stack>
</Card>
);
}
export default function NetworkFlow() {
return (
<Stack direction={{ xs: 'column', md: 'row' }} spacing={0} alignItems="stretch">
{threeMile.map((stage, i) => (
<Stack key={stage.key} direction={{ xs: 'column', md: 'row' }} alignItems="stretch" sx={{ flex: 1 }}>
<Box sx={{ width: '100%', display: 'flex' }}>
<FlowNode stage={stage} />
</Box>
{i < threeMile.length - 1 && (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'grey.400', px: { md: 1 }, py: { xs: 0.25, md: 0 }, transform: { xs: 'rotate(90deg)', md: 'none' } }}>
<ArrowRightAltRoundedIcon />
</Box>
)}
</Stack>
))}
</Stack>
);
}

View File

@@ -19,9 +19,6 @@ export default function PageHeader({ title, breadcrumbs = [], action }) {
</Typography> </Typography>
{breadcrumbs.length > 0 && ( {breadcrumbs.length > 0 && (
<Breadcrumbs separator={<NavigateNextIcon fontSize="small" />} sx={{ mt: 0.5 }}> <Breadcrumbs separator={<NavigateNextIcon fontSize="small" />} sx={{ mt: 0.5 }}>
<Link component={RouterLink} to="/dashboard" underline="hover" color="text.secondary" variant="caption">
Home
</Link>
{breadcrumbs.map((b, i) => {breadcrumbs.map((b, i) =>
b.to && i < breadcrumbs.length - 1 ? ( b.to && i < breadcrumbs.length - 1 ? (
<Link key={i} component={RouterLink} to={b.to} underline="hover" color="text.secondary" variant="caption"> <Link key={i} component={RouterLink} to={b.to} underline="hover" color="text.secondary" variant="caption">

View File

@@ -0,0 +1,46 @@
import { Stack, TextField, MenuItem, InputAdornment, Box } from '@mui/material';
import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
// ==============================|| PAGE TOOLBAR ||============================== //
// Dense search + filters + actions row for table pages. filters: [{ value, onChange,
// label, options:[{value,label}] }]. `search`/`onSearch` wire the search box; `actions`
// renders on the right.
export default function PageToolbar({ search, onSearch, searchPlaceholder = 'Search…', filters = [], actions, sx }) {
return (
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={1.25}
alignItems={{ md: 'center' }}
sx={{ p: 1.5, borderBottom: '1px solid', borderColor: 'divider', ...sx }}
>
{onSearch && (
<TextField
size="small"
value={search}
onChange={(e) => onSearch(e.target.value)}
placeholder={searchPlaceholder}
sx={{ minWidth: { xs: '100%', md: 280 } }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchOutlinedIcon fontSize="small" /></InputAdornment> }}
/>
)}
{filters.map((f) => (
<TextField
key={f.label}
select
size="small"
label={f.label}
value={f.value}
onChange={(e) => f.onChange(e.target.value)}
sx={{ minWidth: { xs: '100%', md: f.width || 160 } }}
>
{f.options.map((o) => (
<MenuItem key={o.value} value={o.value}>{o.label}</MenuItem>
))}
</TextField>
))}
<Box sx={{ flexGrow: 1 }} />
{actions}
</Stack>
);
}

View File

@@ -0,0 +1,85 @@
import { Card, Box, Stack, Typography, LinearProgress } from '@mui/material';
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
import VerifiedUserOutlinedIcon from '@mui/icons-material/VerifiedUserOutlined';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import { commandPipeline } from '@/data/mock';
// ==============================|| END-TO-END PROCESS TRACKER ||============================== //
// Premium vertical timeline: connected progress rail, status-coloured nodes, completion %,
// KPI badges, hover interactions.
const ICON = {
order: ReceiptLongOutlinedIcon, trust: VerifiedUserOutlinedIcon, hub: HubOutlinedIcon, fleet: LocalShippingOutlinedIcon,
ai: AutoAwesomeOutlinedIcon, rider: TwoWheelerOutlinedIcon, track: MyLocationOutlinedIcon, done: CheckRoundedIcon
};
const TONE = {
done: { node: '#00A854', ring: 'rgba(0,168,84,0.16)', bar: 'success' },
active: { node: '#C01227', ring: 'rgba(192,18,39,0.16)', bar: 'primary' },
pending: { node: '#C7CDD6', ring: 'rgba(140,148,162,0.16)', bar: 'inherit' }
};
export default function ProcessTracker({ steps = commandPipeline, title = 'End-to-End Flow' }) {
return (
<Card sx={{ height: '100%' }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ px: 2.5, py: 1.75, borderBottom: '1px solid', borderColor: 'grey.100' }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}>{title}</Typography>
<Stack direction="row" spacing={0.75} alignItems="center">
<Box className="live-pulse" sx={{ color: 'success.main' }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.600' }}>LIVE</Typography>
</Stack>
</Stack>
<Box sx={{ p: 2.5, pt: 1.5 }}>
{steps.map((s, i) => {
const Icon = s.status === 'done' ? CheckRoundedIcon : ICON[s.icon] || ReceiptLongOutlinedIcon;
const tone = TONE[s.status] || TONE.pending;
const last = i === steps.length - 1;
const nextDone = !last && steps[i + 1] && steps[i].status === 'done';
return (
<Stack
key={s.key}
direction="row"
spacing={1.75}
sx={{ minHeight: last ? 'auto' : 58, borderRadius: 1.5, px: 1, mx: -1, transition: 'background .12s', '&:hover': { bgcolor: 'grey.50' } }}
>
{/* rail */}
<Stack alignItems="center" sx={{ width: 30, pt: 0.75 }}>
<Box
sx={{
width: 30, height: 30, borderRadius: '50%', flexShrink: 0,
bgcolor: tone.node, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: `0 0 0 4px ${tone.ring}`
}}
>
<Icon sx={{ fontSize: 16 }} />
</Box>
{!last && <Box sx={{ flexGrow: 1, width: 2.5, my: 0.5, minHeight: 18, borderRadius: 2, bgcolor: nextDone ? 'success.main' : 'grey.200' }} />}
</Stack>
{/* content */}
<Box sx={{ flexGrow: 1, minWidth: 0, pt: 0.5, pb: last ? 0.5 : 1.25 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.900' }} noWrap>{s.label}</Typography>
<Box sx={{ flexShrink: 0, px: 0.9, py: 0.15, borderRadius: 1, bgcolor: 'grey.100' }}>
<Typography variant="caption" sx={{ fontWeight: 800, color: 'grey.800' }}>{s.count}</Typography>
<Typography component="span" variant="caption" color="text.secondary" sx={{ ml: 0.4 }}>{s.sub}</Typography>
</Box>
</Stack>
<Stack direction="row" alignItems="center" spacing={1} sx={{ mt: 0.6 }}>
<LinearProgress variant="determinate" value={s.pct} color={tone.bar === 'inherit' ? 'inherit' : tone.bar} sx={{ flexGrow: 1, height: 5, borderRadius: 3, bgcolor: 'grey.100', ...(tone.bar === 'inherit' && { '& .MuiLinearProgress-bar': { bgcolor: 'grey.400' } }) }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.600', minWidth: 30, textAlign: 'right' }}>{s.pct}%</Typography>
</Stack>
</Box>
</Stack>
);
})}
</Box>
</Card>
);
}

View File

@@ -0,0 +1,41 @@
import { useSearchParams } from 'react-router-dom';
import { Box, Tabs, Tab } from '@mui/material';
// ==============================|| TABBED WORKSPACE ||============================== //
// Merges several existing pages into one workspace under a single nav item. The active tab is
// synced to the `?tab=` query param (deep-linkable, keeps the sidebar item active) and only the
// active panel is mounted — so heavy panels (e.g. the live map) don't run in the background.
// tabs: [{ key, label, element }].
export default function TabbedWorkspace({ tabs }) {
const [params, setParams] = useSearchParams();
const keys = tabs.map((t) => t.key);
const idx = Math.max(0, keys.indexOf(params.get('tab')));
const setIdx = (i) =>
setParams(
(prev) => {
const next = new URLSearchParams(prev);
next.set('tab', keys[i]);
return next;
},
{ replace: true }
);
return (
<Box>
<Tabs
value={idx}
onChange={(_, v) => setIdx(v)}
variant="scrollable"
scrollButtons="auto"
sx={{ mb: 2, borderBottom: 1, borderColor: 'divider', '& .MuiTab-root': { textTransform: 'none', fontWeight: 600, fontSize: '0.9rem' } }}
>
{tabs.map((t) => (
<Tab key={t.key} label={t.label} />
))}
</Tabs>
<Box>{tabs[idx].element}</Box>
</Box>
);
}

View File

@@ -0,0 +1,118 @@
import { useMemo, useState } from 'react';
import { Card, Box, Stack, Typography, Chip, LinearProgress } from '@mui/material';
import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
import './tracking.css';
import { vehicleTypes, vehicleIconComponents } from './vehicleMarker';
// ==============================|| ACTIVE RIDERS LIST ||============================== //
// Right-rail fleet roster — every active rider in one compact, filterable, scrollable list so a
// dispatcher can switch focus instantly without leaving the map. Selecting drives map + timeline.
const liveOf = (d) => (d.status === 'Delivered' ? 'Completed' : d.etaStatus !== 'on-time' ? 'Delayed' : 'Live');
const PILL = {
Live: { bg: '#E3F6EC', fg: '#00773B' },
Delayed: { bg: '#FEEAE9', fg: '#A82216' },
Completed: { bg: '#EEF1F5', fg: '#475569' }
};
const FILTERS = [
{ key: 'all', label: 'All', test: () => true },
{ key: 'live', label: 'Live', test: (d) => liveOf(d) === 'Live' },
{ key: 'delayed', label: 'Delayed', test: (d) => liveOf(d) === 'Delayed' },
{ key: 'completed', label: 'Completed', test: (d) => liveOf(d) === 'Completed' },
{ key: 'priority', label: 'High Priority', test: (d) => d.priority === 'high' || d.priority === 'express' }
];
function RiderCard({ d, selected, onSelect }) {
const vt = vehicleTypes[d.vehicle] || vehicleTypes.Bike;
const Glyph = vehicleIconComponents[d.vehicle];
const state = liveOf(d);
const pill = PILL[state];
const barColor = state === 'Completed' ? 'success' : state === 'Delayed' ? 'error' : 'info';
return (
<Box
onClick={() => onSelect(d.id)}
sx={{
p: 1.25,
borderRadius: 2,
cursor: 'pointer',
border: '1px solid',
borderColor: selected ? 'primary.main' : 'grey.200',
bgcolor: selected ? 'rgba(192,18,39,0.04)' : 'background.paper',
transition: 'border-color .12s, background .12s',
'&:hover': { borderColor: 'primary.light', bgcolor: 'grey.50' }
}}
>
<Stack direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 28, height: 28, borderRadius: 1.5, bgcolor: vt.color, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
{Glyph ? <Glyph sx={{ fontSize: 16 }} /> : null}
</Box>
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={0.5}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main', lineHeight: 1.1 }}>{d.id}</Typography>
<Box component="span" sx={{ px: 0.75, py: 0.15, borderRadius: 1, bgcolor: pill.bg, color: pill.fg, fontSize: 10, fontWeight: 800, flexShrink: 0 }}>{state}</Box>
</Stack>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{d.rider} · {d.vehicle}</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 0.75 }}>
<LinearProgress variant="determinate" value={d.progress} color={barColor} sx={{ flexGrow: 1, height: 5, borderRadius: 3, bgcolor: 'grey.100' }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.600', minWidth: 30, textAlign: 'right' }}>{d.progress}%</Typography>
<Stack direction="row" spacing={0.25} alignItems="center" sx={{ flexShrink: 0, color: pill.fg }}>
<ScheduleOutlinedIcon sx={{ fontSize: 13 }} />
<Typography variant="caption" sx={{ fontWeight: 700 }}>{d.eta}</Typography>
</Stack>
</Stack>
</Box>
);
}
export default function ActiveRidersList({ deliveries, selectedId, onSelect }) {
const [filter, setFilter] = useState('all');
const active = FILTERS.find((f) => f.key === filter) || FILTERS[0];
const rows = useMemo(
() => deliveries.filter(active.test).sort((a, b) => Number(liveOf(b) === 'Delayed') - Number(liveOf(a) === 'Delayed')),
[deliveries, active]
);
const count = (f) => deliveries.filter(f.test).length;
return (
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box sx={{ px: 2, pt: 1.75, pb: 1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 700 }}>Active Riders</Typography>
<Typography variant="caption" color="text.secondary">{rows.length} shown</Typography>
</Stack>
<Stack direction="row" spacing={0.75} sx={{ flexWrap: 'wrap', gap: 0.75 }}>
{FILTERS.map((f) => (
<Chip
key={f.key}
size="small"
label={`${f.label} ${count(f)}`}
onClick={() => setFilter(f.key)}
variant={filter === f.key ? 'filled' : 'outlined'}
sx={{
height: 24, fontWeight: 600, cursor: 'pointer',
...(filter === f.key
? { bgcolor: 'primary.main', color: '#fff', '&:hover': { bgcolor: 'primary.dark' } }
: { borderColor: 'grey.300', color: 'grey.600' })
}}
/>
))}
</Stack>
</Box>
<Box sx={{ flexGrow: 1, minHeight: 0, overflowY: 'auto', px: 2, pb: 1.5 }}>
<Stack spacing={1}>
{rows.map((d) => (
<RiderCard key={d.id} d={d} selected={d.id === selectedId} onSelect={onSelect} />
))}
{rows.length === 0 && <Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 3 }}>No riders match this filter.</Typography>}
</Stack>
</Box>
</Card>
);
}

View File

@@ -0,0 +1,188 @@
import { useMemo, useState } from 'react';
import { Card, Box, Stack, Typography, Tabs, Tab, TextField, InputAdornment, LinearProgress, Chip, MenuItem, IconButton, Button, Tooltip } from '@mui/material';
import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined';
import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded';
import './tracking.css';
import StatusChip from '@/components/StatusChip';
import { vehicleTypes } from '@/data/mock';
import { vehicleIconComponents } from './vehicleMarker';
// ==============================|| OPERATIONAL DELIVERY QUEUE ||============================== //
// Left panel of the control tower: tabbed, searchable, city-filterable list of shipment cards.
// Selecting drives the map; per-card quick actions (Flag / Open 360) commit via `actions`.
const ETA_TONE = { 'on-time': '#00773B', 'at-risk': '#8A6500', delayed: '#A82216' };
const PRIORITY = { high: { fg: '#A82216', bg: '#FEEAE9' }, express: { fg: '#8A6500', bg: '#FFF7E0' }, standard: { fg: '#595959', bg: '#F0F0F0' } };
const matches = (d, q) =>
[d.id, d.rider, d.vehicle, d.origin, d.destination, d.city].some((f) => f.toLowerCase().includes(q));
function DeliveryCard({ d, selected, onSelect, actions }) {
const tone = ETA_TONE[d.etaStatus] || ETA_TONE['on-time'];
const vt = vehicleTypes[d.vehicle] || vehicleTypes.Bike;
const pr = PRIORITY[d.priority] || PRIORITY.standard;
const live = d.status !== 'Delivered';
const stop = (fn) => (e) => { e.stopPropagation(); fn(); };
return (
<Box
onClick={() => onSelect(d.id)}
sx={{
p: 1.5,
borderRadius: 2,
cursor: 'pointer',
border: '1px solid',
borderColor: selected ? 'primary.main' : 'grey.200',
borderLeft: '4px solid',
borderLeftColor: d.etaStatus === 'on-time' ? 'transparent' : tone,
bgcolor: selected ? 'rgba(192,18,39,0.03)' : 'background.paper',
transition: 'border-color .15s, background .15s',
'&:hover': { borderColor: 'primary.light' }
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
<Stack direction="row" spacing={1} alignItems="center" sx={{ minWidth: 0 }}>
<Box sx={{ width: 30, height: 30, borderRadius: 1.5, bgcolor: vt.color, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
{(() => { const G = vehicleIconComponents[d.vehicle]; return G ? <G sx={{ fontSize: 17 }} /> : null; })()}
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main', lineHeight: 1.1 }}>{d.id}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>{d.rider} · {d.vehicle}</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ flexShrink: 0 }}>
<Box component="span" sx={{ px: 0.75, py: 0.25, borderRadius: 1, bgcolor: pr.bg, color: pr.fg, fontSize: 10, fontWeight: 700, textTransform: 'uppercase' }}>
{d.priority}
</Box>
{live && (
<Chip
size="small"
icon={<Box className="live-pulse" sx={{ color: '#00A854', width: 7, height: 7, ml: 0.5 }} />}
label="Live"
sx={{ height: 20, bgcolor: 'success.lighter', color: 'success.dark', fontWeight: 700, '& .MuiChip-label': { px: 0.75, fontSize: 11 } }}
/>
)}
</Stack>
</Stack>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>{d.origin}</Typography>
<ArrowRightAltRoundedIcon sx={{ fontSize: 16, color: 'grey.400', flexShrink: 0 }} />
<Typography variant="caption" sx={{ fontWeight: 600 }} noWrap>{d.destination}</Typography>
</Stack>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 0.75 }}>
<LinearProgress
variant="determinate"
value={d.progress}
sx={{ flexGrow: 1, height: 6, borderRadius: 3, bgcolor: 'grey.100', '& .MuiLinearProgress-bar': { bgcolor: d.status === 'Delivered' ? 'success.main' : d.etaStatus === 'on-time' ? 'info.main' : tone } }}
/>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', minWidth: 34, textAlign: 'right' }}>{d.progress}%</Typography>
</Stack>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mt: 1 }}>
<StatusChip status={d.status} />
<Stack direction="row" spacing={0.5} alignItems="center">
<ScheduleOutlinedIcon sx={{ fontSize: 14, color: tone }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: tone }}>
{d.status === 'Delivered' ? `Delivered ${d.eta}` : `ETA ${d.eta}`}
{d.etaStatus !== 'on-time' && ` · +${d.delayMin}m`}
</Typography>
</Stack>
</Stack>
{/* quick actions */}
{actions && (
<Stack direction="row" spacing={0.5} alignItems="center" justifyContent="flex-end" sx={{ mt: 0.75 }}>
<Tooltip title="Flag exception">
<IconButton size="small" color="error" onClick={stop(() => actions.flag(d.id))} sx={{ p: 0.5 }}>
<FlagOutlinedIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
<Button size="small" endIcon={<OpenInNewRoundedIcon sx={{ fontSize: 13 }} />} onClick={stop(() => actions.open(d.id))} sx={{ fontSize: '0.7rem', fontWeight: 600, px: 0.75, minWidth: 0 }}>
360
</Button>
</Stack>
)}
</Box>
);
}
const TABS = [
{ key: 'active', label: 'Active', test: (d) => d.status !== 'Delivered' },
{ key: 'delayed', label: 'Delayed', test: (d) => d.etaStatus !== 'on-time' },
{ key: 'completed', label: 'Completed', test: (d) => d.status === 'Delivered' },
{ key: 'all', label: 'All', test: () => true }
];
export default function DeliveryQueue({ deliveries, selectedId, onSelect, actions }) {
const [tab, setTab] = useState(0);
const [q, setQ] = useState('');
const [city, setCity] = useState('all');
const cities = useMemo(() => [...new Set(deliveries.map((d) => d.city))], [deliveries]);
const rows = useMemo(() => {
const query = q.trim().toLowerCase();
return deliveries
.filter(TABS[tab].test)
.filter((d) => (city === 'all' ? true : d.city === city))
.filter((d) => (query ? matches(d, query) : true))
.sort((a, b) => Number(b.etaStatus !== 'on-time') - Number(a.etaStatus !== 'on-time'));
}, [deliveries, tab, q, city]);
const counts = useMemo(() => TABS.map((t) => deliveries.filter(t.test).length), [deliveries]);
return (
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box sx={{ px: 2, pt: 2, pb: 1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="h5" sx={{ fontWeight: 700 }}>Active Deliveries</Typography>
<Typography variant="caption" color="text.secondary">{rows.length} shown</Typography>
</Stack>
<Stack direction="row" spacing={1} sx={{ mt: 1.5 }}>
<TextField
fullWidth
size="small"
placeholder="Search shipment, rider, location…"
value={q}
onChange={(e) => setQ(e.target.value)}
InputProps={{ startAdornment: (<InputAdornment position="start"><SearchOutlinedIcon fontSize="small" /></InputAdornment>) }}
/>
<TextField select size="small" value={city} onChange={(e) => setCity(e.target.value)} sx={{ minWidth: 120 }}>
<MenuItem value="all">All cities</MenuItem>
{cities.map((c) => <MenuItem key={c} value={c}>{c}</MenuItem>)}
</TextField>
</Stack>
</Box>
<Tabs
value={tab}
onChange={(_, v) => setTab(v)}
variant="fullWidth"
sx={{ px: 1, minHeight: 40, '& .MuiTab-root': { minHeight: 40, textTransform: 'none', fontWeight: 600, fontSize: 13 } }}
>
{TABS.map((t, i) => (
<Tab key={t.key} label={`${t.label} ${counts[i]}`} />
))}
</Tabs>
<Box sx={{ flexGrow: 1, minHeight: 0, overflowY: 'auto', px: 2, py: 1.5 }}>
<Stack spacing={1.25}>
{rows.map((d) => (
<DeliveryCard key={d.id} d={d} selected={d.id === selectedId} onSelect={onSelect} actions={actions} />
))}
{rows.length === 0 && (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
No deliveries match this view.
</Typography>
)}
</Stack>
</Box>
</Card>
);
}

View File

@@ -0,0 +1,366 @@
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet.markercluster';
import { Box, Card, Stack, Typography, Button, IconButton, Tooltip, LinearProgress, Collapse } from '@mui/material';
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
import CallOutlinedIcon from '@mui/icons-material/CallOutlined';
import ChatBubbleOutlineRoundedIcon from '@mui/icons-material/ChatBubbleOutlineRounded';
import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded';
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded';
import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import './tracking.css';
import { vehicleIcon, endpointIcon, clusterIcon, vehicleIconComponents } from './vehicleMarker';
import { pointAlongRoute } from '@/utils/geo';
import { vehicleTypes, fleetTotals, cityCenters, networkCorridors } from '@/data/mock';
const ll = (p) => (Array.isArray(p) ? p : [p.lat, p.lng]);
// ---- inter-city corridors: gentle arcs between the three metros + active-load chips ----
function arc(a, b, bend = 0.16) {
const dx = b[0] - a[0];
const dy = b[1] - a[1];
const ctrl = [(a[0] + b[0]) / 2 - dy * bend, (a[1] + b[1]) / 2 + dx * bend];
const pts = [];
for (let i = 0; i <= 18; i += 1) {
const t = i / 18;
const u = 1 - t;
pts.push([u * u * a[0] + 2 * u * t * ctrl[0] + t * t * b[0], u * u * a[1] + 2 * u * t * ctrl[1] + t * t * b[1]]);
}
const mid = [0.25 * a[0] + 0.5 * ctrl[0] + 0.25 * b[0], 0.25 * a[1] + 0.5 * ctrl[1] + 0.25 * b[1]];
return { pts, mid };
}
const laneChip = (count, from, to) =>
L.divIcon({
html: `<div class="lane-chip"><span class="lane-chip-n">${count}</span><span class="lane-chip-l">${from}${to}</span></div>`,
className: 'lane-chip-divicon',
iconSize: [0, 0]
});
// Precomputed once (cities & counts are static) so the live re-render never recomputes geometry.
const CORRIDORS = networkCorridors.map((c) => {
const { pts, mid } = arc([cityCenters[c.from].lat, cityCenters[c.from].lng], [cityCenters[c.to].lat, cityCenters[c.to].lng]);
return { ...c, pts, mid, chip: laneChip(c.active, c.from.slice(0, 3).toUpperCase(), c.to.slice(0, 3).toUpperCase()) };
});
function NetworkCorridors() {
return (
<>
{CORRIDORS.map((c) => (
<Fragment key={`${c.from}-${c.to}`}>
<Polyline positions={c.pts} pathOptions={{ color: '#94A3B8', weight: 1.5, opacity: 0.5, dashArray: '2 7', lineCap: 'round' }} interactive={false} />
<Marker position={c.mid} icon={c.chip} interactive={false} keyboard={false} />
</Fragment>
))}
</>
);
}
// ---- ambient fleet rendered through a Leaflet marker-cluster group ----
function FleetClusters({ vehicles }) {
const map = useMap();
useEffect(() => {
const group = L.markerClusterGroup({
iconCreateFunction: (c) => clusterIcon(c.getChildCount()),
showCoverageOnHover: false,
maxClusterRadius: 55,
spiderfyOnMaxZoom: true
});
vehicles.forEach((v) => {
const m = L.marker([v.lat, v.lng], { icon: vehicleIcon({ type: v.type, bearing: v.bearing }) });
m.bindPopup(`<strong>${v.id}</strong><br/>${v.type} · ${v.rider}<br/>${v.status === 'idle' ? 'Idle' : 'On trip'}`);
group.addLayer(m);
});
map.addLayer(group);
return () => map.removeLayer(group);
}, [map, vehicles]);
return null;
}
// ---- imperative effects: size correctly, frame the three cities, follow the selection ----
// Refit only when the *selection* changes (not on every live-progress tick) so the map
// doesn't constantly re-zoom while vehicles move.
function MapEffects({ selectedId, selectedRoute, selectedPoint, homeBounds }) {
const map = useMap();
const routeRef = useRef(selectedRoute);
routeRef.current = selectedRoute;
const pointRef = useRef(selectedPoint);
pointRef.current = selectedPoint;
const prevSel = useRef(null);
const mounted = useRef(false);
useEffect(() => {
// The map mounts inside a flex container that may size after init — settle the size,
// then perform a single gentle onboarding glide from the wide framing into the network.
map.invalidateSize();
map.flyToBounds(homeBounds, { padding: [50, 50], duration: 1.6, easeLinearity: 0.25 });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!mounted.current) { mounted.current = true; prevSel.current = selectedId; return; }
map.invalidateSize();
if (selectedId) {
if (prevSel.current && prevSel.current !== selectedId && pointRef.current) {
// switching between riders → keep the current zoom, just recenter (fleet-monitor UX)
map.setView(pointRef.current, map.getZoom(), { animate: true });
} else if (routeRef.current?.length) {
// first focus from the network view → frame the route once
map.fitBounds(routeRef.current, { padding: [60, 60], maxZoom: 14, duration: 0.8 });
}
} else {
map.flyToBounds(homeBounds, { padding: [50, 50], duration: 0.9 });
}
prevSel.current = selectedId;
}, [selectedId, map]);
return null;
}
export default function FleetMap({ vehicles, deliveries, selectedId, onSelect, lastUpdated, actions }) {
const [map, setMap] = useState(null);
const [expanded, setExpanded] = useState(false);
useEffect(() => { setExpanded(false); }, [selectedId]); // new rider opens compact
const tracked = useMemo(() => deliveries.filter((d) => d.route?.length), [deliveries]);
const selected = useMemo(() => deliveries.find((d) => d.id === selectedId) || null, [deliveries, selectedId]);
const focus = Boolean(selected); // Selected Tracking Mode
const selPos = useMemo(() => (selected ? pointAlongRoute(selected.route, selected.progressExact ?? selected.progress) : null), [selected]);
// Cache vehicle icons by a coarse signature so the live animation only calls setLatLng
// (smooth glide) instead of rebuilding the marker DOM every frame. Bearing is bucketed so
// the icon stays stable along straight stretches.
const iconCache = useRef(new Map());
const getVehicleIcon = (d, bearing, isSel) => {
const delayed = d.etaStatus !== 'on-time';
const bucket = Math.round(bearing / 6) * 6;
const key = `${d.vehicle}|${isSel}|${delayed}|${bucket}`;
let icon = iconCache.current.get(key);
if (!icon) {
icon = vehicleIcon({ type: d.vehicle, bearing: bucket, selected: isSel, delayed });
iconCache.current.set(key, icon);
}
return icon;
};
// The service area is exactly the three cities — used for the initial fit, the recenter
// control and the pan limits, so the operator never loses context.
const homeBounds = useMemo(() => L.latLngBounds(Object.values(cityCenters).map((c) => [c.lat, c.lng])), []);
const introBounds = useMemo(() => homeBounds.pad(0.35), [homeBounds]); // slightly wider first frame for the glide
const maxBounds = useMemo(() => homeBounds.pad(0.5), [homeBounds]);
const recenter = () => map && map.fitBounds(homeBounds, { padding: [50, 50] });
const trackRider = () => { if (map && selPos?.point) map.flyTo(selPos.point, Math.max(map.getZoom(), 14), { duration: 0.8 }); };
const legend = [
{ label: 'Bike', type: 'Bike', color: vehicleTypes.Bike.color, count: fleetTotals.Bike },
{ label: 'Auto', type: 'Auto', color: vehicleTypes.Auto.color, count: fleetTotals.Auto },
{ label: 'Truck', type: 'Truck', color: vehicleTypes.Truck.color, count: fleetTotals.Truck },
{ label: 'Van', type: 'Van', color: vehicleTypes.Van.color, count: fleetTotals.Van },
{ label: 'Delayed', color: '#F04134', count: fleetTotals.delayed }
];
return (
<Box sx={{ position: 'relative', height: '100%', width: '100%', borderRadius: 2, overflow: 'hidden', border: '1px solid', borderColor: 'grey.200' }}>
<MapContainer
ref={setMap}
bounds={introBounds}
boundsOptions={{ padding: [40, 40] }}
minZoom={5}
maxZoom={15}
maxBounds={maxBounds}
maxBoundsViscosity={0.85}
zoomControl
scrollWheelZoom
zoomSnap={0.5}
zoomDelta={0.5}
wheelPxPerZoomLevel={140}
wheelDebounceTime={45}
bounceAtZoomLimits={false}
style={{ height: '100%', width: '100%' }}
>
<TileLayer
attribution='&copy; OpenStreetMap &copy; CARTO'
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
/>
<MapEffects selectedId={selectedId} selectedRoute={selected?.route} selectedPoint={selPos?.point} homeBounds={homeBounds} />
{/* Selected Tracking Mode: hide the ambient network so only the chosen shipment remains */}
{!focus && <NetworkCorridors />}
{!focus && <FleetClusters vehicles={vehicles} />}
{/* selected route: white casing → completed (green) under remaining (blue), clean round caps */}
{selected && selPos && (
<>
<Polyline positions={selected.route} pathOptions={{ color: '#ffffff', weight: 7, opacity: 0.9, lineCap: 'round', lineJoin: 'round' }} />
<Polyline positions={selPos.remaining} pathOptions={{ color: '#2563EB', weight: 3.5, opacity: 0.9, lineCap: 'round', lineJoin: 'round' }} />
<Polyline positions={selPos.completed} pathOptions={{ color: '#16A34A', weight: 3.5, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }} />
<Marker position={ll(selected.pickup)} icon={endpointIcon('#16A34A')} />
<Marker position={ll(selected.drop)} icon={endpointIcon('#EF4444')} />
</>
)}
{/* tracked shipment vehicles (always individually visible & clickable) */}
{tracked.map((d) => {
const pos = pointAlongRoute(d.route, d.progressExact ?? d.progress);
return (
<Marker
key={d.id}
position={pos.point}
icon={getVehicleIcon(d, pos.bearing, d.id === selectedId)}
opacity={focus && d.id !== selectedId ? 0.7 : 1}
eventHandlers={{ click: () => onSelect(d.id) }}
zIndexOffset={d.id === selectedId ? 1000 : 500}
/>
);
})}
</MapContainer>
{/* live status — top-left */}
<Stack
direction="row"
spacing={0.75}
alignItems="center"
sx={{ position: 'absolute', top: 12, left: 52, zIndex: 1000, bgcolor: 'rgba(255,255,255,0.94)', px: 1.25, py: 0.5, borderRadius: 1.5, boxShadow: 1 }}
>
<Box className="live-pulse" sx={{ color: 'success.main' }} />
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.800' }}>LIVE</Typography>
<Typography variant="caption" color="text.secondary">· updated {lastUpdated}</Typography>
</Stack>
{/* recenter control — top-right (below default zoom on left) */}
<Tooltip title="Center on fleet" placement="left">
<IconButton
onClick={recenter}
size="small"
sx={{ position: 'absolute', top: 12, right: 12, zIndex: 1000, bgcolor: 'rgba(255,255,255,0.94)', boxShadow: 1, '&:hover': { bgcolor: '#fff' } }}
>
<MyLocationOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
{/* legend — bottom-right (hidden in Selected Tracking Mode to reduce clutter) */}
{!focus && (
<Card sx={{ position: 'absolute', bottom: 24, right: 12, zIndex: 1000, px: 1.5, py: 1, boxShadow: 2 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.700', display: 'block', mb: 0.5 }}>Fleet on map</Typography>
<Stack spacing={0.5}>
{legend.map((l) => {
const Glyph = l.type ? vehicleIconComponents[l.type] : null;
return (
<Stack key={l.label} direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 20, display: 'flex', justifyContent: 'center' }}>
{Glyph ? (
<Glyph sx={{ fontSize: 16, color: l.color }} />
) : (
<Box sx={{ width: 11, height: 11, borderRadius: '50%', bgcolor: l.color }} />
)}
</Box>
<Typography variant="caption" sx={{ color: 'grey.700', minWidth: 46 }}>{l.label}</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: l.color }}>{l.count}</Typography>
</Stack>
);
})}
</Stack>
</Card>
)}
{/* compact, expandable tracking card — top-left, never covers the route */}
{selected && (
<Card
sx={{
position: 'absolute',
top: 76,
left: 12,
zIndex: 1000,
width: { xs: 244, sm: 288, lg: 304 },
maxWidth: 'calc(100% - 24px)',
boxShadow: 4,
overflow: 'hidden'
}}
>
<Box sx={{ p: 1.5 }}>
{/* identity row */}
<Stack direction="row" alignItems="flex-start" spacing={1}>
<Box sx={{ width: 30, height: 30, borderRadius: 1.5, bgcolor: (vehicleTypes[selected.vehicle] || {}).color, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
{(() => { const G = vehicleIconComponents[selected.vehicle]; return G ? <G sx={{ fontSize: 18 }} /> : null; })()}
</Box>
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Stack direction="row" spacing={0.75} alignItems="center">
<Typography sx={{ fontWeight: 800, color: 'primary.main', fontSize: '0.95rem', lineHeight: 1.1 }} noWrap>{selected.id}</Typography>
<Box className="live-pulse" sx={{ color: 'success.main', width: 7, height: 7, flexShrink: 0 }} />
</Stack>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{selected.rider} · {selected.vehicle}</Typography>
</Box>
<IconButton size="small" onClick={() => onSelect(null)} sx={{ m: -0.5, flexShrink: 0 }} aria-label="Close"><CloseRoundedIcon fontSize="small" /></IconButton>
</Stack>
{/* compact metrics */}
<Stack direction="row" sx={{ mt: 1.25 }}>
{[
{ l: 'Progress', v: `${selected.progress}%`, c: 'grey.900' },
{ l: 'ETA', v: selected.eta, c: 'grey.900' },
{ l: 'Delay', v: selected.etaStatus === 'on-time' ? 'On time' : `+${selected.delayMin} min`, c: selected.etaStatus === 'on-time' ? 'success.dark' : 'error.dark' }
].map((m) => (
<Box key={m.l} sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', lineHeight: 1.2 }}>{m.l}</Typography>
<Typography variant="body2" sx={{ fontWeight: 800, color: m.c }} noWrap>{m.v}</Typography>
</Box>
))}
</Stack>
{/* expanded detail */}
<Collapse in={expanded} timeout={260} unmountOnExit>
<Box sx={{ pt: 1.5, mt: 1.5, borderTop: '1px solid', borderColor: 'grey.100' }}>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>{selected.origin}</Typography>
<ArrowRightAltRoundedIcon sx={{ fontSize: 16, color: 'grey.400', flexShrink: 0 }} />
<Typography variant="caption" sx={{ fontWeight: 700 }} noWrap>{selected.destination}</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={selected.progress}
sx={{ mt: 1.25, height: 7, borderRadius: 4, bgcolor: 'grey.100', '& .MuiLinearProgress-bar': { borderRadius: 4, bgcolor: selected.etaStatus === 'on-time' ? 'info.main' : '#F04134' } }}
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
Last update · {lastUpdated}
</Typography>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 1.25 }}>
<Tooltip title="Call rider">
<IconButton size="small" onClick={() => actions?.call(selected)} sx={{ border: '1px solid', borderColor: 'grey.300' }}><CallOutlinedIcon fontSize="small" /></IconButton>
</Tooltip>
<Tooltip title="Message rider">
<IconButton size="small" onClick={() => actions?.message(selected)} sx={{ border: '1px solid', borderColor: 'grey.300' }}><ChatBubbleOutlineRoundedIcon fontSize="small" /></IconButton>
</Tooltip>
<Box sx={{ flexGrow: 1 }} />
<Tooltip title="Center on rider">
<IconButton size="small" onClick={trackRider} sx={{ border: '1px solid', borderColor: 'grey.300' }}><MyLocationOutlinedIcon fontSize="small" /></IconButton>
</Tooltip>
</Stack>
<Button fullWidth size="small" variant="contained" disableElevation endIcon={<OpenInNewRoundedIcon sx={{ fontSize: 15 }} />} onClick={() => actions?.open(selected.id)} sx={{ mt: 1 }}>
Shipment Details
</Button>
</Box>
</Collapse>
{/* expand / collapse toggle */}
<Button
fullWidth
size="small"
onClick={() => setExpanded((e) => !e)}
endIcon={expanded ? <ExpandLessRoundedIcon /> : <ExpandMoreRoundedIcon />}
sx={{ mt: 1, fontWeight: 700, color: 'text.secondary' }}
>
{expanded ? 'Collapse' : 'Expand'}
</Button>
</Box>
</Card>
)}
</Box>
);
}

View File

@@ -0,0 +1,49 @@
import { useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet';
import L from 'leaflet';
import { Box } from '@mui/material';
import 'leaflet/dist/leaflet.css';
import './tracking.css';
import { vehicleIcon, endpointIcon } from './vehicleMarker';
import { pointAlongRoute } from '@/utils/geo';
// ==============================|| MINI MAP — single-shipment route ||============================== //
// A focused, non-clustered real map for one shipment: route (completed/remaining), pickup/drop pins,
// and the live vehicle marker placed from `progress`. Reuses the same Leaflet layer as FleetMap so the
// fake `MapPlaceholder` grid can be retired wherever a real per-shipment map is needed (Shipment 360 …).
const ll = (p) => (Array.isArray(p) ? p : [p.lat, p.lng]);
function Fit({ route }) {
const map = useMap();
useEffect(() => {
map.invalidateSize();
if (route?.length) map.fitBounds(route, { padding: [30, 30], maxZoom: 14 });
}, [map, route]);
return null;
}
export default function MiniMap({ route = [], progress = 0, vehicle = 'Bike', pickup, drop, height = 300 }) {
const hasRoute = route && route.length > 0;
const pos = hasRoute ? pointAlongRoute(route, progress) : null;
const center = pos ? pos.point : [12.9716, 77.5946];
return (
<Box sx={{ height, width: '100%', borderRadius: 2, overflow: 'hidden', border: '1px solid', borderColor: 'grey.200' }}>
<MapContainer center={center} zoom={12} zoomControl scrollWheelZoom={false} style={{ height: '100%', width: '100%' }}>
<TileLayer attribution="&copy; OpenStreetMap" url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png" />
<Fit route={route} />
{pos && (
<>
<Polyline positions={pos.remaining} pathOptions={{ color: '#1D4ED8', weight: 5, opacity: 0.85 }} />
<Polyline positions={pos.completed} pathOptions={{ color: '#00A854', weight: 5, opacity: 0.95 }} />
<Marker position={pos.point} icon={vehicleIcon({ type: vehicle, bearing: pos.bearing })} />
</>
)}
{pickup && <Marker position={ll(pickup)} icon={endpointIcon('#00A854')} />}
{drop && <Marker position={ll(drop)} icon={endpointIcon('#F04134')} />}
</MapContainer>
</Box>
);
}

View File

@@ -0,0 +1,111 @@
import { Box, Card, Stack, Typography, IconButton, Divider } from '@mui/material';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import PriorityHighRoundedIcon from '@mui/icons-material/PriorityHighRounded';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
import './tracking.css';
import StatusChip from '@/components/StatusChip';
import { vehicleTypes } from '@/data/mock';
import { vehicleIconComponents } from './vehicleMarker';
// ==============================|| RIDER TIMELINE ||============================== //
// Replaces the generic activity feed in Selected Tracking Mode — a focused, Uber/Swiggy-style
// lifecycle for one shipment, derived live from its progress and delay state.
function buildStages(d) {
const p = d.progress;
const delayed = d.etaStatus !== 'on-time';
const stages = [
{ key: 'assigned', label: 'Shipment Assigned', time: '10:02 AM', done: true },
{ key: 'picked', label: 'Picked Up', time: '10:11 AM', done: p >= 5 },
{ key: 'checkpoint', label: 'Reached Checkpoint', time: '10:29 AM', done: p >= 34 }
];
if (delayed) {
stages.push({ key: 'delay', label: 'Delay Detected', time: `+${d.delayMin} min vs SLA`, done: p >= 38, warn: true });
}
stages.push({ key: 'approaching', label: 'Approaching Destination', time: p >= 85 ? 'Arriving now' : `Est. ${d.eta}`, done: p >= 85 });
stages.push({ key: 'delivered', label: 'Delivered', time: d.status === 'Delivered' ? d.eta : `ETA ${d.eta}`, done: p >= 100 });
return stages;
}
export default function RiderTimeline({ delivery: d, onClose }) {
if (!d) return null;
const stages = buildStages(d);
const activeIdx = stages.findIndex((s) => !s.done); // first incomplete = current step
const Glyph = vehicleIconComponents[d.vehicle];
const vc = (vehicleTypes[d.vehicle] || {}).color;
return (
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* header */}
<Box sx={{ px: 2, pt: 2, pb: 1.5 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="h5" sx={{ fontWeight: 700 }}>Rider Timeline</Typography>
<IconButton size="small" onClick={onClose} aria-label="Exit tracking"><CloseRoundedIcon fontSize="small" /></IconButton>
</Stack>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 1.5 }}>
<Box sx={{ width: 32, height: 32, borderRadius: 1.5, bgcolor: vc, color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
{Glyph ? <Glyph sx={{ fontSize: 18 }} /> : null}
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main', lineHeight: 1.15 }}>{d.id}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>{d.rider} · {d.vehicle}</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 1.25, minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>{d.origin}</Typography>
<ArrowRightAltRoundedIcon sx={{ fontSize: 16, color: 'grey.400', flexShrink: 0 }} />
<Typography variant="caption" sx={{ fontWeight: 600 }} noWrap>{d.destination}</Typography>
</Stack>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 1 }}>
<StatusChip status={d.status} />
<Typography variant="caption" sx={{ fontWeight: 700, color: d.etaStatus === 'on-time' ? 'success.dark' : 'error.dark' }}>
{d.progress}% · ETA {d.eta}{d.etaStatus !== 'on-time' ? ` · +${d.delayMin}m` : ''}
</Typography>
</Stack>
</Box>
<Divider />
{/* timeline */}
<Box sx={{ flexGrow: 1, minHeight: 0, overflowY: 'auto', px: 2, py: 2 }}>
{stages.map((s, i) => {
const active = i === activeIdx;
const color = s.warn ? '#B45309' : s.done ? '#16A34A' : active ? '#2563EB' : '#9AA4B2';
const last = i === stages.length - 1;
return (
<Stack key={s.key} direction="row" spacing={1.5} sx={{ minHeight: last ? 'auto' : 52 }}>
<Stack alignItems="center" sx={{ flexShrink: 0 }}>
<Box
className={active ? 'tl-dot tl-dot-active' : 'tl-dot'}
sx={{
width: 22, height: 22, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
bgcolor: s.done || s.warn ? color : '#fff',
border: '2px solid', borderColor: color, color: '#fff', mt: 0.25,
'--tl-c': color
}}
>
{s.done ? <CheckRoundedIcon sx={{ fontSize: 14 }} /> : s.warn ? <PriorityHighRoundedIcon sx={{ fontSize: 14 }} /> : <Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: active ? color : 'grey.300' }} />}
</Box>
{!last && <Box sx={{ flexGrow: 1, width: '2px', my: 0.5, bgcolor: s.done ? '#16A34A' : 'grey.200' }} />}
</Stack>
<Box sx={{ pb: last ? 0 : 1.5, pt: 0.25 }}>
<Typography variant="body2" sx={{ fontWeight: s.done || active ? 700 : 500, color: s.warn ? '#B45309' : s.done || active ? 'text.primary' : 'text.secondary', lineHeight: 1.2 }}>
{s.label}
</Typography>
<Typography variant="caption" sx={{ color: s.warn ? '#B45309' : active ? 'primary.main' : 'text.secondary', fontWeight: active || s.warn ? 600 : 400 }}>
{active && !s.warn ? `In progress · ${s.time}` : s.time}
</Typography>
</Box>
</Stack>
);
})}
</Box>
</Card>
);
}

View File

@@ -0,0 +1,77 @@
import { Card, Box, Stack, Typography, Divider } from '@mui/material';
import './tracking.css';
import { executionOps } from '@/data/mock';
// ==============================|| TRACKING CONTROL BAR ||============================== //
// Operational KPI strip for the control tower — roomy cells with prominent values, live
// indicators and alert tones. Single row on desktop, wraps on small screens.
function Kpi({ glyph, label, value, color, pulse }) {
return (
<Stack
direction="row"
spacing={1.5}
alignItems="center"
sx={{
px: 2.5,
py: 2.75,
minHeight: 108,
flex: 1,
minWidth: 160,
transition: 'background-color .15s ease',
'&:hover': { bgcolor: 'grey.50' }
}}
>
{pulse ? (
<Box className="live-pulse" sx={{ color, flexShrink: 0, width: 11, height: 11 }} />
) : (
<Box component="span" sx={{ fontSize: 20, flexShrink: 0, lineHeight: 1 }}>{glyph}</Box>
)}
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontSize: '0.875rem', fontWeight: 600, color: 'text.secondary', lineHeight: 1.2, mb: 0.5, display: 'block' }} noWrap>
{label}
</Typography>
<Typography sx={{ fontSize: '2rem', fontWeight: 800, lineHeight: 1, color: color || 'grey.900' }}>
{value}
</Typography>
</Box>
</Stack>
);
}
export default function TrackingControlBar() {
const o = executionOps;
const items = [
{ label: 'Active Deliveries', value: o.activeDeliveries, color: '#00A854', pulse: true },
{ label: 'Delayed', value: o.delayed, color: '#F04134', pulse: true },
{ label: 'On-Time Rate', value: `${o.onTime}%`, color: '#00773B', glyph: '🟢' },
{ label: 'Active Riders', value: o.activeRiders, glyph: '🛵' },
{ label: 'Re-routes Today', value: o.reroutes, glyph: '🔄' },
{ label: 'Critical Alerts', value: o.criticalAlerts, color: '#B38600', glyph: '⚠️' }
];
return (
<Card
sx={{
mb: 2,
flexShrink: 0,
borderColor: 'grey.300',
boxShadow: '0 1px 3px rgba(16,24,40,0.06), 0 1px 2px rgba(16,24,40,0.04)',
transition: 'box-shadow .15s ease',
'&:hover': { boxShadow: '0 4px 14px rgba(16,24,40,0.10)' }
}}
>
<Stack
direction="row"
alignItems="stretch"
divider={<Divider orientation="vertical" flexItem />}
sx={{ overflowX: 'auto', flexWrap: { xs: 'wrap', md: 'nowrap' } }}
>
{items.map((it) => (
<Kpi key={it.label} {...it} />
))}
</Stack>
</Card>
);
}

View File

@@ -0,0 +1,196 @@
/* ==============================|| LIVE TRACKING MAP — marker & overlay styles ||============================== */
/* Premium vehicle marker: a crisp white Material silhouette on a coloured disc, with a
direction arrow that orbits to the travel heading. The disc stays upright (icon readable);
only the arrow rotates. */
.vm-divicon { background: none; border: none; }
.vm {
position: relative;
width: 36px;
height: 36px;
transition: transform 0.18s ease;
}
.vm-disc {
position: absolute;
top: 50%;
left: 50%;
width: 26px;
height: 26px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: var(--vc);
border: 2px solid #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.vm-disc svg { display: block; }
.vm-rot {
position: absolute;
inset: 0;
z-index: 1;
transition: transform 0.5s ease-out;
}
.vm-arrow {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 7px solid var(--vc);
opacity: 0.9;
}
/* Selected: a gentle scale + one slow expanding ring (no heavy glow, no gaming pulse). */
.vm-sel { transform: scale(1.16); z-index: 1000; }
.vm-sel .vm-disc { box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9), 0 1px 4px rgba(0, 0, 0, 0.3); }
.vm-sel::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 26px;
height: 26px;
border-radius: 50%;
border: 2px solid var(--vc);
transform: translate(-50%, -50%);
animation: vm-ring 2.8s ease-out infinite;
pointer-events: none;
}
@keyframes vm-ring {
0% { opacity: 0.28; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.75); }
}
.vm-del .vm-disc { box-shadow: 0 0 0 1.5px rgba(240, 65, 52, 0.35), 0 1px 3px rgba(0, 0, 0, 0.24); }
/* Pulsing "live" dot used on KPIs, queue cards and the map endpoint markers. */
.live-pulse {
position: relative;
display: inline-block;
width: 9px;
height: 9px;
border-radius: 50%;
background: currentColor;
}
.live-pulse::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
background: currentColor;
animation: live-pulse-ring 1.6s ease-out infinite;
}
@keyframes live-pulse-ring {
0% { transform: scale(1); opacity: 0.4; }
100% { transform: scale(2.6); opacity: 0; }
}
/* Pickup / drop endpoint nodes — white core with a thick coloured ring */
.ep-divicon { background: none; border: none; }
.ep {
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
border: 4px solid var(--epc);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
/* Cluster bubble — restrained, professional. A solid disc with a thin tinted ring. */
.vcl-divicon { background: none; border: none; }
.vcl {
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #fff;
background: #1D4ED8;
border: 2.5px solid #fff;
border-radius: 50%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22), 0 0 0 3px rgba(29, 78, 216, 0.12);
}
/* Rider-timeline current-step dot — one quiet halo, no gaming pulse */
.tl-dot-active { box-shadow: 0 0 0 0 var(--tl-c); animation: tl-halo 2.2s ease-out infinite; }
@keyframes tl-halo {
0% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.35); }
70% { box-shadow: 0 0 0 7px rgba(37, 99, 235, 0); }
100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); }
}
/* Inter-city corridor load chip — quiet pill at the lane midpoint */
.lane-chip-divicon { background: none; border: none; }
.lane-chip {
transform: translate(-50%, -50%);
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
padding: 1px 7px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid #e2e6ec;
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.1);
}
.lane-chip-n { font-size: 11px; font-weight: 800; color: #334155; line-height: 1; }
.lane-chip-l { font-size: 8.5px; font-weight: 700; letter-spacing: 0.04em; color: #94a3b8; line-height: 1; }
/* Keep Leaflet controls visually aligned with the enterprise UI */
.leaflet-container { font-family: inherit; background: #eef1f5; }
.leaflet-bar { border: none !important; box-shadow: 0 1px 5px rgba(0,0,0,0.15) !important; }
.leaflet-bar a { color: #434343; border-bottom-color: #f0f0f0 !important; }
.leaflet-control-attribution { font-size: 10px; opacity: 0.7; }
/* ===== Hub Network — command-center map ===== */
/* City wordmark watermark behind the hubs */
.city-divicon { background: none; border: none; }
.city-wm {
transform: translate(-50%, -50%);
color: rgba(120, 132, 150, 0.45);
font-weight: 800;
font-size: 15px;
letter-spacing: 0.18em;
text-transform: uppercase;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.95);
white-space: nowrap;
}
/* Hub node: utilization disc + floating label */
.hub-divicon { background: none; border: none; }
.hub { display: flex; align-items: center; gap: 7px; }
.hub-dot {
flex-shrink: 0;
border-radius: 50%;
background: var(--hc);
border: 3px solid #fff;
color: #fff;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(16, 24, 40, 0.32), 0 0 0 4px var(--hh);
}
.hub-label {
display: flex;
flex-direction: column;
line-height: 1.15;
background: rgba(255, 255, 255, 0.97);
border: 1px solid #e6e9ef;
border-radius: 7px;
padding: 2px 8px;
box-shadow: 0 2px 6px rgba(16, 24, 40, 0.14);
}
.hub-label b { font-size: 11px; font-weight: 700; color: #1f2937; white-space: nowrap; }
.hub-label span { font-size: 9.5px; font-weight: 600; color: #8a94a6; white-space: nowrap; }
/* Ambient last-mile fleet dot */
.fleetdot-divicon { background: none; border: none; }
.fleetdot { width: 9px; height: 9px; border-radius: 50%; background: var(--fc); border: 1.5px solid #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.3); }
/* Animated line-haul corridor flow */
.corridor-flow { animation: corridor-dash 0.9s linear infinite; }
@keyframes corridor-dash { to { stroke-dashoffset: -28; } }

View File

@@ -0,0 +1,75 @@
import L from 'leaflet';
import TwoWheelerRoundedIcon from '@mui/icons-material/TwoWheelerRounded';
import ElectricRickshawRoundedIcon from '@mui/icons-material/ElectricRickshawRounded';
import LocalShippingRoundedIcon from '@mui/icons-material/LocalShippingRounded';
import AirportShuttleRoundedIcon from '@mui/icons-material/AirportShuttleRounded';
import { vehicleTypes } from '@/data/mock';
// Re-exported so tracking components can import type tokens from one place.
export { vehicleTypes };
// ==============================|| VEHICLE MARKER FACTORY ||============================== //
// Premium fleet markers: a crisp white Material vehicle silhouette on a coloured disc with a
// direction arrow that rotates to the travel heading. SVG (not emoji) so it renders identically
// everywhere and stays sharp at any zoom.
// Shared type → MUI icon component map (used by the legend, queue & info card for consistency).
export const vehicleIconComponents = {
Bike: TwoWheelerRoundedIcon,
Auto: ElectricRickshawRoundedIcon,
Truck: LocalShippingRoundedIcon,
Van: AirportShuttleRoundedIcon
};
// Matching Material (Rounded) path geometry, used to paint the silhouette inside map markers.
const PATHS = {
Bike: 'M20 11c-.18 0-.36.03-.53.05L17.41 9H19c.55 0 1-.45 1-1v-.38c0-.74-.78-1.23-1.45-.89l-2.28 1.14L13.7 5.3c-.18-.19-.44-.3-.7-.3h-3c-.55 0-1 .45-1 1s.45 1 1 1h2.17c.27 0 .52.11.71.29L14.59 9h-3.35c-.16 0-.31.04-.45.11l-3.14 1.57c-.38.19-.85.12-1.15-.19l-1.2-1.2C5.11 9.11 4.85 9 4.59 9H1c-.55 0-1 .45-1 1s.45 1 1 1h3C1.48 11-.49 13.32.11 15.94c.33 1.45 1.5 2.62 2.95 2.95C5.68 19.49 8 17.52 8 15l1.41 1.41c.38.38.89.59 1.42.59h1.01c.72 0 1.38-.38 1.74-1.01l2.91-5.09 1.01 1.01c-1.13.91-1.76 2.41-1.38 4.05.34 1.44 1.51 2.61 2.95 2.94 2.61.59 4.93-1.39 4.93-3.9 0-2.21-1.79-4-4-4M4 17c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2m16 0c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2',
Auto: 'M21 11.18V9.72c0-.47-.16-.92-.46-1.28L16.6 3.72c-.38-.46-.94-.72-1.54-.72H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h.18C3.6 16.16 4.7 17 6 17s2.4-.84 2.82-2h8.37c.41 1.16 1.51 2 2.82 2 1.66 0 3-1.34 3-3-.01-1.3-.85-2.4-2.01-2.82M18.4 9H16V6.12zM4 5h3v4H3V6c0-.55.45-1 1-1m2 10c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m3-2v-2h2c.55 0 1-.45 1-1s-.45-1-1-1H9V5h4c.55 0 1 .45 1 1v7zm11 2c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1M7 20h4v-2l6 3h-4v2z',
Truck: 'M19.5 8H17V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v9c0 1.1.9 2 2 2 0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3h1c.55 0 1-.45 1-1v-3.33c0-.43-.14-.85-.4-1.2L20.3 8.4c-.19-.25-.49-.4-.8-.4M6 18c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m13.5-8.5 1.96 2.5H17V9.5zM18 18c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1',
Van: 'm22.41 10.41-4.83-4.83c-.37-.37-.88-.58-1.41-.58H3c-1.1 0-2 .89-2 2v7c0 1.1.9 2 2 2 0 1.66 1.34 3 3 3s3-1.34 3-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3c1.1 0 2-.9 2-2v-2.17c0-.53-.21-1.04-.59-1.42M3 10V8c0-.55.45-1 1-1h3v4H4c-.55 0-1-.45-1-1m3 7.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25M13 11H9V7h4zm5 6.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25M15 11V7h1l4 4z'
};
const svgCache = new Map();
function vehicleSvg(type, color, size) {
const k = `${type}|${color}|${size}`;
if (svgCache.has(k)) return svgCache.get(k);
const d = PATHS[type] || PATHS.Bike;
const svg = `<svg viewBox="0 0 24 24" width="${size}" height="${size}" fill="${color}" aria-hidden="true"><path d="${d}"/></svg>`;
svgCache.set(k, svg);
return svg;
}
export function vehicleIcon({ type, bearing = 0, selected = false, delayed = false }) {
const cfg = vehicleTypes[type] || vehicleTypes.Bike;
const color = delayed ? '#F04134' : cfg.color;
const cls = ['vm', selected ? 'vm-sel' : '', delayed ? 'vm-del' : ''].filter(Boolean).join(' ');
const glyph = vehicleSvg(type, '#fff', selected ? 18 : 15);
const html = `
<div class="${cls}" style="--vc:${color}">
<div class="vm-rot" style="transform: rotate(${bearing}deg)"><i class="vm-arrow"></i></div>
<div class="vm-disc">${glyph}</div>
</div>`;
const size = 36;
return L.divIcon({ html, className: 'vm-divicon', iconSize: [size, size], iconAnchor: [size / 2, size / 2] });
}
export function endpointIcon(color) {
return L.divIcon({
html: `<div class="ep" style="--epc:${color}"></div>`,
className: 'ep-divicon',
iconSize: [18, 18],
iconAnchor: [9, 9]
});
}
export function clusterIcon(count) {
const size = count < 10 ? 36 : count < 50 ? 44 : 52;
return L.divIcon({
html: `<div class="vcl" style="width:${size}px;height:${size}px;font-size:${count < 50 ? 13 : 15}px">${count}</div>`,
className: 'vcl-divicon',
iconSize: [size, size]
});
}

View File

@@ -1,7 +1,14 @@
// ==============================|| DOORMILE - MOCK DATA ||============================== // // ==============================|| DOORMILE - MOCK DATA ||============================== //
// Static demo data powering every screen. // Static demo data powering every screen.
export const locations = ['Bengaluru', 'Mumbai', 'Delhi NCR', 'Hyderabad', 'Chennai', 'Pune']; export const locations = ['Coimbatore', 'Bengaluru', 'Hyderabad'];
// Real operating geography — the three live metros and their coordinates.
export const cityCenters = {
Coimbatore: { lat: 11.0168, lng: 76.9558 },
Bengaluru: { lat: 12.9716, lng: 77.5946 },
Hyderabad: { lat: 17.385, lng: 78.4867 }
};
export const tenantsList = [ export const tenantsList = [
'Freshly Foods', 'Freshly Foods',
@@ -15,12 +22,12 @@ export const tenantsList = [
export const orders = [ export const orders = [
{ id: 'DM-10241', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'HSR Layout, Sec 2', customer: 'Riya Sharma', qty: 3, cod: 0, kms: 4.2, charges: 86, notes: 'Leave at door', status: 'delivered', date: '2026-06-04', payment: 'prepaid' }, { id: 'DM-10241', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'HSR Layout, Sec 2', customer: 'Riya Sharma', qty: 3, cod: 0, kms: 4.2, charges: 86, notes: 'Leave at door', status: 'delivered', date: '2026-06-04', payment: 'prepaid' },
{ id: 'DM-10242', tenant: 'UrbanCart', location: 'Bengaluru', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', customer: 'Karthik Rao', qty: 1, cod: 1299, kms: 12.8, charges: 142, notes: 'Call on arrival', status: 'active', date: '2026-06-05', payment: 'cod' }, { id: 'DM-10242', tenant: 'UrbanCart', location: 'Bengaluru', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', customer: 'Karthik Rao', qty: 1, cod: 1299, kms: 12.8, charges: 142, notes: 'Call on arrival', status: 'active', date: '2026-06-05', payment: 'cod' },
{ id: 'DM-10243', tenant: 'MediQuick Pharma', location: 'Mumbai', pickup: 'Andheri West', drop: 'Bandra East', customer: 'Neha Kulkarni', qty: 2, cod: 0, kms: 6.5, charges: 98, notes: '', status: 'picked', date: '2026-06-05', payment: 'prepaid' }, { id: 'DM-10243', tenant: 'MediQuick Pharma', location: 'Hyderabad', pickup: 'Hitech City Hub', drop: 'Banjara Hills', customer: 'Neha Kulkarni', qty: 2, cod: 0, kms: 6.5, charges: 98, notes: '', status: 'picked', date: '2026-06-05', payment: 'prepaid' },
{ id: 'DM-10244', tenant: 'BloomBox Florists', location: 'Delhi NCR', pickup: 'Connaught Place', drop: 'Saket, Block C', customer: 'Arjun Mehta', qty: 1, cod: 450, kms: 9.1, charges: 110, notes: 'Fragile', status: 'pending', date: '2026-06-05', payment: 'cod' }, { id: 'DM-10244', tenant: 'BloomBox Florists', location: 'Coimbatore', pickup: 'Gandhipuram', drop: 'R.S. Puram', customer: 'Arjun Mehta', qty: 1, cod: 450, kms: 9.1, charges: 110, notes: 'Fragile', status: 'pending', date: '2026-06-05', payment: 'cod' },
{ id: 'DM-10245', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'BTM Layout', customer: 'Sneha Iyer', qty: 4, cod: 0, kms: 3.4, charges: 74, notes: '', status: 'cancelled', date: '2026-06-03', payment: 'prepaid' }, { id: 'DM-10245', tenant: 'Freshly Foods', location: 'Bengaluru', pickup: 'Koramangala Hub', drop: 'BTM Layout', customer: 'Sneha Iyer', qty: 4, cod: 0, kms: 3.4, charges: 74, notes: '', status: 'cancelled', date: '2026-06-03', payment: 'prepaid' },
{ id: 'DM-10246', tenant: 'TechNova Retail', location: 'Hyderabad', pickup: 'Hitech City', drop: 'Gachibowli', customer: 'Vikram Reddy', qty: 1, cod: 4999, kms: 5.6, charges: 120, notes: 'ID required', status: 'created', date: '2026-06-05', payment: 'cod' }, { id: 'DM-10246', tenant: 'TechNova Retail', location: 'Hyderabad', pickup: 'Hitech City', drop: 'Gachibowli', customer: 'Vikram Reddy', qty: 1, cod: 4999, kms: 5.6, charges: 120, notes: 'ID required', status: 'created', date: '2026-06-05', payment: 'cod' },
{ id: 'DM-10247', tenant: 'GreenLeaf Organics', location: 'Pune', pickup: 'Koregaon Park', drop: 'Viman Nagar', customer: 'Pooja Desai', qty: 2, cod: 0, kms: 7.2, charges: 102, notes: '', status: 'delivered', date: '2026-06-04', payment: 'prepaid' }, { id: 'DM-10247', tenant: 'GreenLeaf Organics', location: 'Coimbatore', pickup: 'Peelamedu Hub', drop: 'Saravanampatti', customer: 'Pooja Desai', qty: 2, cod: 0, kms: 7.2, charges: 102, notes: '', status: 'delivered', date: '2026-06-04', payment: 'prepaid' },
{ id: 'DM-10248', tenant: 'UrbanCart', location: 'Chennai', pickup: 'T. Nagar', drop: 'Adyar', customer: 'Suresh Kumar', qty: 1, cod: 799, kms: 8.3, charges: 108, notes: 'Weekend slot', status: 'pending', date: '2026-06-05', payment: 'cod' } { id: 'DM-10248', tenant: 'UrbanCart', location: 'Bengaluru', pickup: 'Jayanagar', drop: 'JP Nagar', customer: 'Suresh Kumar', qty: 1, cod: 799, kms: 8.3, charges: 108, notes: 'Weekend slot', status: 'pending', date: '2026-06-05', payment: 'cod' }
]; ];
export const orderTimeline = [ export const orderTimeline = [
@@ -41,10 +48,10 @@ export const deliveries = orders.map((o, i) => ({
export const riders = [ export const riders = [
{ id: 'RDR-001', userId: 'U1043', name: 'Mohan Das', phone: '+91 98450 11223', address: 'Koramangala, Bengaluru', vehicle: 'Honda Activa', vehicleNo: 'KA-05-HJ-4521', shift: 'Morning', start: '08:00', end: '16:00', fare: 320, fuel: 110, status: 'online', deliveries: 14, rating: 4.8 }, { id: 'RDR-001', userId: 'U1043', name: 'Mohan Das', phone: '+91 98450 11223', address: 'Koramangala, Bengaluru', vehicle: 'Honda Activa', vehicleNo: 'KA-05-HJ-4521', shift: 'Morning', start: '08:00', end: '16:00', fare: 320, fuel: 110, status: 'online', deliveries: 14, rating: 4.8 },
{ id: 'RDR-002', userId: 'U1044', name: 'Imran Sheikh', phone: '+91 98860 44781', address: 'Andheri, Mumbai', vehicle: 'TVS Jupiter', vehicleNo: 'MH-02-CD-9087', shift: 'Evening', start: '14:00', end: '22:00', fare: 280, fuel: 95, status: 'on-delivery', deliveries: 11, rating: 4.6 }, { id: 'RDR-002', userId: 'U1044', name: 'Imran Sheikh', phone: '+91 98860 44781', address: 'Madhapur, Hyderabad', vehicle: 'TVS Jupiter', vehicleNo: 'TS-09-CD-9087', shift: 'Evening', start: '14:00', end: '22:00', fare: 280, fuel: 95, status: 'on-delivery', deliveries: 11, rating: 4.6 },
{ id: 'RDR-003', userId: 'U1045', name: 'Ravi Teja', phone: '+91 99000 22119', address: 'Gachibowli, Hyderabad', vehicle: 'Hero Splendor', vehicleNo: 'TS-09-AB-3344', shift: 'Morning', start: '08:00', end: '16:00', fare: 300, fuel: 120, status: 'online', deliveries: 9, rating: 4.9 }, { id: 'RDR-003', userId: 'U1045', name: 'Ravi Teja', phone: '+91 99000 22119', address: 'Gachibowli, Hyderabad', vehicle: 'Hero Splendor', vehicleNo: 'TS-09-AB-3344', shift: 'Morning', start: '08:00', end: '16:00', fare: 300, fuel: 120, status: 'online', deliveries: 9, rating: 4.9 },
{ id: 'RDR-004', userId: 'U1046', name: 'Sandeep Roy', phone: '+91 90080 55672', address: 'Saket, Delhi', vehicle: 'Bajaj Pulsar', vehicleNo: 'DL-3C-XY-1290', shift: 'Night', start: '22:00', end: '06:00', fare: 350, fuel: 130, status: 'offline', deliveries: 0, rating: 4.4 }, { id: 'RDR-004', userId: 'U1046', name: 'Sandeep Roy', phone: '+91 90080 55672', address: 'Gandhipuram, Coimbatore', vehicle: 'Bajaj Pulsar', vehicleNo: 'TN-37-XY-1290', shift: 'Night', start: '22:00', end: '06:00', fare: 350, fuel: 130, status: 'offline', deliveries: 0, rating: 4.4 },
{ id: 'RDR-005', userId: 'U1047', name: 'Faisal Khan', phone: '+91 97410 88123', address: 'Adyar, Chennai', vehicle: 'Honda Dio', vehicleNo: 'TN-07-PQ-6655', shift: 'Evening', start: '14:00', end: '22:00', fare: 290, fuel: 100, status: 'on-delivery', deliveries: 7, rating: 4.7 } { id: 'RDR-005', userId: 'U1047', name: 'Faisal Khan', phone: '+91 97410 88123', address: 'Peelamedu, Coimbatore', vehicle: 'Honda Dio', vehicleNo: 'TN-37-PQ-6655', shift: 'Evening', start: '14:00', end: '22:00', fare: 290, fuel: 100, status: 'on-delivery', deliveries: 7, rating: 4.7 }
]; ];
export const riderLogs = [ export const riderLogs = [
@@ -56,16 +63,16 @@ export const riderLogs = [
export const customers = [ export const customers = [
{ id: 1, name: 'Riya Sharma', phone: '+91 98111 22334', email: 'riya.sharma@mail.com', address: '24, 1st Cross, HSR Layout', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560102', totalOrders: 18, joined: '2025-03-12' }, { id: 1, name: 'Riya Sharma', phone: '+91 98111 22334', email: 'riya.sharma@mail.com', address: '24, 1st Cross, HSR Layout', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560102', totalOrders: 18, joined: '2025-03-12' },
{ id: 2, name: 'Karthik Rao', phone: '+91 98222 33445', email: 'karthik.rao@mail.com', address: '8, Palm Meadows, Whitefield', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560066', totalOrders: 7, joined: '2025-07-21' }, { id: 2, name: 'Karthik Rao', phone: '+91 98222 33445', email: 'karthik.rao@mail.com', address: '8, Palm Meadows, Whitefield', location: 'Bengaluru', city: 'Bengaluru', state: 'Karnataka', postcode: '560066', totalOrders: 7, joined: '2025-07-21' },
{ id: 3, name: 'Neha Kulkarni', phone: '+91 98333 44556', email: 'neha.k@mail.com', address: '102, Sea View, Bandra East', location: 'Mumbai', city: 'Mumbai', state: 'Maharashtra', postcode: '400051', totalOrders: 31, joined: '2024-11-02' }, { id: 3, name: 'Neha Kulkarni', phone: '+91 98333 44556', email: 'neha.k@mail.com', address: '102, Lake View, Banjara Hills', location: 'Hyderabad', city: 'Hyderabad', state: 'Telangana', postcode: '500034', totalOrders: 31, joined: '2024-11-02' },
{ id: 4, name: 'Arjun Mehta', phone: '+91 98444 55667', email: 'arjun.m@mail.com', address: 'C-14, Saket', location: 'Delhi NCR', city: 'New Delhi', state: 'Delhi', postcode: '110017', totalOrders: 5, joined: '2025-09-18' }, { id: 4, name: 'Arjun Mehta', phone: '+91 98444 55667', email: 'arjun.m@mail.com', address: '14, R.S. Puram', location: 'Coimbatore', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '641002', totalOrders: 5, joined: '2025-09-18' },
{ id: 5, name: 'Pooja Desai', phone: '+91 98555 66778', email: 'pooja.d@mail.com', address: '4, Lane 5, Koregaon Park', location: 'Pune', city: 'Pune', state: 'Maharashtra', postcode: '411001', totalOrders: 12, joined: '2025-01-30' } { id: 5, name: 'Pooja Desai', phone: '+91 98555 66778', email: 'pooja.d@mail.com', address: '4, Lane 5, Saravanampatti', location: 'Coimbatore', city: 'Coimbatore', state: 'Tamil Nadu', postcode: '641035', totalOrders: 12, joined: '2025-01-30' }
]; ];
export const tenants = [ export const tenants = [
{ id: 1, name: 'Freshly Foods', contact: 'Anil Gupta', phone: '+91 80451 22000', email: 'ops@freshlyfoods.in', address: 'Koramangala 4th Block', city: 'Bengaluru', postcode: '560034', lat: '12.9352', lng: '77.6245', status: 'active', volume: 1240 }, { id: 1, name: 'Freshly Foods', contact: 'Anil Gupta', phone: '+91 80451 22000', email: 'ops@freshlyfoods.in', address: 'Koramangala 4th Block', city: 'Bengaluru', postcode: '560034', lat: '12.9352', lng: '77.6245', status: 'active', volume: 1240 },
{ id: 2, name: 'UrbanCart', contact: 'Meera Nair', phone: '+91 22480 11233', email: 'logistics@urbancart.com', address: 'Indiranagar 100ft Rd', city: 'Bengaluru', postcode: '560038', lat: '12.9719', lng: '77.6412', status: 'active', volume: 980 }, { id: 2, name: 'UrbanCart', contact: 'Meera Nair', phone: '+91 22480 11233', email: 'logistics@urbancart.com', address: 'Indiranagar 100ft Rd', city: 'Bengaluru', postcode: '560038', lat: '12.9719', lng: '77.6412', status: 'active', volume: 980 },
{ id: 3, name: 'MediQuick Pharma', contact: 'Rohit Sen', phone: '+91 22556 99001', email: 'dispatch@mediquick.in', address: 'Andheri West', city: 'Mumbai', postcode: '400058', lat: '19.1351', lng: '72.8290', status: 'pending', volume: 410 }, { id: 3, name: 'MediQuick Pharma', contact: 'Rohit Sen', phone: '+91 40556 99001', email: 'dispatch@mediquick.in', address: 'Hitech City', city: 'Hyderabad', postcode: '500081', lat: '17.4435', lng: '78.3772', status: 'pending', volume: 410 },
{ id: 4, name: 'BloomBox Florists', contact: 'Tina Roy', phone: '+91 11409 33221', email: 'hello@bloombox.in', address: 'Connaught Place', city: 'New Delhi', postcode: '110001', lat: '28.6315', lng: '77.2167', status: 'inactive', volume: 120 }, { id: 4, name: 'BloomBox Florists', contact: 'Tina Roy', phone: '+91 42240 33221', email: 'hello@bloombox.in', address: 'Gandhipuram', city: 'Coimbatore', postcode: '641012', lat: '11.0168', lng: '76.9558', status: 'inactive', volume: 120 },
{ id: 5, name: 'TechNova Retail', contact: 'Sameer Joshi', phone: '+91 40229 77654', email: 'ops@technova.in', address: 'Hitech City', city: 'Hyderabad', postcode: '500081', lat: '17.4435', lng: '78.3772', status: 'active', volume: 760 } { id: 5, name: 'TechNova Retail', contact: 'Sameer Joshi', phone: '+91 40229 77654', email: 'ops@technova.in', address: 'Hitech City', city: 'Hyderabad', postcode: '500081', lat: '17.4435', lng: '78.3772', status: 'active', volume: 760 }
]; ];
@@ -78,8 +85,8 @@ export const tenantPricing = [
export const pricing = [ export const pricing = [
{ id: 1, location: 'Bengaluru', pricingId: 'PR-001', name: 'Standard', slab: '0-5 km', basePrice: 40, minKm: 2, pricePerKm: 9, maxKm: 5, minOrders: 1 }, { id: 1, location: 'Bengaluru', pricingId: 'PR-001', name: 'Standard', slab: '0-5 km', basePrice: 40, minKm: 2, pricePerKm: 9, maxKm: 5, minOrders: 1 },
{ id: 2, location: 'Bengaluru', pricingId: 'PR-002', name: 'Express', slab: '5-10 km', basePrice: 65, minKm: 5, pricePerKm: 8, maxKm: 10, minOrders: 1 }, { id: 2, location: 'Bengaluru', pricingId: 'PR-002', name: 'Express', slab: '5-10 km', basePrice: 65, minKm: 5, pricePerKm: 8, maxKm: 10, minOrders: 1 },
{ id: 3, location: 'Mumbai', pricingId: 'PR-003', name: 'Standard', slab: '0-5 km', basePrice: 45, minKm: 2, pricePerKm: 10, maxKm: 5, minOrders: 1 }, { id: 3, location: 'Hyderabad', pricingId: 'PR-003', name: 'Standard', slab: '0-5 km', basePrice: 45, minKm: 2, pricePerKm: 10, maxKm: 5, minOrders: 1 },
{ id: 4, location: 'Delhi NCR', pricingId: 'PR-004', name: 'Bulk', slab: '10+ km', basePrice: 95, minKm: 10, pricePerKm: 7, maxKm: 25, minOrders: 5 } { id: 4, location: 'Coimbatore', pricingId: 'PR-004', name: 'Bulk', slab: '10+ km', basePrice: 95, minKm: 10, pricePerKm: 7, maxKm: 25, minOrders: 5 }
]; ];
export const invoices = [ export const invoices = [
@@ -178,53 +185,122 @@ export const statusBreakdown = [
// Mirrors doormile.com's primary narrative: Origin → Hub → Hub → Doorstep. // Mirrors doormile.com's primary narrative: Origin → Hub → Hub → Doorstep.
export const threeMile = [ export const threeMile = [
{ {
key: 'first', title: 'First Mile', subtitle: 'Origin to Hub', color: '#EA580C', key: 'first', title: 'First Mile', subtitle: 'Origin Hub', color: '#EA580C',
metric: 184, metricLabel: 'pickups today', onTime: 98.1, metric: 184, metricLabel: 'active pickups', throughput: 412, onTime: 98.1, exceptions: 2,
utilization: 78, bottleneck: 'Peelamedu dock queue', aiRec: 'Add 2 pickup riders in Coimbatore',
features: ['AI-scheduled pickups', 'Dynamic load consolidation', 'Yard & dock management', 'Pickup quality checks'] features: ['AI-scheduled pickups', 'Dynamic load consolidation', 'Yard & dock management', 'Pickup quality checks']
}, },
{ {
key: 'mid', title: 'Mid Mile', subtitle: 'Hub to Hub Transit', color: '#0E7C7B', key: 'mid', title: 'Mid Mile', subtitle: 'Hub Hub Transit', color: '#0E7C7B',
metric: 4, metricLabel: 'line-hauls live', onTime: 97.4, metric: 4, metricLabel: 'line-hauls live', throughput: 38, onTime: 97.4, exceptions: 3,
utilization: 71, bottleneck: 'CBE → BLR corridor +18m', aiRec: 'Pre-stage backup at Hyderabad hub',
features: ['Optimised line-haul routing', 'Cross-docking & sortation', 'Live SLA monitoring', 'EV-first corridors'] features: ['Optimised line-haul routing', 'Cross-docking & sortation', 'Live SLA monitoring', 'EV-first corridors']
}, },
{ {
key: 'last', title: 'Last Mile', subtitle: 'Hub to Doorstep', color: '#1D4ED8', key: 'last', title: 'Last Mile', subtitle: 'Hub Doorstep', color: '#1D4ED8',
metric: 96, metricLabel: 'out for delivery', onTime: 98.9, metric: 96, metricLabel: 'out for delivery', throughput: 1330, onTime: 98.9, exceptions: 1,
utilization: 84, bottleneck: null, aiRec: 'Consolidate 6 Whitefield drops into 1 run',
features: ['Multi-stop route optimization', 'Precise delivery windows', 'Digital proof of delivery', 'Real-time customer updates'] features: ['Multi-stop route optimization', 'Precise delivery windows', 'Digital proof of delivery', 'Real-time customer updates']
} }
]; ];
// ==============================|| END-TO-END SHIPMENT JOURNEY (hop-by-hop) ||============================== // // ==============================|| AI COMMAND CENTER — pipeline + impact ||============================== //
// Follows ONE parcel Chennai → Bengaluru through every node: agent → nearest hub → origin main hub // The end-to-end flow Orders → Delivered, each stage carrying a live count for the command centre.
// → line-haul → destination main hub → sub hub → delivery agent → customer. With live monitoring & reroute. export const commandPipeline = [
export const shipmentJourney = { { key: 'orders', label: 'Orders Received', count: '1,402', sub: 'today', icon: 'order', pct: 100, status: 'done' },
id: 'DM-CHN-BLR-7741', { key: 'trust', label: 'Trust Validation', count: '99.2%', sub: 'cleared', icon: 'trust', pct: 99, status: 'done' },
product: 'Documents & electronics · 3.2 kg', { key: 'hub', label: 'Hub Allocation', count: '6', sub: 'hubs', icon: 'hub', pct: 100, status: 'done' },
from: { city: 'Chennai', area: 'T. Nagar', name: 'Suresh Kumar' }, { key: 'fleet', label: 'Fleet Assignment', count: '124', sub: 'vehicles', icon: 'fleet', pct: 96, status: 'done' },
to: { city: 'Bengaluru', area: 'Koramangala', name: 'Riya Sharma' }, { key: 'dispatch', label: 'Dispatch AI', count: '41', sub: 'optimized', icon: 'ai', pct: 88, status: 'active' },
client: 'TechNova Retail', { key: 'rider', label: 'Rider Allocation', count: '48', sub: 'active', icon: 'rider', pct: 74, status: 'active' },
distance: 346, { key: 'tracking', label: 'Live Tracking', count: '96', sub: 'in transit', icon: 'track', pct: 52, status: 'active' },
mode: 'Standard · EV-first', { key: 'delivered', label: 'Delivered', count: '1,330', sub: 'today', icon: 'done', pct: 95, status: 'active' }
placed: '08 Jun, 09:12 AM', ];
eta: '09 Jun, 02:30 PM',
// ==============================|| HUB OPERATIONS CENTER (no map) ||============================== //
// Per-city rollups for the Hub Operations cards + comparison + activity.
export const hubCityStats = [
{ city: 'Coimbatore', code: 'CBE', capacity: 10400, utilization: 70, processed: 5840, riders: 14, sla: 97.2, health: 'healthy' },
{ city: 'Bengaluru', code: 'BLR', capacity: 13200, utilization: 64, processed: 8490, riders: 22, sla: 98.6, health: 'healthy' },
{ city: 'Hyderabad', code: 'HYD', capacity: 9300, utilization: 50, processed: 4600, riders: 12, sla: 96.4, health: 'watch' }
];
export const hubThroughput = [
{ m: 'Mon', cbe: 720, blr: 1180, hyd: 560 },
{ m: 'Tue', cbe: 810, blr: 1240, hyd: 620 },
{ m: 'Wed', cbe: 760, blr: 1310, hyd: 680 },
{ m: 'Thu', cbe: 880, blr: 1290, hyd: 640 },
{ m: 'Fri', cbe: 940, blr: 1420, hyd: 710 },
{ m: 'Sat', cbe: 1020, blr: 1510, hyd: 760 }
];
export const hubActivity = [
{ time: '10:42 AM', hub: 'Hoskote Regional Hub', text: 'Inbound line-haul from Coimbatore scanned in · 320 parcels', type: 'inbound' },
{ time: '10:31 AM', hub: 'Coimbatore Regional Hub', text: 'Sortation wave 3 completed · 0 misroutes', type: 'sort' },
{ time: '10:18 AM', hub: 'Hyderabad Regional Hub', text: 'Outbound EV truck dispatched to Bengaluru', type: 'outbound' },
{ time: '09:58 AM', hub: 'Koramangala Micro Hub', text: 'Last-mile batch released to 8 riders', type: 'dispatch' },
{ time: '09:40 AM', hub: 'Peelamedu Micro Hub', text: 'Dock 3 backlog cleared', type: 'sort' }
];
export const maintenanceNotices = [
{ hub: 'Peelamedu Micro Hub', item: 'Backup generator test', due: 'Overdue 1 day', severity: 'high' },
{ hub: 'Hoskote Regional Hub', item: 'Conveyor belt B service', due: 'Due in 2 days', severity: 'medium' },
{ hub: 'Hyderabad Regional Hub', item: 'Dock leveler inspection', due: 'Scheduled tonight', severity: 'low' }
];
// AI contribution metrics for the AI Impact section.
export const aiImpact = {
routeSavings: 34, delaysPrevented: 23, optimizationActions: 41,
fuelSavedL: 1240, co2ReducedKg: 12840, networkEfficiency: 92
};
// ==============================|| TRIP JOURNEY (vehicle trip + manifest) ||============================== //
// Primary entity = ONE line-haul TRIP: a single vehicle + driver moving a sealed manifest of many
// shipments along the Coimbatore → Bengaluru corridor. Milestones are TRIP-level only; each
// shipment's own lifecycle lives on Shipment 360 (/orders/:id), linked from the manifest.
export const trip = {
id: 'TRIP-CBE-BLR-0463',
corridor: 'CBE → BLR',
status: 'In Transit',
mode: 'EV-first line-haul',
vehicle: { reg: 'TN-37-LH-0463', type: 'EV Truck 4W', capacityKg: 1200, battery: 62 },
driver: { name: 'Imran Sheikh', phone: '+91 98860 44781' },
from: { city: 'Coimbatore', hub: 'Coimbatore Regional Hub' },
to: { city: 'Bengaluru', hub: 'Hoskote Regional Hub' },
distance: 365,
departed: '08 Jun, 12:05 PM',
eta: '09 Jun, 06:30 AM',
progress: 52, progress: 52,
currentStage: 'Line-Haul · Chennai → Bengaluru', loadKg: 940,
hops: [ pieces: 312,
{ key: 'booked', mile: 'First Mile', title: 'Shipment Booked', node: 'Chennai · T. Nagar', handler: 'Customer · Suresh Kumar', icon: 'order', time: '08 Jun, 09:12 AM', status: 'done', detail: 'Shipment ID generated · digital docs & e-waybill created' }, // TRIP-level milestones (not per-shipment)
{ key: 'pickup', mile: 'First Mile', title: 'Agent Pickup', node: 'Chennai · T. Nagar', handler: 'Rider · Faisal Khan (EV 2W)', icon: 'agent', time: '08 Jun, 10:05 AM', status: 'done', detail: 'OTP verified · photo proof captured · tamper seal applied' }, milestones: [
{ key: 'nearhub', mile: 'First Mile', title: 'Nearest Hub — Check-in', node: 'Nungambakkam Micro Hub', handler: 'Hub operations', icon: 'hub', time: '08 Jun, 10:48 AM', status: 'done', detail: 'Scanned in · sorted for origin main hub' }, { key: 'sealed', title: 'Manifest Sealed', node: 'Coimbatore Regional Hub', time: '08 Jun, 11:20 AM', status: 'done', detail: '12 shipments · 312 pieces · 940 kg consolidated' },
{ key: 'mainhub-out', mile: 'Mid Mile', title: 'Origin Main Hub — Dispatched', node: 'Guindy Regional Hub (Chennai)', handler: 'Hub operations', icon: 'hub', time: '08 Jun, 01:20 PM', status: 'done', detail: 'Consolidated & loaded onto line-haul EV truck' }, { key: 'departed', title: 'Loaded & Departed', node: 'Coimbatore Regional Hub', time: '08 Jun, 12:05 PM', status: 'done', detail: 'Bay 4 · e-waybill EWB-77410 generated' },
{ key: 'linehaul', mile: 'Mid Mile', title: 'Line-Haul In Transit', node: 'NH48 · Chennai → Bengaluru', handler: 'Driver · Imran Sheikh (EV Truck 4W)', icon: 'truck', time: '08 Jun, 01:35 PM', status: 'active', detail: 'En route · live GPS · 178 km to destination hub' }, { key: 'transit', title: 'In Transit · Line-Haul', node: 'CBE → BLR corridor (NH-544)', time: '08 Jun, 12:05 PM', status: 'active', detail: 'Live GPS · 168 km to destination hub · ETA protected' },
{ key: 'desthub', mile: 'Mid Mile', title: 'Destination Main Hub — Arrival', node: 'Hoskote Regional Hub (Bengaluru)', handler: 'Hub operations', icon: 'hub', time: 'Est. 09 Jun, 06:30 AM', status: 'pending', detail: 'Inbound scan & sortation' }, { key: 'arrival', title: 'Arrival Scan', node: 'Hoskote Regional Hub', time: 'Est. 09 Jun, 06:30 AM', status: 'pending', detail: 'Inbound gate scan & trip reconciliation' },
{ key: 'subhub', mile: 'Last Mile', title: 'Sub Hub — Last-Mile Sort', node: 'Koramangala Micro Hub', handler: 'Hub operations', icon: 'hub', time: 'Est. 09 Jun, 10:15 AM', status: 'pending', detail: 'Routed to delivery agent zone' }, { key: 'sorted', title: 'Unloaded & Sorted', node: 'Hoskote Regional Hub', time: 'Est. 09 Jun, 07:30 AM', status: 'pending', detail: 'Shipments broken to last-mile micro hubs' },
{ key: 'assigned', mile: 'Last Mile', title: 'Delivery Agent Assigned', node: 'Bengaluru · Koramangala', handler: 'MileTruth AI · auto-assign', icon: 'agent', time: 'Est. 09 Jun, 11:00 AM', status: 'pending', detail: 'Multi-stop route optimized for the agent' }, { key: 'handover', title: 'Handover to Last-Mile', node: 'Bengaluru micro hubs', time: 'Est. 09 Jun, 09:00 AM', status: 'pending', detail: 'Each shipment enters its own last-mile journey' }
{ key: 'ofd', mile: 'Last Mile', title: 'Out for Delivery', node: 'Bengaluru · Koramangala', handler: 'Rider · auto-assigned (EV 2W)', icon: 'agent', time: 'Est. 09 Jun, 01:40 PM', status: 'pending', detail: 'Live tracking link shared with customer' }, ],
{ key: 'delivered', mile: 'Last Mile', title: 'Delivered', node: 'Bengaluru · Koramangala', handler: 'Customer · Riya Sharma', icon: 'done', time: 'Est. 09 Jun, 02:30 PM', status: 'pending', detail: 'OTP + photo proof of delivery' } // every shipment riding this trip — links into Shipment 360
manifest: [
{ id: 'DM-10242', client: 'UrbanCart', customer: 'Karthik Rao', dropArea: 'Whitefield', pieces: 1, weightKg: 12.8, status: 'On board' },
{ id: 'DM-10241', client: 'Freshly Foods', customer: 'Riya Sharma', dropArea: 'HSR Layout', pieces: 3, weightKg: 9.4, status: 'On board' },
{ id: 'DM-10245', client: 'Freshly Foods', customer: 'Sneha Iyer', dropArea: 'BTM Layout', pieces: 4, weightKg: 14.2, status: 'On board' },
{ id: 'DM-10248', client: 'UrbanCart', customer: 'Suresh Kumar', dropArea: 'Jayanagar', pieces: 1, weightKg: 8.3, status: 'On board' },
{ id: 'DM-10261', client: 'TechNova Retail', customer: 'Vikram Reddy', dropArea: 'Indiranagar', pieces: 2, weightKg: 6.1, status: 'On board' },
{ id: 'DM-10263', client: 'GreenLeaf Organics', customer: 'Pooja Desai', dropArea: 'Koramangala', pieces: 5, weightKg: 22.0, status: 'On board' },
{ id: 'DM-10266', client: 'BloomBox Florists', customer: 'Arjun Mehta', dropArea: 'JP Nagar', pieces: 1, weightKg: 3.2, status: 'On board' },
{ id: 'DM-10270', client: 'UrbanCart', customer: 'Meera Nair', dropArea: 'Marathahalli', pieces: 2, weightKg: 5.5, status: 'On board' },
{ id: 'DM-10274', client: 'Freshly Foods', customer: 'Anil Gupta', dropArea: 'Electronic City', pieces: 3, weightKg: 11.7, status: 'On board' },
{ id: 'DM-10277', client: 'TechNova Retail', customer: 'Sameer Joshi', dropArea: 'Hebbal', pieces: 1, weightKg: 4.0, status: 'On board' },
{ id: 'DM-10281', client: 'MediQuick Pharma', customer: 'Rohit Sen', dropArea: 'Whitefield', pieces: 2, weightKg: 7.6, status: 'On board' },
{ id: 'DM-10285', client: 'GreenLeaf Organics', customer: 'Tina Roy', dropArea: 'Yelahanka', pieces: 6, weightKg: 26.4, status: 'On board' }
], ],
events: [ events: [
{ time: '08 Jun, 03:10 PM', type: 'reroute', title: 'Traffic on NH48 near Krishnagiri', detail: 'MileTruth AI rerouted via Hosur bypass — ETA protected (+0 min)' }, { time: '08 Jun, 02:10 PM', type: 'reroute', title: 'Congestion near Salem bypass', detail: 'MileTruth AI rerouted via NH-544 — ETA protected (+0 min)' },
{ time: '08 Jun, 05:40 PM', type: 'monitor', title: 'Weather check · clear', detail: 'No disruption forecast on the corridor' }, { time: '08 Jun, 05:40 PM', type: 'monitor', title: 'Weather check · clear', detail: 'No disruption forecast on the CBE → BLR corridor' },
{ time: '08 Jun, 08:15 PM', type: 'monitor', title: 'On schedule', detail: 'Vehicle at 62% battery · charging stop planned at Hosur' } { time: '08 Jun, 08:15 PM', type: 'monitor', title: 'On schedule', detail: 'Vehicle at 62% battery · charging stop planned at Krishnagiri' }
] ]
}; };
@@ -249,12 +325,12 @@ export const verticalOf = (tenant) => tenantVertical[tenant] || 'Enterprise & B2
// ==============================|| PHARMA COLD-CHAIN ||============================== // // ==============================|| PHARMA COLD-CHAIN ||============================== //
export const coldChainSummary = { monitored: 248, inRange: 241, atRisk: 5, breaches: 2, compliance: 99.2, avgTemp: 4.2 }; export const coldChainSummary = { monitored: 248, inRange: 241, atRisk: 5, breaches: 2, compliance: 99.2, avgTemp: 4.2 };
export const coldChainShipments = [ export const coldChainShipments = [
{ id: 'DM-CC-2041', product: 'Insulin vials', tenant: 'MediQuick Pharma', range: '2 8 °C', temp: 4.6, route: 'Andheri → Bandra', status: 'in-range', excursionMin: 0 }, { id: 'DM-CC-2041', product: 'Insulin vials', tenant: 'MediQuick Pharma', range: '2 8 °C', temp: 4.6, route: 'Hyderabad MedTech Zone → Jubilee Hills Hospital', status: 'in-range', excursionMin: 0 },
{ id: 'DM-CC-2042', product: 'mRNA vaccine', tenant: 'MediQuick Pharma', range: '-20 -15 °C', temp: -17.2, route: 'Hub HYD → BLR', status: 'in-range', excursionMin: 0 }, { id: 'DM-CC-2042', product: 'mRNA vaccine', tenant: 'MediQuick Pharma', range: '-20 -15 °C', temp: -17.2, route: 'Hub HYD → Hub BLR', status: 'in-range', excursionMin: 0 },
{ id: 'DM-CC-2043', product: 'Frozen seafood', tenant: 'Freshly Foods', range: '≤ -18 °C', temp: -12.4, route: 'Koramangala → HSR', status: 'breach', excursionMin: 14 }, { id: 'DM-CC-2043', product: 'Blood plasma', tenant: 'MediQuick Pharma', range: '≤ -18 °C', temp: -12.4, route: 'Bengaluru Pharma Park → Whitefield Clinic', status: 'breach', excursionMin: 14 },
{ id: 'DM-CC-2044', product: 'Dairy & yogurt', tenant: 'GreenLeaf Organics', range: '2 6 °C', temp: 7.1, route: 'Koregaon → Viman Nagar', status: 'at-risk', excursionMin: 3 }, { id: 'DM-CC-2044', product: 'Dairy & yogurt', tenant: 'GreenLeaf Organics', range: '2 6 °C', temp: 7.1, route: 'Coimbatore Cold Store → Peelamedu', status: 'at-risk', excursionMin: 3 },
{ id: 'DM-CC-2045', product: 'Blood samples', tenant: 'MediQuick Pharma', range: '2 8 °C', temp: 5.3, route: 'Connaught Pl → Saket', status: 'in-range', excursionMin: 0 }, { id: 'DM-CC-2045', product: 'Blood samples', tenant: 'MediQuick Pharma', range: '2 8 °C', temp: 5.3, route: 'Coimbatore Medical Hub → Peelamedu Hospital', status: 'in-range', excursionMin: 0 },
{ id: 'DM-CC-2046', product: 'Antibiotics', tenant: 'MediQuick Pharma', range: '15 25 °C', temp: 21.4, route: 'Hitech City → Gachibowli', status: 'in-range', excursionMin: 0 } { id: 'DM-CC-2046', product: 'Antibiotics', tenant: 'MediQuick Pharma', range: '15 25 °C', temp: 21.4, route: 'Hyderabad MedTech Zone → Gachibowli Clinic', status: 'in-range', excursionMin: 0 }
]; ];
// ==============================|| LOGISTICS OPERATING SYSTEM — LAYER DATA ||============================== // // ==============================|| LOGISTICS OPERATING SYSTEM — LAYER DATA ||============================== //
@@ -291,37 +367,37 @@ export const trustQueue = [
]; ];
// ---- Layer 3: Intelligent Hub Network ---- // ---- Layer 3: Intelligent Hub Network ----
// Live hub network across the three operating metros: Coimbatore · Bengaluru · Hyderabad.
export const hubs = [ export const hubs = [
{ id: 'HUB-BLR-01', name: 'Koramangala Micro Hub', type: 'Micro Hub', city: 'Bengaluru', lat: 12.9352, lng: 77.6245, capacity: 1200, load: 940, inbound: 180, outbound: 210, dock: 4, status: 'online' }, { id: 'HUB-CBE-RG', name: 'Coimbatore Regional Hub', type: 'Regional Hub', city: 'Coimbatore', lat: 11.0168, lng: 76.9558, capacity: 9000, load: 6120, inbound: 1400, outbound: 1520, dock: 18, status: 'online' },
{ id: 'HUB-BLR-02', name: 'Whitefield City Hub', type: 'City Hub', city: 'Bengaluru', lat: 12.9698, lng: 77.7499, capacity: 4000, load: 2860, inbound: 520, outbound: 610, dock: 10, status: 'online' }, { id: 'HUB-CBE-01', name: 'Peelamedu Micro Hub', type: 'Micro Hub', city: 'Coimbatore', lat: 11.0290, lng: 76.9960, capacity: 1400, load: 1180, inbound: 240, outbound: 260, dock: 5, status: 'busy' },
{ id: 'HUB-BLR-RG', name: 'Hoskote Regional Hub', type: 'Regional Hub', city: 'Bengaluru', lat: 13.0707, lng: 77.7980, capacity: 12000, load: 7400, inbound: 1900, outbound: 2100, dock: 24, status: 'online' }, { id: 'HUB-BLR-RG', name: 'Hoskote Regional Hub', type: 'Regional Hub', city: 'Bengaluru', lat: 13.0707, lng: 77.7980, capacity: 12000, load: 7400, inbound: 1900, outbound: 2100, dock: 24, status: 'online' },
{ id: 'HUB-MUM-01', name: 'Andheri City Hub', type: 'City Hub', city: 'Mumbai', lat: 19.1197, lng: 72.8468, capacity: 4500, load: 3950, inbound: 700, outbound: 760, dock: 12, status: 'busy' }, { id: 'HUB-BLR-01', name: 'Koramangala Micro Hub', type: 'Micro Hub', city: 'Bengaluru', lat: 12.9352, lng: 77.6245, capacity: 1200, load: 1090, inbound: 180, outbound: 210, dock: 4, status: 'busy' },
{ id: 'HUB-HYD-01', name: 'Hitech Cross Dock', type: 'Cross Dock', city: 'Hyderabad', lat: 17.4435, lng: 78.3772, capacity: 3000, load: 1100, inbound: 340, outbound: 360, dock: 8, status: 'online' }, { id: 'HUB-HYD-RG', name: 'Hyderabad Regional Hub', type: 'Regional Hub', city: 'Hyderabad', lat: 17.4435, lng: 78.3772, capacity: 8000, load: 3960, inbound: 920, outbound: 980, dock: 16, status: 'online' },
{ id: 'HUB-DEL-RG', name: 'Bilaspur Regional Hub', type: 'Regional Hub', city: 'Delhi NCR', lat: 28.4231, lng: 77.0490, capacity: 14000, load: 11200, inbound: 2400, outbound: 2600, dock: 28, status: 'busy' } { id: 'HUB-HYD-01', name: 'Gachibowli Micro Hub', type: 'Micro Hub', city: 'Hyderabad', lat: 17.4401, lng: 78.3489, capacity: 1300, load: 640, inbound: 150, outbound: 165, dock: 5, status: 'online' }
]; ];
export const hubNetworkTypes = [ export const hubNetworkTypes = [
{ type: 'Micro Hubs (Urban)', count: 5, desc: 'Last-mile EV staging' }, { type: 'Regional Hubs', count: 3, desc: 'Aggregation & line-haul origin' },
{ type: 'City Hubs', count: 4, desc: 'Sort & local distribution' }, { type: 'Micro Hubs (Urban)', count: 3, desc: 'Last-mile EV staging' }
{ type: 'Regional Hubs', count: 2, desc: 'Aggregation & line-haul origin' },
{ type: 'Cross Dock Points', count: 1, desc: 'No-store transfer' }
]; ];
// Inter-city line-haul corridors connecting the three regional hubs (the network triangle).
export const lineHauls = [ export const lineHauls = [
{ id: 'LH-001', from: 'Hoskote Regional Hub', to: 'Bilaspur Regional Hub', corridor: 'BLR → DEL', distance: 2150, vehicle: 'EV Truck 4W', load: 86, eta: '34h', status: 'in-transit' }, { id: 'LH-001', from: 'Coimbatore Regional Hub', to: 'Hoskote Regional Hub', corridor: 'CBE → BLR', distance: 365, vehicle: 'EV Truck 4W', load: 84, eta: '7h', status: 'in-transit' },
{ id: 'LH-002', from: 'Andheri City Hub', to: 'Hoskote Regional Hub', corridor: 'MUM → BLR', distance: 980, vehicle: 'ICE Truck 6W', load: 72, eta: '18h', status: 'in-transit' }, { id: 'LH-002', from: 'Hoskote Regional Hub', to: 'Hyderabad Regional Hub', corridor: 'BLR → HYD', distance: 570, vehicle: 'EV Truck 4W', load: 71, eta: '11h', status: 'in-transit' },
{ id: 'LH-003', from: 'Hitech Cross Dock', to: 'Hoskote Regional Hub', corridor: 'HYD → BLR', distance: 570, vehicle: 'EV Truck 4W', load: 64, eta: '11h', status: 'scheduled' }, { id: 'LH-003', from: 'Hyderabad Regional Hub', to: 'Coimbatore Regional Hub', corridor: 'HYD → CBE', distance: 790, vehicle: 'ICE Truck 6W', load: 58, eta: '15h', status: 'scheduled' },
{ id: 'LH-004', from: 'Bilaspur Regional Hub', to: 'Andheri City Hub', corridor: 'DEL → MUM', distance: 1420, vehicle: 'ICE Truck 6W', load: 91, eta: '24h', status: 'loading' } { id: 'LH-004', from: 'Hoskote Regional Hub', to: 'Coimbatore Regional Hub', corridor: 'BLR → CBE', distance: 365, vehicle: 'EV Truck 4W', load: 92, eta: '7h', status: 'loading' }
]; ];
// ---- Layer 4: Fleet & Rider Operating System ---- // ---- Layer 4: Fleet & Rider Operating System ----
export const fleet = [ export const fleet = [
{ id: 'VH-EV-018', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 78, range: 96, health: 94, capacityKg: 600, status: 'on-trip', rider: 'Mohan Das', hub: 'Koramangala Micro Hub', uptime: 98.2 }, { id: 'VH-EV-018', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 78, range: 96, health: 94, capacityKg: 600, status: 'on-trip', rider: 'Mohan Das', hub: 'Koramangala Micro Hub', uptime: 98.2 },
{ id: 'VH-EV-022', model: 'Euler HiLoad EV', type: 'EV 3W', powertrain: 'EV', battery: 41, range: 38, health: 89, capacityKg: 688, status: 'charging', rider: '—', hub: 'Whitefield City Hub', uptime: 96.5 }, { id: 'VH-EV-022', model: 'Euler HiLoad EV', type: 'EV 3W', powertrain: 'EV', battery: 41, range: 38, health: 89, capacityKg: 688, status: 'charging', rider: '—', hub: 'Hoskote Regional Hub', uptime: 96.5 },
{ id: 'VH-2W-104', model: 'Ola S1 Pro', type: 'EV 2W', powertrain: 'EV', battery: 63, range: 71, health: 91, capacityKg: 20, status: 'on-trip', rider: 'Ravi Teja', hub: 'Hitech Cross Dock', uptime: 97.1 }, { id: 'VH-2W-104', model: 'Ola S1 Pro', type: 'EV 2W', powertrain: 'EV', battery: 63, range: 71, health: 91, capacityKg: 20, status: 'on-trip', rider: 'Ravi Teja', hub: 'Hyderabad Regional Hub', uptime: 97.1 },
{ id: 'VH-IC-211', model: 'Mahindra Bolero', type: 'ICE 4W', powertrain: 'ICE', battery: null, range: 320, health: 82, capacityKg: 1500, status: 'idle', rider: '—', hub: 'Hoskote Regional Hub', uptime: 93.4 }, { id: 'VH-IC-211', model: 'Mahindra Bolero', type: 'ICE 4W', powertrain: 'ICE', battery: null, range: 320, health: 82, capacityKg: 1500, status: 'idle', rider: '—', hub: 'Hoskote Regional Hub', uptime: 93.4 },
{ id: 'VH-2W-118', model: 'Honda Activa', type: 'ICE 2W', powertrain: 'ICE', battery: null, range: 180, health: 76, capacityKg: 25, status: 'maintenance', rider: '—', hub: 'Andheri City Hub', uptime: 88.9 }, { id: 'VH-2W-118', model: 'Honda Activa', type: 'ICE 2W', powertrain: 'ICE', battery: null, range: 180, health: 76, capacityKg: 25, status: 'maintenance', rider: '—', hub: 'Coimbatore Regional Hub', uptime: 88.9 },
{ id: 'VH-EV-031', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 88, range: 108, health: 96, capacityKg: 600, status: 'idle', rider: '—', hub: 'Bilaspur Regional Hub', uptime: 99.0 } { id: 'VH-EV-031', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 88, range: 108, health: 96, capacityKg: 600, status: 'idle', rider: '—', hub: 'Hyderabad Regional Hub', uptime: 99.0 }
]; ];
export const fleetSummary = { export const fleetSummary = {
@@ -332,8 +408,8 @@ export const fleetSummary = {
// ---- Layer 5: MileTruth AI Engine — dispatch & optimization ---- // ---- Layer 5: MileTruth AI Engine — dispatch & optimization ----
export const dispatchQueue = [ export const dispatchQueue = [
{ id: 'DM-10246', pickup: 'Hitech City', drop: 'Gachibowli', priority: 'high', sla: '45 min', suggestedRider: 'Ravi Teja', confidence: 96, status: 'matched', etaMin: 22 }, { id: 'DM-10246', pickup: 'Hitech City', drop: 'Gachibowli', priority: 'high', sla: '45 min', suggestedRider: 'Ravi Teja', confidence: 96, status: 'matched', etaMin: 22 },
{ id: 'DM-10244', pickup: 'Connaught Place', drop: 'Saket, Block C', priority: 'standard', sla: '90 min', suggestedRider: 'Sandeep Roy', confidence: 88, status: 'optimizing', etaMin: 41 }, { id: 'DM-10244', pickup: 'Gandhipuram', drop: 'R.S. Puram', priority: 'standard', sla: '90 min', suggestedRider: 'Sandeep Roy', confidence: 88, status: 'optimizing', etaMin: 41 },
{ id: 'DM-10248', pickup: 'T. Nagar', drop: 'Adyar', priority: 'standard', sla: '120 min', suggestedRider: 'Faisal Khan', confidence: 91, status: 'matched', etaMin: 33 }, { id: 'DM-10248', pickup: 'Jayanagar', drop: 'JP Nagar', priority: 'standard', sla: '120 min', suggestedRider: 'Faisal Khan', confidence: 91, status: 'matched', etaMin: 33 },
{ id: 'DM-10242', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', priority: 'express', sla: '60 min', suggestedRider: 'Mohan Das', confidence: 94, status: 'dispatched', etaMin: 28 } { id: 'DM-10242', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', priority: 'express', sla: '60 min', suggestedRider: 'Mohan Das', confidence: 94, status: 'dispatched', etaMin: 28 }
]; ];
@@ -359,13 +435,152 @@ export const aiMetrics = { routeSavings: 34, avgEtaAccuracy: 92, batchRate: 2.6,
// ---- Layer 6: Execution & Visibility ---- // ---- Layer 6: Execution & Visibility ----
export const executionFeed = [ export const executionFeed = [
{ id: 'DM-10242', stage: 'In-Transit', rider: 'Mohan Das', loc: 'HSR Layout', detail: 'On route · 4.2 km to drop', time: '10:42 AM', proof: null }, { id: 'DM-10242', stage: 'In-Transit', rider: 'Mohan Das', loc: 'Indiranagar → Whitefield', detail: 'On route · 4.2 km to drop', time: '10:42 AM', proof: null },
{ id: 'DM-10243', stage: 'Picked Up', rider: 'Imran Sheikh', loc: 'Andheri West', detail: 'OTP verified · photo proof logged', time: '10:38 AM', proof: 'pickup' }, { id: 'DM-10243', stage: 'Picked Up', rider: 'Imran Sheikh', loc: 'Gandhipuram → Peelamedu', detail: 'OTP verified · photo proof logged', time: '10:38 AM', proof: 'pickup' },
{ id: 'DM-10247', stage: 'Delivered', rider: 'Ravi Teja', loc: 'Viman Nagar', detail: 'eSign captured · POD uploaded', time: '10:30 AM', proof: 'delivery' }, { id: 'DM-10247', stage: 'Delivered', rider: 'Ravi Teja', loc: 'Banjara Hills → Madhapur', detail: 'eSign captured · POD uploaded', time: '10:30 AM', proof: 'delivery' },
{ id: 'DM-10251', stage: 'Exception', rider: 'Faisal Khan', loc: 'Adyar', detail: 'Customer unavailable · reattempt scheduled', time: '10:21 AM', proof: null }, { id: 'DM-10251', stage: 'Exception', rider: 'Imran Sheikh', loc: 'R.S. Puram → Saravanampatti', detail: 'Customer unavailable · reattempt scheduled', time: '10:21 AM', proof: null },
{ id: 'DM-10246', stage: 'Dispatched', rider: 'Sandeep Roy', loc: 'Hitech City', detail: 'Rider en route to pickup', time: '10:18 AM', proof: null } { id: 'DM-10246', stage: 'Dispatched', rider: 'Sandeep Roy', loc: 'Hitech City → Gachibowli', detail: 'Rider en route to pickup', time: '10:18 AM', proof: null }
]; ];
// Active Deliveries Board — the live shipments an ops manager is accountable for right now.
// etaStatus drives row tone & ETA colour: on-time (green), at-risk (amber), delayed (red).
// delayMin is the signed deviation vs SLA (negative = ahead, positive = late) for delayed/at-risk rows.
// ---- Live Tracking Control Tower — geo-coded shipments with real coordinates & routes ----
// Each delivery carries pickup/drop lat-lng plus a road-shaped polyline; the map interpolates
// the vehicle's live position from `progress`. vehicle ∈ Bike | Auto | Truck | Van.
export const activeDeliveries = [
{
id: 'DM-10242', rider: 'Mohan Das', vehicle: 'Bike', priority: 'express', city: 'Bengaluru',
origin: 'Indiranagar Store', destination: 'Whitefield, Phase 1', eta: '11:10 AM', etaStatus: 'on-time', delayMin: 0, status: 'In-Transit', progress: 68,
pickup: { lat: 12.9719, lng: 77.6412 }, drop: { lat: 12.9698, lng: 77.7499 },
route: [[12.9719, 77.6412], [12.9716, 77.672], [12.975, 77.701], [12.9705, 77.73], [12.9698, 77.7499]]
},
{
id: 'DM-10243', rider: 'Imran Sheikh', vehicle: 'Bike', priority: 'high', city: 'Coimbatore',
origin: 'Gandhipuram', destination: 'Peelamedu', eta: '11:24 AM', etaStatus: 'delayed', delayMin: 18, status: 'In-Transit', progress: 42,
pickup: { lat: 11.0177, lng: 76.9656 }, drop: { lat: 11.03, lng: 76.9963 },
route: [[11.0177, 76.9656], [11.021, 76.976], [11.027, 76.988], [11.03, 76.9963]]
},
{
id: 'DM-10246', rider: 'Sandeep Roy', vehicle: 'Van', priority: 'high', city: 'Hyderabad',
origin: 'Hitech City', destination: 'Gachibowli', eta: '11:02 AM', etaStatus: 'on-time', delayMin: 0, status: 'Dispatched', progress: 12,
pickup: { lat: 17.4435, lng: 78.3772 }, drop: { lat: 17.4401, lng: 78.3489 },
route: [[17.4435, 78.3772], [17.443, 78.365], [17.441, 78.355], [17.4401, 78.3489]]
},
{
id: 'DM-10248', rider: 'Faisal Khan', vehicle: 'Truck', priority: 'standard', city: 'Bengaluru',
origin: 'BTM Layout Depot', destination: 'Jayanagar 4th Block', eta: '11:35 AM', etaStatus: 'at-risk', delayMin: 7, status: 'In-Transit', progress: 55,
pickup: { lat: 12.9166, lng: 77.6101 }, drop: { lat: 12.925, lng: 77.5938 },
route: [[12.9166, 77.6101], [12.92, 77.602], [12.925, 77.5938]]
},
{
id: 'DM-10244', rider: 'Sandeep Roy', vehicle: 'Van', priority: 'standard', city: 'Bengaluru',
origin: 'Koramangala Hub', destination: 'Electronic City', eta: '12:05 PM', etaStatus: 'at-risk', delayMin: 9, status: 'Picked Up', progress: 30,
pickup: { lat: 12.9352, lng: 77.6245 }, drop: { lat: 12.8452, lng: 77.6602 },
route: [[12.9352, 77.6245], [12.91, 77.635], [12.88, 77.648], [12.8452, 77.6602]]
},
{
id: 'DM-10251', rider: 'Imran Sheikh', vehicle: 'Auto', priority: 'high', city: 'Coimbatore',
origin: 'R.S. Puram', destination: 'Saravanampatti', eta: 'Reattempt', etaStatus: 'delayed', delayMin: 35, status: 'Exception', progress: 80,
pickup: { lat: 11.005, lng: 76.95 }, drop: { lat: 11.079, lng: 77.009 },
route: [[11.005, 76.95], [11.03, 76.97], [11.055, 76.99], [11.079, 77.009]]
},
{
id: 'DM-10241', rider: 'Mohan Das', vehicle: 'Auto', priority: 'standard', city: 'Bengaluru',
origin: 'Koramangala Hub', destination: 'HSR Layout, Sec 2', eta: '10:58 AM', etaStatus: 'on-time', delayMin: 0, status: 'In-Transit', progress: 88,
pickup: { lat: 12.9352, lng: 77.6245 }, drop: { lat: 12.9081, lng: 77.6476 },
route: [[12.9352, 77.6245], [12.922, 77.636], [12.9081, 77.6476]]
},
{
id: 'DM-10247', rider: 'Ravi Teja', vehicle: 'Bike', priority: 'express', city: 'Hyderabad',
origin: 'Banjara Hills', destination: 'Madhapur', eta: '10:46 AM', etaStatus: 'on-time', delayMin: 0, status: 'In-Transit', progress: 74,
pickup: { lat: 17.4156, lng: 78.4347 }, drop: { lat: 17.4483, lng: 78.3915 },
route: [[17.4156, 78.4347], [17.43, 78.41], [17.4483, 78.3915]]
},
{
id: 'DM-10239', rider: 'Ravi Teja', vehicle: 'Bike', priority: 'standard', city: 'Bengaluru',
origin: 'Koramangala Hub', destination: 'Koramangala 5th Block', eta: '10:18 AM', etaStatus: 'on-time', delayMin: 0, status: 'Delivered', progress: 100,
pickup: { lat: 12.9352, lng: 77.6245 }, drop: { lat: 12.9352, lng: 77.6135 },
route: [[12.9352, 77.6245], [12.9352, 77.619], [12.9352, 77.6135]]
},
{
id: 'DM-10240', rider: 'Sandeep Roy', vehicle: 'Van', priority: 'standard', city: 'Hyderabad',
origin: 'Hitech City', destination: 'Kondapur', eta: '10:05 AM', etaStatus: 'on-time', delayMin: 0, status: 'Delivered', progress: 100,
pickup: { lat: 17.4435, lng: 78.3772 }, drop: { lat: 17.4615, lng: 78.3635 },
route: [[17.4435, 78.3772], [17.452, 78.37], [17.4615, 78.3635]]
}
];
// Control-tower KPI strip — the live operational pulse across the whole fleet.
export const executionOps = {
activeDeliveries: 96,
delayed: 4,
onTime: 98.6,
activeRiders: 12,
reroutes: 7,
criticalAlerts: 3
};
// Vehicle-type design tokens — icon glyph + brand colour, shared by queue, markers & legend.
export const vehicleTypes = {
Bike: { glyph: '🛵', color: '#1D4ED8' },
Auto: { glyph: '🛺', color: '#EA580C' },
Truck: { glyph: '🚚', color: '#7C3AED' },
Van: { glyph: '🚐', color: '#0E7C7B' }
};
// Fleet totals for the map legend (whole fleet, not just tracked shipments).
export const fleetTotals = { Bike: 68, Auto: 16, Truck: 8, Van: 6, delayed: 4 };
// Inter-city corridor load — active shipments moving between the three service metros.
// Rendered as faint lanes on the map and summarised in the corridor strip.
export const networkCorridors = [
{ from: 'Coimbatore', to: 'Bengaluru', active: 12, onTime: 97 },
{ from: 'Bengaluru', to: 'Hyderabad', active: 9, onTime: 98 },
{ from: 'Coimbatore', to: 'Hyderabad', active: 6, onTime: 96 }
];
// Ambient live fleet scattered around the three metros — gives the map a real control-tower
// density and feeds marker clustering. Deterministically generated so positions stay stable.
export const fleetVehicles = (() => {
const centers = {
Bengaluru: [12.9716, 77.5946],
Coimbatore: [11.0168, 76.9558],
Hyderabad: [17.385, 78.4867]
};
const cityKeys = Object.keys(centers);
// small seeded PRNG (mulberry32) for stable scatter
let s = 0x9e3779b9;
const rnd = () => {
s |= 0; s = (s + 0x6d2b79f5) | 0;
let t = Math.imul(s ^ (s >>> 15), 1 | s);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
const counts = { Bike: 64, Auto: 14, Truck: 7, Van: 3 }; // tracked vehicles make up the legend remainder
const riderNames = ['Mohan Das', 'Imran Sheikh', 'Ravi Teja', 'Sandeep Roy', 'Faisal Khan', 'Arjun Mehta', 'Vikram Reddy', 'Suresh Kumar'];
const out = [];
let n = 0;
Object.entries(counts).forEach(([type, count]) => {
for (let i = 0; i < count; i++) {
const ck = cityKeys[Math.floor(rnd() * cityKeys.length)];
const [clat, clng] = centers[ck];
const spread = ck === 'Bengaluru' ? 0.09 : 0.06;
out.push({
id: `VH-${type.slice(0, 2).toUpperCase()}-${String(++n).padStart(3, '0')}`,
type,
city: ck,
lat: clat + (rnd() - 0.5) * spread * 2,
lng: clng + (rnd() - 0.5) * spread * 2,
bearing: Math.floor(rnd() * 360),
rider: riderNames[Math.floor(rnd() * riderNames.length)],
status: rnd() > 0.25 ? 'on-trip' : 'idle'
});
}
});
return out;
})();
export const executionStages = [ export const executionStages = [
{ key: 'pickup', title: 'Pickup Execution', items: ['Rider reaches pickup', 'OTP / eSign', 'Photo proof', 'Proof of pickup'], count: 12 }, { key: 'pickup', title: 'Pickup Execution', items: ['Rider reaches pickup', 'OTP / eSign', 'Photo proof', 'Proof of pickup'], count: 12 },
{ key: 'transit', title: 'In-Transit Tracking', items: ['Live GPS', 'Route monitoring', 'ETA updates', 'Geofencing'], count: 96 }, { key: 'transit', title: 'In-Transit Tracking', items: ['Live GPS', 'Route monitoring', 'ETA updates', 'Geofencing'], count: 96 },
@@ -417,3 +632,79 @@ export const integrations = [
{ name: 'Transport Partners', group: 'Partners & Ecosystem', desc: 'Line-haul carriers', status: 'connected', calls: '8 carriers', icon: 'partner' }, { name: 'Transport Partners', group: 'Partners & Ecosystem', desc: 'Line-haul carriers', status: 'connected', calls: '8 carriers', icon: 'partner' },
{ name: 'Warehouse Partners', group: 'Partners & Ecosystem', desc: 'Fulfilment nodes', status: 'pending', calls: '—', icon: 'partner' } { name: 'Warehouse Partners', group: 'Partners & Ecosystem', desc: 'Fulfilment nodes', status: 'pending', calls: '—', icon: 'partner' }
]; ];
// ==============================|| MILETRUTH AI — INSIGHTS & RECOMMENDATIONS ||============================== //
// Powers the dashboard "DoorMile AI Insights" command panel.
// type drives the icon; severity drives the chip colour (high=error, medium=warning, low/info=info).
export const aiInsights = [
{
id: 'AI-201', type: 'route', severity: 'low',
title: '6 deliveries can be consolidated',
detail: 'Six Whitefield drops can merge into one multi-stop run — saves 41 km today.',
action: 'Apply optimized route'
},
{
id: 'AI-202', type: 'delay', severity: 'medium',
title: 'Route congestion detected near Silk Board',
detail: '3 Bengaluru shipments trending +14 min; congestion clearing after 5 PM.',
action: 'Reroute via Sarjapur Road'
},
{
id: 'AI-203', type: 'capacity', severity: 'medium',
title: 'Peelamedu hub utilization at 82%',
detail: 'Coimbatore inbound surge this evening; load factor approaching threshold.',
action: 'Divert overflow to Coimbatore Regional Hub'
},
{
id: 'AI-204', type: 'risk', severity: 'high',
title: 'Reassign rider suggested · DM-10251',
detail: 'R.S. Puram → Saravanampatti at risk after a failed attempt — nearer rider available.',
action: 'Reassign to Sandeep Roy'
},
{
id: 'AI-205', type: 'route', severity: 'info',
title: 'EV-first opportunity · 9 ICE trips swappable',
detail: 'Charging windows align for 9 trips currently on ICE vehicles across BLR & HYD.',
action: 'Schedule EV swap'
},
{
id: 'AI-206', type: 'delay', severity: 'info',
title: 'Line-haul delay risk · BLR → HYD',
detail: 'Weather model predicts 22 min delay after 4 PM on the corridor.',
action: 'Pre-stage backup at Hyderabad Regional Hub'
}
];
// Network capacity utilization (%) — drives the AI panel mini-block.
export const capacityUtilization = { overall: 82, hubs: 76, fleet: 71, riders: 84 };
// ==============================|| EXCEPTION CENTER — LIVE OPERATIONAL ISSUES ||============================== //
// The triage queue an ops manager works first. category drives grouping/filter chips;
// severity → row tone (high=error, medium=warning, low=info); status → workflow state.
export const exceptionQueue = [
{ id: 'DM-10251', category: 'Customer Unavailable', severity: 'high', detail: 'R.S. Puram → Saravanampatti · customer not reachable', owner: 'Imran Sheikh', age: '9 min', status: 'open' },
{ id: 'HUB-CBE-01', category: 'Hub Capacity Exceeded', severity: 'high', detail: 'Peelamedu Micro Hub at 84% · inbound surge', owner: 'Hub ops · CBE', age: '8 min', status: 'open' },
{ id: 'DM-10243', category: 'Traffic Delay', severity: 'medium', detail: 'Gandhipuram → Peelamedu · +18 min on current traffic', owner: 'Imran Sheikh', age: '12 min', status: 'investigating' },
{ id: 'DM-CC-2043', category: 'Weather Delay', severity: 'high', detail: 'Cold-chain breach risk · Bengaluru Pharma Park run', owner: 'MediQuick Pharma', age: '14 min', status: 'open' },
{ id: 'VH-2W-118', category: 'Vehicle Breakdown', severity: 'medium', detail: 'Honda Activa down · Coimbatore Regional Hub', owner: 'Fleet ops · CBE', age: '21 min', status: 'open' },
{ id: 'DM-10244', category: 'Incorrect Address', severity: 'medium', detail: 'Incomplete drop address · R.S. Puram', owner: 'Sandeep Roy', age: '46 min', status: 'assigned' },
{ id: 'DM-10248', category: 'Traffic Delay', severity: 'low', detail: 'Rider off optimized path · Jayanagar loop', owner: 'Faisal Khan', age: '6 min', status: 'monitoring' }
];
// ==============================|| ATTENTION REQUIRED — DECISION CARDS ||============================== //
// Rolled-up counts of what needs a manager's attention now. Each card is a single click into
// the module that owns the workflow. severity → accent (high=error, medium=warning, low=info).
export const attentionItems = [
{ key: 'delayed', label: 'Delayed Orders', count: 7, severity: 'high', icon: 'delay', to: '/orders' },
{ key: 'critical', label: 'Critical Alerts', count: 3, severity: 'high', icon: 'alert', to: '/tracking' },
{ key: 'hub', label: 'Hub Bottleneck', count: 1, severity: 'medium', icon: 'hub', to: '/hubs' },
{ key: 'route', label: 'Route Deviations', count: 2, severity: 'medium', icon: 'route', to: '/dispatch' }
];
// Compact tracking-alert feed for the Live Operations column.
export const trackingAlerts = [
{ id: 'DM-10242', type: 'geofence', detail: 'Approaching drop · 4.2 km · HSR Layout', time: '2 min ago', severity: 'low' },
{ id: 'DM-10251', type: 'exception', detail: 'Customer unavailable · reattempt scheduled', time: '6 min ago', severity: 'high' },
{ id: 'DM-10243', type: 'delay', detail: 'ETA slipping · +18 min vs SLA', time: '11 min ago', severity: 'medium' },
{ id: 'DM-10246', type: 'dispatch', detail: 'Rider en route to pickup · Hitech City', time: '14 min ago', severity: 'low' }
];

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { import {
AppBar, AppBar,
@@ -17,15 +17,17 @@ import {
Tooltip, Tooltip,
Button, Button,
Stack, Stack,
Select,
alpha alpha
} from '@mui/material'; } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu'; import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
import PersonOutlineIcon from '@mui/icons-material/PersonOutline'; import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import LogoutIcon from '@mui/icons-material/Logout'; import LogoutIcon from '@mui/icons-material/Logout';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
@@ -33,33 +35,41 @@ import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
import DoneAllIcon from '@mui/icons-material/DoneAll'; import DoneAllIcon from '@mui/icons-material/DoneAll';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
import { locations } from '@/data/mock';
import { useFilters } from '@/store/Filters';
const RED = '#C01227'; // brand accent (avatars, dots) const RED = '#C01227'; // brand accent (reserved for attention: avatar, unread dots)
const BAR = '#8E1F2A'; // muted deep-brick top bar (toned down from vivid #C01227)
const INITIAL_NOTIFICATIONS = [ const INITIAL_NOTIFICATIONS = [
{ id: 1, icon: Inventory2OutlinedIcon, title: 'New order #ORD-10482 placed', time: '2 min ago', to: '/orders', read: false }, { id: 1, icon: Inventory2OutlinedIcon, title: 'New order #ORD-10482 placed', time: '2 min ago', to: '/orders', read: false },
{ id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/riders', read: false }, { id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/fleet', read: false },
{ id: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false }, { id: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false },
{ id: 4, icon: AssignmentOutlinedIcon, title: 'MileTruth AI re-optimized 41 routes', time: '3 hrs ago', to: '/dispatch', read: true } { id: 4, icon: AssignmentOutlinedIcon, title: 'MileTruth AI re-optimized 41 routes', time: '3 hrs ago', to: '/dispatch', read: true }
]; ];
const MESSAGES = [
{ id: 1, name: 'Priya Nair', text: 'Can we reroute the MG Road batch?', time: '5 min ago', initials: 'PN' },
{ id: 2, name: 'Imran Khan', text: 'Reached the warehouse, loading now.', time: '22 min ago', initials: 'IK' },
{ id: 3, name: 'Acme Logistics', text: 'Please confirm the revised pricing.', time: '2 hrs ago', initials: 'AL' }
];
export default function Header({ onToggle }) { export default function Header({ onToggle }) {
const navigate = useNavigate(); const navigate = useNavigate();
const [account, setAccount] = useState(null); const [account, setAccount] = useState(null);
const [notifAnchor, setNotifAnchor] = useState(null); const [notifAnchor, setNotifAnchor] = useState(null);
const [msgAnchor, setMsgAnchor] = useState(null);
const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS); const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const { location, setLocation } = useFilters(); // global location — single source of truth
const searchRef = useRef(null);
const unread = notifications.filter((n) => !n.read).length; const unread = notifications.filter((n) => !n.read).length;
// ⌘K / Ctrl+K focuses global search
useEffect(() => {
const onKey = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
searchRef.current?.focus();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const openNotif = (e) => setNotifAnchor(e.currentTarget); const openNotif = (e) => setNotifAnchor(e.currentTarget);
const closeNotif = () => setNotifAnchor(null); const closeNotif = () => setNotifAnchor(null);
const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
@@ -79,76 +89,124 @@ export default function Header({ onToggle }) {
<AppBar <AppBar
position="fixed" position="fixed"
elevation={0} elevation={0}
sx={{ bgcolor: BAR, color: '#fff', zIndex: (t) => t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }} sx={{
bgcolor: '#FFFFFF',
color: 'grey.800',
zIndex: (t) => t.zIndex.drawer + 1,
borderBottom: '1px solid',
borderColor: 'grey.200'
}}
> >
<Toolbar sx={{ minHeight: 64, px: { xs: 1.5, sm: 2.5 }, gap: 1 }}> <Toolbar sx={{ minHeight: 64, px: { xs: 1.5, sm: 2.5 }, gap: 1 }}>
<IconButton color="inherit" edge="start" onClick={onToggle} sx={{ mr: 0.5 }}> {/* LEFT — hamburger + brand (equal-flex zone) */}
<MenuIcon /> <Box sx={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
</IconButton> <IconButton edge="start" onClick={onToggle} sx={{ color: 'grey.700' }}>
<MenuIcon />
{/* Brand wordmark — left side */} </IconButton>
<Box <Box
onClick={() => navigate('/dashboard')} onClick={() => navigate('/dashboard')}
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer', flexShrink: 0 }}
> >
<Logo onDark height={22} /> <Logo height={24} />
</Box>
</Box> </Box>
<Box sx={{ flexGrow: 1 }} /> {/* CENTER — global search, the primary nav element (fixed width = stays truly centered) */}
{/* Search — moved to the right */}
<Box <Box
component="form" component="form"
onSubmit={submitSearch} onSubmit={submitSearch}
sx={{ sx={{
display: { xs: 'none', sm: 'flex' }, display: { xs: 'none', sm: 'flex' },
flexShrink: 0,
alignItems: 'center', alignItems: 'center',
bgcolor: alpha('#fff', 0.16), height: 46,
borderRadius: 2, px: 1.75,
px: 1.5, bgcolor: 'grey.50',
py: 0.5, border: '1px solid',
width: { sm: 240, md: 320 }, borderColor: 'grey.200',
'&:hover': { bgcolor: alpha('#fff', 0.22) }, borderRadius: 2.5,
'&:focus-within': { bgcolor: alpha('#fff', 0.26) } width: { sm: 300, md: 460, lg: 560 },
transition: 'all 0.15s ease',
'&:hover': { borderColor: 'grey.300' },
'&:focus-within': { borderColor: 'primary.main', bgcolor: '#fff', boxShadow: '0 0 0 3px rgba(192,18,39,0.08)' }
}} }}
> >
<SearchIcon sx={{ fontSize: 20, mr: 1, opacity: 0.9 }} /> <SearchIcon sx={{ fontSize: 20, mr: 1, color: 'grey.500' }} />
<InputBase <InputBase
inputRef={searchRef}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="Search orders, riders, customers…" placeholder="Search orders, shipments, riders, customers…"
sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }} sx={{ color: 'grey.800', fontSize: '0.875rem', flex: 1 }}
inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }} inputProps={{ 'aria-label': 'search' }}
/> />
<Box
sx={{
display: { xs: 'none', md: 'flex' },
alignItems: 'center',
gap: 0.25,
px: 0.75,
py: 0.4,
ml: 1,
borderRadius: 1,
border: '1px solid',
borderColor: 'grey.300',
bgcolor: '#fff',
color: 'grey.500',
fontSize: '0.6875rem',
fontWeight: 700,
lineHeight: 1,
flexShrink: 0
}}
>
K
</Box>
</Box> </Box>
<Tooltip title="Messages"> {/* RIGHT — location + notifications + profile (equal-flex zone, right-aligned) */}
<IconButton color="inherit" onClick={(e) => setMsgAnchor(e.currentTarget)}> <Box sx={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.5 }}>
<Badge badgeContent={MESSAGES.length} color="warning"> <Select
<ChatBubbleOutlineIcon /> value={location}
</Badge> onChange={(e) => setLocation(e.target.value)}
</IconButton> size="small"
</Tooltip> IconComponent={KeyboardArrowDownIcon}
<Tooltip title="Notifications"> startAdornment={<PlaceOutlinedIcon sx={{ fontSize: 18, color: 'grey.500', mr: 0.75 }} />}
<IconButton color="inherit" onClick={openNotif}> sx={{
<Badge badgeContent={unread} color="warning"> display: { xs: 'none', md: 'flex' },
<NotificationsNoneIcon /> minWidth: 168,
</Badge> bgcolor: 'grey.50',
</IconButton> '& .MuiOutlinedInput-notchedOutline': { borderColor: 'grey.200' },
</Tooltip> '& .MuiSelect-select': { display: 'flex', alignItems: 'center', fontSize: '0.8125rem', fontWeight: 600, color: 'grey.700' }
}}
>
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => (
<MenuItem key={l} value={l}>{l}</MenuItem>
))}
</Select>
<Box <Tooltip title="Notifications">
onClick={(e) => setAccount(e.currentTarget)} <IconButton onClick={openNotif} sx={{ color: 'grey.700' }}>
sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 0.5, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: alpha('#fff', 0.14) } }} <Badge badgeContent={unread} color="error">
> <NotificationsNoneIcon />
<Avatar sx={{ width: 34, height: 34, bgcolor: '#fff', color: RED, fontWeight: 700 }}>AD</Avatar> </Badge>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}> </IconButton>
<Typography variant="subtitle2" sx={{ color: '#fff', fontWeight: 600 }}> </Tooltip>
Aman Deshmukh
</Typography> <Box
<Typography variant="caption" sx={{ color: alpha('#fff', 0.8) }}> onClick={(e) => setAccount(e.currentTarget)}
Operations Admin sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: 'grey.100' } }}
</Typography> >
<Avatar sx={{ width: 34, height: 34, bgcolor: RED, color: '#fff', fontWeight: 700, fontSize: '0.8rem' }}>AD</Avatar>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
<Typography variant="subtitle2" sx={{ color: 'grey.800', fontWeight: 600 }}>
Aman Deshmukh
</Typography>
<Typography variant="caption" sx={{ color: 'grey.500' }}>
Operations Admin
</Typography>
</Box>
<KeyboardArrowDownIcon sx={{ fontSize: 18, color: 'grey.500', display: { xs: 'none', md: 'block' } }} />
</Box> </Box>
</Box> </Box>
@@ -180,7 +238,7 @@ export default function Header({ onToggle }) {
return ( return (
<MenuItem key={n.id} onClick={() => onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}> <MenuItem key={n.id} onClick={() => onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}> <ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: n.read ? 'grey.200' : alpha(RED, 0.12), color: RED }}> <Avatar sx={{ width: 34, height: 34, bgcolor: n.read ? 'grey.100' : alpha(RED, 0.12), color: n.read ? 'grey.500' : RED }}>
<Icon fontSize="small" /> <Icon fontSize="small" />
</Avatar> </Avatar>
</ListItemIcon> </ListItemIcon>
@@ -200,39 +258,6 @@ export default function Header({ onToggle }) {
</MenuItem> </MenuItem>
</Menu> </Menu>
{/* Messages dropdown */}
<Menu
anchorEl={msgAnchor}
open={Boolean(msgAnchor)}
onClose={() => setMsgAnchor(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, width: 340, maxWidth: '90vw' } }}
>
<Typography variant="subtitle1" sx={{ fontWeight: 700, px: 2, py: 1.25 }}>
Messages
</Typography>
<Divider />
{MESSAGES.map((m) => (
<MenuItem key={m.id} onClick={() => setMsgAnchor(null)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: alpha(RED, 0.12), color: RED, fontWeight: 700, fontSize: '0.8rem' }}>
{m.initials}
</Avatar>
</ListItemIcon>
<ListItemText
primary={m.name}
secondary={m.text}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 700 }}
secondaryTypographyProps={{ fontSize: '0.8rem' }}
/>
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, mt: 0.5, flexShrink: 0 }}>
{m.time}
</Typography>
</MenuItem>
))}
</Menu>
{/* Account dropdown */} {/* Account dropdown */}
<Menu <Menu
anchorEl={account} anchorEl={account}

View File

@@ -1,4 +1,3 @@
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { import {
Drawer, Drawer,
@@ -8,52 +7,62 @@ import {
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
Typography, Typography,
Collapse,
Tooltip, Tooltip,
Toolbar Toolbar
} from '@mui/material'; } from '@mui/material';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
import navItems from '@/menu/navItems'; import navItems from '@/menu/navItems';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
export const DRAWER_WIDTH = 232; export const DRAWER_WIDTH = 200;
export const MINI_WIDTH = 76; export const MINI_WIDTH = 68;
const NAV_BG = '#8E1F2A'; // muted deep-brick brand red (toned down from vivid #C01227) const BRAND = '#C01227';
function NavLeaf({ item, open, active, depth = 0, onClick }) { // Light, operational sidebar — white surface, tight grouping, a single strong red active state.
function NavLeaf({ item, open, active, onClick }) {
const Icon = item.icon; const Icon = item.icon;
const button = ( const button = (
<ListItemButton <ListItemButton
selected={active} selected={active}
onClick={onClick} onClick={onClick}
sx={{ sx={{
minHeight: 44, position: 'relative',
my: 0.25, minHeight: 40,
mx: open ? 1 : 0.75, my: 0.2,
px: open ? 1.5 : 0, mx: open ? 0.75 : 0.5,
px: open ? 1.25 : 0,
justifyContent: open ? 'flex-start' : 'center', justifyContent: open ? 'flex-start' : 'center',
borderRadius: 2, borderRadius: 1.5,
color: 'rgba(255,255,255,0.78)', color: 'grey.800',
'& .MuiListItemIcon-root': { color: 'inherit' }, transition: 'background-color .12s ease, color .12s ease',
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' }, '& .MuiListItemIcon-root': { color: '#B0566A', transition: 'color .12s ease' },
'&:hover': { bgcolor: 'rgba(192,18,39,0.07)', color: BRAND, '& .MuiListItemIcon-root': { color: BRAND } },
'&.Mui-selected': { '&.Mui-selected': {
bgcolor: 'rgba(255,255,255,0.18)', bgcolor: 'rgba(192,18,39,0.14)',
color: '#fff', color: BRAND,
'&:hover': { bgcolor: 'rgba(255,255,255,0.22)' } '& .MuiListItemIcon-root': { color: BRAND },
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: 5,
bottom: 5,
width: 3,
borderRadius: '0 3px 3px 0',
bgcolor: BRAND
},
'&:hover': { bgcolor: 'rgba(192,18,39,0.18)' }
} }
}} }}
> >
<ListItemIcon sx={{ minWidth: open ? 34 : 'auto', justifyContent: 'center' }}> <ListItemIcon sx={{ minWidth: open ? 30 : 'auto', justifyContent: 'center' }}>
{depth > 0 && !Icon ? <FiberManualRecordIcon sx={{ fontSize: 8 }} /> : Icon ? <Icon fontSize="small" /> : null} <Icon sx={{ fontSize: 20 }} />
</ListItemIcon> </ListItemIcon>
{open && ( {open && (
<ListItemText <ListItemText
primary={item.title} primary={item.title}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: active ? 700 : 500 }} primaryTypographyProps={{ fontSize: '0.82rem', fontWeight: active ? 700 : 500, letterSpacing: '0.01em' }}
/> />
)} )}
</ListItemButton> </ListItemButton>
@@ -68,122 +77,66 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
const isActive = (url) => url && location.pathname.startsWith(url); const isActive = (url) => url && location.pathname.startsWith(url);
const expanded = open || isMobile; const expanded = open || isMobile;
const initialOpen = navItems
.flatMap((g) => g.items)
.filter((i) => i.children && i.children.some((c) => isActive(c.url)))
.map((i) => i.id);
const [collapse, setCollapse] = useState(initialOpen);
const go = (url) => { const go = (url) => {
navigate(url); navigate(url);
if (isMobile) onMobileClose(); if (isMobile) onMobileClose();
}; };
const content = ( const content = (
<Box sx={{ bgcolor: NAV_BG, height: '100%', color: '#fff', display: 'flex', flexDirection: 'column' }}> <Box sx={{ bgcolor: '#FFF5F5', height: '100%', display: 'flex', flexDirection: 'column' }}>
<Toolbar sx={{ px: expanded ? 2.5 : 0, justifyContent: expanded ? 'flex-start' : 'center', minHeight: 64 }}> {/* brand — restrained height, generous top alignment */}
<Logo onDark compact={!expanded} /> <Toolbar
sx={{
px: expanded ? 2.25 : 0,
pt: 0.5,
justifyContent: expanded ? 'flex-start' : 'center',
minHeight: 60,
borderBottom: '1px solid',
borderColor: 'rgba(192,18,39,0.10)'
}}
>
<Logo compact={!expanded} height={16} />
</Toolbar> </Toolbar>
{/* navigation */}
<Box <Box
sx={{ sx={{
overflowY: 'auto', overflowY: 'auto',
overflowX: 'hidden', overflowX: 'hidden',
flexGrow: 1, flexGrow: 1,
pb: 2, py: 1,
// slim, subtle scrollbar tuned for the dark-red sidebar — only shows on hover
scrollbarWidth: 'thin', scrollbarWidth: 'thin',
scrollbarColor: 'transparent transparent',
'&:hover': { scrollbarColor: 'rgba(255,255,255,0.3) transparent' },
'&::-webkit-scrollbar': { width: 6 }, '&::-webkit-scrollbar': { width: 6 },
'&::-webkit-scrollbar-track': { background: 'transparent' }, '&::-webkit-scrollbar-thumb': { backgroundColor: 'transparent', borderRadius: 8 },
'&::-webkit-scrollbar-thumb': { '&:hover::-webkit-scrollbar-thumb': { backgroundColor: 'rgba(0,0,0,0.18)' }
backgroundColor: 'transparent',
borderRadius: 8,
transition: 'background-color 0.2s ease'
},
'&:hover::-webkit-scrollbar-thumb': { backgroundColor: 'rgba(255,255,255,0.28)' },
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: 'rgba(255,255,255,0.45)' }
}} }}
> >
{navItems.map((grp) => ( {navItems.map((grp) => (
<Box key={grp.group} sx={{ mt: 1 }}> <Box key={grp.group || grp.items[0].id} sx={{ mt: grp.group ? 1.25 : 0.25 }}>
{expanded && ( {expanded && grp.group && (
<Typography <Typography
variant="overline" sx={{
sx={{ px: 2.5, color: 'rgba(255,255,255,0.55)', fontSize: '0.6875rem', letterSpacing: '0.08em' }} display: 'block',
px: 2,
mb: 0.25,
color: 'rgba(158,14,32,0.62)',
fontSize: '0.6875rem',
fontWeight: 800,
textTransform: 'uppercase',
letterSpacing: '0.13em'
}}
> >
{grp.group} {grp.group}
</Typography> </Typography>
)} )}
<List disablePadding sx={{ mt: 0.5 }}> <List disablePadding>
{grp.items.map((item) => { {grp.items.map((item) => (
if (item.children) { <NavLeaf key={item.id} item={item} open={expanded} active={isActive(item.url)} onClick={() => go(item.url)} />
const opened = collapse.includes(item.id); ))}
const childActive = item.children.some((c) => isActive(c.url));
const Icon = item.icon;
const head = (
<ListItemButton
onClick={() =>
expanded
? setCollapse((p) => (p.includes(item.id) ? p.filter((x) => x !== item.id) : [...p, item.id]))
: go(item.children[0].url)
}
sx={{
minHeight: 44,
my: 0.25,
mx: expanded ? 1 : 0.75,
px: expanded ? 1.5 : 0,
justifyContent: expanded ? 'flex-start' : 'center',
borderRadius: 2,
color: childActive ? '#fff' : 'rgba(255,255,255,0.78)',
bgcolor: childActive && !opened ? 'rgba(255,255,255,0.12)' : 'transparent',
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' }
}}
>
<ListItemIcon sx={{ minWidth: expanded ? 34 : 'auto', justifyContent: 'center', color: 'inherit' }}>
<Icon fontSize="small" />
</ListItemIcon>
{expanded && (
<>
<ListItemText primary={item.title} primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 500 }} />
{opened ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
</>
)}
</ListItemButton>
);
return (
<Box key={item.id}>
{expanded ? head : <Tooltip title={item.title} placement="right">{head}</Tooltip>}
{expanded && (
<Collapse in={opened} timeout="auto" unmountOnExit>
<Box sx={{ pl: 1.5 }}>
{item.children.map((c) => (
<NavLeaf key={c.id} item={c} open depth={1} active={isActive(c.url)} onClick={() => go(c.url)} />
))}
</Box>
</Collapse>
)}
</Box>
);
}
return (
<NavLeaf key={item.id} item={item} open={expanded} active={isActive(item.url)} onClick={() => go(item.url)} />
);
})}
</List> </List>
</Box> </Box>
))} ))}
</Box> </Box>
{expanded && (
<Box sx={{ p: 2, borderTop: '1px solid rgba(255,255,255,0.12)' }}>
<Typography variant="caption" sx={{ color: '#fff', fontWeight: 600, display: 'block', lineHeight: 1.3 }}>
Delivering Trust.
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
Beyond Boundaries · v1.0
</Typography>
</Box>
)}
</Box> </Box>
); );
@@ -201,23 +154,24 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
); );
} }
// Desktop: an in-flow grid-column element (NOT a fixed Drawer paper) so it can never be clipped
// or overlap the content. Width comes from the layout grid; this just fills its column and pins
// to the viewport while content scrolls.
return ( return (
<Drawer <Box
variant="permanent" component="aside"
sx={{ sx={{
width: open ? DRAWER_WIDTH : MINI_WIDTH, gridColumn: 1,
flexShrink: 0, position: 'sticky',
whiteSpace: 'nowrap', top: 0,
'& .MuiDrawer-paper': { height: '100vh',
width: open ? DRAWER_WIDTH : MINI_WIDTH, overflow: 'hidden',
border: 'none', borderRight: '1px solid',
overflowX: 'hidden', borderColor: 'rgba(192,18,39,0.14)',
transition: (t) => t.transitions.create('width', { duration: t.transitions.duration.standard }) boxShadow: '1px 0 4px rgba(192,18,39,0.05)'
}
}} }}
open={open}
> >
{content} {content}
</Drawer> </Box>
); );
} }

View File

@@ -6,9 +6,16 @@ import { useTheme } from '@mui/material/styles';
import Header from './Header'; import Header from './Header';
import Sidebar, { DRAWER_WIDTH, MINI_WIDTH } from './Sidebar'; import Sidebar, { DRAWER_WIDTH, MINI_WIDTH } from './Sidebar';
// ==============================|| MAIN LAYOUT — CSS GRID SHELL ||============================== //
// One deterministic grid: [ sidebar (fixed px) | content (flexible, minWidth 0) ].
// The sidebar width is the single source of truth (exported from Sidebar.jsx). No width-calc
// hacks, no negative margins, no fixed-paper overlap — the content column begins exactly where
// the sidebar ends, the sidebar can never be clipped, and content can never overflow horizontally.
export default function MainLayout() { export default function MainLayout() {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('lg')); // Sidebar stays a permanent column on md+ (and through browser zoom); overlay drawer below md.
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
@@ -17,26 +24,27 @@ export default function MainLayout() {
else setOpen((p) => !p); else setOpen((p) => !p);
}; };
const sidebarW = open ? DRAWER_WIDTH : MINI_WIDTH;
return ( return (
<Box sx={{ display: 'flex', bgcolor: 'background.default', minHeight: '100vh' }}> <Box
sx={{
minHeight: '100vh',
bgcolor: 'background.default',
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: `${sidebarW}px minmax(0, 1fr)` },
transition: theme.transitions.create('grid-template-columns', { duration: theme.transitions.duration.standard })
}}
>
<Header onToggle={toggle} /> <Header onToggle={toggle} />
<Sidebar
open={open} {/* column 1 on md+ (in-flow); overlay drawer on mobile (taken out of flow) */}
isMobile={isMobile} <Sidebar open={open} isMobile={isMobile} mobileOpen={mobileOpen} onMobileClose={() => setMobileOpen(false)} />
mobileOpen={mobileOpen}
onMobileClose={() => setMobileOpen(false)} {/* column 2 — content; minWidth:0 + overflowX:clip guarantee no horizontal spill */}
/> <Box component="main" sx={{ minWidth: 0, minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Box <Toolbar sx={{ minHeight: 64, flexShrink: 0 }} />
component="main" <Box sx={{ p: { xs: 2, sm: 3 }, flexGrow: 1, minWidth: 0, overflowX: 'clip' }}>
sx={{
flexGrow: 1,
width: { lg: `calc(100% - ${open ? DRAWER_WIDTH : MINI_WIDTH}px)` },
minHeight: '100vh',
transition: theme.transitions.create('width', { duration: theme.transitions.duration.standard })
}}
>
<Toolbar sx={{ minHeight: 64 }} />
<Box sx={{ p: { xs: 2, sm: 3 } }}>
<Outlet /> <Outlet />
</Box> </Box>
</Box> </Box>

View File

@@ -7,6 +7,8 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import theme from '@/theme'; import theme from '@/theme';
import App from '@/App'; import App from '@/App';
import { OpsProvider } from '@/store/OpsStore';
import { FilterProvider } from '@/store/Filters';
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
@@ -14,7 +16,11 @@ ReactDOM.createRoot(document.getElementById('root')).render(
<CssBaseline /> <CssBaseline />
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
<BrowserRouter> <BrowserRouter>
<App /> <FilterProvider>
<OpsProvider>
<App />
</OpsProvider>
</FilterProvider>
</BrowserRouter> </BrowserRouter>
</LocalizationProvider> </LocalizationProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -1,102 +1,50 @@
import DashboardOutlinedIcon from '@mui/icons-material/DashboardOutlined'; import SpaceDashboardOutlinedIcon from '@mui/icons-material/SpaceDashboardOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined';
import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined';
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
import SummarizeOutlinedIcon from '@mui/icons-material/SummarizeOutlined';
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
import SecurityOutlinedIcon from '@mui/icons-material/SecurityOutlined';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'; import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined';
import TrendingUpOutlinedIcon from '@mui/icons-material/TrendingUpOutlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import ApiOutlinedIcon from '@mui/icons-material/ApiOutlined'; import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined'; import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
// ==============================|| DOORMILE - LOGISTICS OPERATING SYSTEM NAV ||============================== // // ==============================|| DOORMILE LOGISTICS OS NAVIGATION ||============================== //
// Groups mirror the 8-layer end-to-end flow of the Doormile operating system. // Consolidated operational IA. The first group has no label (single Control Center entry — no
// redundant header). All icons are the Material *Outlined* family for one consistent visual weight.
const navItems = [ const navItems = [
{ {
group: 'Overview', group: '',
items: [{ id: 'dashboard', title: 'Control Center', url: '/dashboard', icon: SpaceDashboardOutlinedIcon }]
},
{
group: 'Operations',
items: [ items: [
{ id: 'dashboard', title: 'System Overview', url: '/dashboard', icon: DashboardOutlinedIcon }, { id: 'dispatch-tracking', title: 'Dispatch & Tracking', url: '/tracking', icon: MyLocationOutlinedIcon },
{ id: 'three-mile', title: 'Three-Mile Network', url: '/three-mile', icon: RouteOutlinedIcon } { id: 'shipments', title: 'Shipments', url: '/orders', icon: Inventory2OutlinedIcon }
] ]
}, },
{ {
group: '1 · Book & Create', group: 'Network',
items: [ items: [
{ id: 'orders', title: 'Shipments', url: '/orders', icon: Inventory2OutlinedIcon }, { id: 'fleet', title: 'Fleet', url: '/fleet', icon: LocalShippingOutlinedIcon },
{ id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon } { id: 'hubs', title: 'Hubs', url: '/hubs', icon: HubOutlinedIcon }
] ]
}, },
{ {
group: '2 · Trust & Identity', group: 'Business',
items: [{ id: 'trust', title: 'Trust & Compliance', url: '/trust', icon: SecurityOutlinedIcon }]
},
{
group: '3 · Hub Network',
items: [{ id: 'hubs', title: 'Hub Network', url: '/hubs', icon: HubOutlinedIcon }]
},
{
group: '4 · Fleet & Riders',
items: [ items: [
{ id: 'fleet', title: 'Fleet', url: '/fleet', icon: ElectricRickshawOutlinedIcon }, { id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon },
{ id: 'riders', title: 'Riders', url: '/riders', icon: TwoWheelerOutlinedIcon } { id: 'finance', title: 'Finance', url: '/invoice', icon: PaymentsOutlinedIcon }
] ]
}, },
{ {
group: '5 · MileTruth AI', group: 'Insights',
items: [{ id: 'dispatch', title: 'AI Dispatch', url: '/dispatch', icon: AutoAwesomeOutlinedIcon }] items: [{ id: 'reports', title: 'Reports', url: '/reports', icon: BarChartOutlinedIcon }]
}, },
{ {
group: '6 · Execution', group: 'System',
items: [ items: [{ id: 'settings', title: 'Settings', url: '/settings', icon: SettingsOutlinedIcon }]
{ id: 'tracking', title: 'Live Tracking', url: '/tracking', icon: MyLocationOutlinedIcon },
{ id: 'journey', title: 'Shipment Journey', url: '/tracking/journey', icon: AltRouteOutlinedIcon },
{ id: 'cold-chain', title: 'Cold Chain', url: '/cold-chain', icon: AcUnitOutlinedIcon },
{ id: 'deliveries', title: 'Deliveries', url: '/deliveries', icon: MopedOutlinedIcon }
]
},
{
group: '7 · Analytics',
items: [
{ id: 'analytics', title: 'Analytics', url: '/analytics', icon: TrendingUpOutlinedIcon },
{
id: 'reports',
title: 'Reports',
icon: BarChartOutlinedIcon,
children: [
{ id: 'orders-summary', title: 'Order Summary', url: '/reports/orders-summary', icon: SummarizeOutlinedIcon },
{ id: 'orders-details', title: 'Order Details', url: '/reports/orders-details', icon: FactCheckOutlinedIcon },
{ id: 'riders-summary', title: 'Riders Summary', url: '/reports/riders-summary', icon: TwoWheelerOutlinedIcon },
{ id: 'riders-logs', title: 'Riders Logs', url: '/reports/riders-logs', icon: MapOutlinedIcon }
]
}
]
},
{
group: '8 · Integrations',
items: [
{ id: 'integrations', title: 'Integrations', url: '/integrations', icon: ApiOutlinedIcon },
{ id: 'tenants', title: 'Clients', url: '/tenants', icon: ApartmentOutlinedIcon }
]
},
{
group: 'Finance',
items: [
{ id: 'pricing', title: 'Pricing', url: '/pricing', icon: PaymentsOutlinedIcon },
{ id: 'invoice', title: 'Invoice', url: '/invoice', icon: ReceiptLongOutlinedIcon }
]
} }
]; ];

View File

@@ -1,225 +1,237 @@
import { Grid, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, MenuItem, TextField, Avatar, LinearProgress, Chip } from '@mui/material'; import { useNavigate } from 'react-router-dom';
import { Grid, Stack, Typography, Box, Button, Divider, LinearProgress, Chip, Avatar } from '@mui/material';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee'; import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined'; import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
import SpeedOutlinedIcon from '@mui/icons-material/SpeedOutlined';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard'; import KpiStrip from '@/components/KpiStrip';
import MainCard from '@/components/MainCard'; import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip'; import StatusChip from '@/components/StatusChip';
import AreaChart from '@/components/charts/AreaChart'; import AreaChart from '@/components/charts/AreaChart';
import DonutChart from '@/components/charts/DonutChart'; import ProcessTracker from '@/components/ProcessTracker';
import UserAvatar from '@/components/UserAvatar'; import AiImpactSummary from '@/components/AiImpactSummary';
import SystemPipeline from '@/components/SystemPipeline';
import ThreeMileStrip from '@/components/ThreeMileStrip';
import Toast, { useToast } from '@/components/Toast'; import Toast, { useToast } from '@/components/Toast';
import { ordersTrend, statusBreakdown, orders, riders, aiMetrics, fleetSummary, verticals, verticalOf } from '@/data/mock'; import { dispatchQueue, activeDeliveries, aiInsights, executionFeed, fleetSummary, lanePerformance, hubCityStats, ordersTrend, analyticsKpis } from '@/data/mock';
import { inr } from '@/utils/format'; import { inr } from '@/utils/format';
const VERTICAL_COLOR = Object.fromEntries(verticals.map((v) => [v.label, v.color])); const SEV_DOT = { high: '#F04134', medium: '#FFBF00', low: '#00A2AE', info: '#8C8C8C' };
const hubUtil = Math.round(hubCityStats.reduce((s, h) => s + h.utilization, 0) / hubCityStats.length);
function SectionLabel({ children }) {
return <Typography variant="overline" color="text.secondary" sx={{ letterSpacing: '0.08em', display: 'block', mb: 1.25 }}>{children}</Typography>;
}
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate();
const [toast, showToast] = useToast(); const [toast, showToast] = useToast();
const priority = activeDeliveries.filter((d) => (d.priority === 'high' || d.priority === 'express') && d.status !== 'Delivered').slice(0, 4);
const delayed = activeDeliveries.filter((d) => d.etaStatus !== 'on-time' && d.status !== 'Delivered').slice(0, 4);
const recs = aiInsights.slice(0, 4);
const kpis = [
{ label: 'Total Orders', value: '1,402', icon: Inventory2OutlinedIcon },
{ label: 'Active Shipments', value: '96', color: '#1D4ED8', icon: LocalShippingOutlinedIcon },
{ label: 'Riders Online', value: '48', color: '#00773B', icon: TwoWheelerOutlinedIcon },
{ label: 'Hub Utilization', value: `${hubUtil}%`, color: hubUtil > 80 ? '#A82216' : '#8A6500', icon: HubOutlinedIcon },
{ label: 'Revenue Today', value: inr(384200), color: '#00727B', icon: CurrencyRupeeIcon },
{ label: 'SLA Performance', value: `${analyticsKpis.slaAchievement}%`, color: '#00773B', icon: TaskAltOutlinedIcon }
];
return ( return (
<> <>
<PageHeader <PageHeader
title="System Overview" title="Operations Control Center"
breadcrumbs={[{ label: 'Dashboard' }]} breadcrumbs={[{ label: 'Control Center' }]}
action={ action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('Snapshot exported as CSV')}>Export</Button>}
<Stack direction="row" spacing={1.5}>
<TextField select size="small" defaultValue="all" sx={{ minWidth: 150 }}>
<MenuItem value="all">All Locations</MenuItem>
<MenuItem value="blr">Bengaluru</MenuItem>
<MenuItem value="mum">Mumbai</MenuItem>
</TextField>
<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('System overview exported as CSV')}>Export</Button>
</Stack>
}
/> />
{/* End-to-end operating-system pipeline */} {/* Top row — 6 live KPIs */}
<Box sx={{ mb: 1 }}> <KpiStrip items={kpis} />
<Typography variant="overline" color="text.secondary" sx={{ letterSpacing: '0.08em' }}>
End-to-End Intelligent Logistics Flow
</Typography>
</Box>
<Box sx={{ mb: 3 }}>
<SystemPipeline />
</Box>
{/* Three-Mile model — First → Mid → Last */} {/* Second row — dispatch intelligence */}
<Box sx={{ mb: 0.5 }}> <Box sx={{ mt: 1 }}>
<Typography variant="overline" color="text.secondary" sx={{ letterSpacing: '0.08em' }}> <SectionLabel>Dispatch & Exceptions</SectionLabel>
Three-Mile Network · One Connected System <Grid container spacing={2.5}>
</Typography> <Grid item xs={12} md={6} lg={3}>
</Box> <MainCard title="Live Dispatch Queue" action={<Button size="small" onClick={() => navigate('/orders/assign')} sx={{ fontWeight: 600 }}>Open</Button>} noPadding>
<Box sx={{ mb: 3 }}> <Stack divider={<Divider />}>
<ThreeMileStrip compact /> {dispatchQueue.map((d) => (
</Box> <Box key={d.id} sx={{ px: 2, py: 1.25 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Grid container spacing={2.5}> <Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main' }}>{d.id}</Typography>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Shipments" value="1,402" icon={Inventory2OutlinedIcon} trend={8.4} caption="vs last month" /></Grid> <Chip size="small" label={`${d.confidence}%`} sx={{ height: 18, bgcolor: 'grey.100', fontWeight: 700, '& .MuiChip-label': { px: 0.75, fontSize: 10 } }} />
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered" value="1,330" icon={LocalShippingOutlinedIcon} color="success" trend={6.1} caption="98.6% on-time" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active Riders" value="48" icon={TwoWheelerOutlinedIcon} color="info" trend={-2.3} caption="of 124 fleet" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Revenue" value={inr(384200)} icon={CurrencyRupeeIcon} color="warning" trend={11.7} caption="vs last month" /></Grid>
<Grid item xs={12} lg={8}>
<MainCard
title="Orders Overview"
action={<Stack direction="row" spacing={2}><Legend color="#C01227" label="Orders" /><Legend color="#00A854" label="Delivered" /></Stack>}
>
<AreaChart
labels={ordersTrend.map((d) => d.m)}
series={[
{ name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) },
{ name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) }
]}
/>
</MainCard>
</Grid>
<Grid item xs={12} lg={4}>
<MainCard title="Order Status">
<Box sx={{ py: 2 }}>
<DonutChart data={statusBreakdown} centerValue="1,402" centerLabel="Orders" />
</Box>
</MainCard>
</Grid>
{/* MileTruth AI + Sustainability */}
<Grid item xs={12} lg={5}>
<MainCard
title={
<Stack direction="row" spacing={1} alignItems="center">
<Avatar variant="rounded" sx={{ bgcolor: '#FFF1E6', color: '#EA580C', width: 32, height: 32 }}><AutoAwesomeOutlinedIcon fontSize="small" /></Avatar>
<Typography variant="h5">MileTruth AI Engine</Typography>
</Stack>
}
>
<Grid container spacing={2}>
<Grid item xs={6}><AiStat icon={RouteOutlinedIcon} color="#C01227" value={`${aiMetrics.routeSavings}%`} label="Route savings" /></Grid>
<Grid item xs={6}><AiStat icon={SpeedOutlinedIcon} color="#C01227" value={`${aiMetrics.avgEtaAccuracy}%`} label="ETA accuracy" /></Grid>
<Grid item xs={6}><AiStat icon={AutoAwesomeOutlinedIcon} color="#C01227" value={aiMetrics.reoptToday} label="Re-optimizations today" /></Grid>
<Grid item xs={6}><AiStat icon={LocalShippingOutlinedIcon} color="#C01227" value={`${aiMetrics.delaysAvoided}/${aiMetrics.delaysPredicted}`} label="Delays avoided" /></Grid>
</Grid>
</MainCard>
</Grid>
<Grid item xs={12} lg={3}>
<MainCard title="EV-First Operations">
<Stack alignItems="center" spacing={1} sx={{ py: 1 }}>
<Avatar variant="rounded" sx={{ bgcolor: 'success.lighter', color: 'success.main', width: 48, height: 48 }}><EnergySavingsLeafOutlinedIcon /></Avatar>
<Typography variant="h2" sx={{ fontWeight: 800, color: 'success.main' }}>{fleetSummary.evShare}%</Typography>
<Typography variant="caption" color="text.secondary">EV fleet share</Typography>
<Box sx={{ width: '100%', mt: 1 }}>
<LinearProgress variant="determinate" value={fleetSummary.evShare} color="success" sx={{ height: 8, borderRadius: 4 }} />
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>{(fleetSummary.co2SavedKg / 1000).toFixed(1)}t CO₂ saved this month</Typography>
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} lg={4}>
<MainCard title="Top Riders Today">
<Stack divider={<Divider />} spacing={0}>
{riders.slice(0, 4).map((r, i) => (
<Stack key={r.id} direction="row" spacing={2} alignItems="center" sx={{ py: 1.1 }}>
<Typography variant="subtitle2" color="text.secondary" sx={{ width: 18 }}>{i + 1}</Typography>
<UserAvatar name={r.name} size={36} />
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle2">{r.name}</Typography>
<Typography variant="caption" color="text.secondary">{r.vehicle} · {r.rating}</Typography>
</Box>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="subtitle2">{r.deliveries}</Typography>
<Typography variant="caption" color="text.secondary">deliveries</Typography>
</Box>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} lg={4}>
<MainCard title="By Industry Vertical">
<Stack spacing={1.5}>
{verticals.map((v) => (
<Box key={v.key} sx={{ border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 1.5 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Stack direction="row" spacing={1} alignItems="center">
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: v.color }} />
<Box>
<Typography variant="subtitle2">{v.label}</Typography>
<Typography variant="caption" color="text.secondary">{v.desc}</Typography>
</Box>
</Stack> </Stack>
<Box sx={{ textAlign: 'right' }}> <Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{d.pickup} {d.drop}</Typography>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.900' }}>{v.shipments}</Typography> <Typography variant="caption" sx={{ color: 'grey.500' }}>AI {d.suggestedRider} · SLA {d.sla}</Typography>
<Typography variant="caption" color="text.secondary">{v.onTime}% on-time</Typography> </Box>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<MainCard title="Priority Deliveries" noPadding>
<Stack divider={<Divider />}>
{priority.map((d) => (
<Stack key={d.id} direction="row" justifyContent="space-between" alignItems="center" sx={{ px: 2, py: 1.25 }}>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main' }}>{d.id}</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{d.destination}</Typography>
</Box>
<Box component="span" sx={{ px: 0.75, py: 0.25, borderRadius: 1, bgcolor: '#FEEAE9', color: '#A82216', fontSize: 10, fontWeight: 700, textTransform: 'uppercase', flexShrink: 0 }}>{d.priority}</Box>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<MainCard title="Delayed Shipments" noPadding>
<Stack divider={<Divider />}>
{delayed.map((d) => (
<Stack key={d.id} direction="row" justifyContent="space-between" alignItems="center" sx={{ px: 2, py: 1.25 }}>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main' }}>{d.id}</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{d.rider}</Typography>
</Box>
<Stack direction="row" spacing={0.4} alignItems="center" sx={{ color: 'error.main', flexShrink: 0 }}>
<ScheduleOutlinedIcon sx={{ fontSize: 14 }} />
<Typography variant="caption" sx={{ fontWeight: 700 }}>+{d.delayMin}m</Typography>
</Stack>
</Stack>
))}
{delayed.length === 0 && <Typography variant="caption" color="text.secondary" sx={{ p: 2, textAlign: 'center' }}>No delays.</Typography>}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} md={6} lg={3}>
<MainCard title="AI Recommendations" noPadding>
<Stack divider={<Divider />}>
{recs.map((r) => (
<Stack key={r.id} direction="row" spacing={1} alignItems="flex-start" sx={{ px: 2, py: 1.25 }}>
<AutoAwesomeOutlinedIcon sx={{ fontSize: 15, color: '#EA580C', mt: 0.2, flexShrink: 0 }} />
<Box sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.800', display: 'block', lineHeight: 1.3 }}>{r.title}</Typography>
<Typography variant="caption" sx={{ color: 'primary.main', fontWeight: 600 }}>{r.action}</Typography>
</Box> </Box>
</Stack> </Stack>
</Box> ))}
))} </Stack>
</Stack> </MainCard>
</MainCard> </Grid>
</Grid> </Grid>
</Box>
<Grid item xs={12} lg={8}> {/* Third row — events / fleet / route efficiency */}
<MainCard title="Recent Shipments" noPadding> <Box sx={{ mt: 3.5 }}>
<Table> <SectionLabel>Operational Intelligence</SectionLabel>
<TableHead> <Grid container spacing={2.5}>
<TableRow> <Grid item xs={12} lg={4}>
<TableCell>Order ID</TableCell> <MainCard title="Recent Operational Events" noPadding>
<TableCell>Customer</TableCell> <Stack divider={<Divider />}>
<TableCell>Vertical</TableCell> {executionFeed.map((e, i) => (
<TableCell>Route</TableCell> <Stack key={`${e.id}-${i}`} direction="row" spacing={1.25} alignItems="flex-start" sx={{ px: 2, py: 1.25 }}>
<TableCell>Status</TableCell> <Box sx={{ mt: 0.6, width: 8, height: 8, borderRadius: '50%', bgcolor: e.stage === 'Exception' ? 'error.main' : e.stage === 'Delivered' ? 'success.main' : 'info.main', flexShrink: 0 }} />
<TableCell align="right">Amount</TableCell> <Box sx={{ minWidth: 0, flexGrow: 1 }}>
</TableRow> <Stack direction="row" justifyContent="space-between" spacing={1}>
</TableHead> <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>{e.stage}</Typography>
<TableBody> <Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>{e.time}</Typography>
{orders.slice(0, 6).map((o) => { </Stack>
const v = verticalOf(o.tenant); <Typography variant="caption" color="text.secondary" sx={{ display: 'block' }} noWrap>{e.id} · {e.loc}</Typography>
return ( </Box>
<TableRow key={o.id} hover> </Stack>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{o.id}</TableCell> ))}
<TableCell>{o.customer}</TableCell> </Stack>
<TableCell> </MainCard>
<Chip size="small" label={v} sx={{ bgcolor: hexA(VERTICAL_COLOR[v], 0.12), color: VERTICAL_COLOR[v], fontWeight: 600 }} /> </Grid>
</TableCell>
<TableCell> <Grid item xs={12} md={6} lg={4}>
<Typography variant="caption" color="text.secondary">{o.pickup} {o.drop}</Typography> <MainCard title="Fleet Status Overview">
</TableCell> <Grid container spacing={1.5} sx={{ mb: 1.5 }}>
<TableCell><StatusChip status={o.status} /></TableCell> {[['On trip', fleetSummary.onTrip, '#1D4ED8'], ['Charging', fleetSummary.charging, '#00A2AE'], ['Idle', fleetSummary.idle, '#8C8C8C'], ['Maintenance', fleetSummary.maintenance, '#F04134']].map(([l, v, c]) => (
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell> <Grid item xs={6} key={l}>
</TableRow> <Box sx={{ border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 1.25 }}>
); <Typography variant="h5" sx={{ fontWeight: 800, color: c }}>{v}</Typography>
})} <Typography variant="caption" color="text.secondary">{l}</Typography>
</TableBody> </Box>
</Table> </Grid>
</MainCard> ))}
</Grid>
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.5 }}>
<Typography variant="caption" color="text.secondary">EV fleet share</Typography>
<Typography variant="caption" sx={{ fontWeight: 700 }}>{fleetSummary.evShare}%</Typography>
</Stack>
<LinearProgress variant="determinate" value={fleetSummary.evShare} color="success" sx={{ height: 7, borderRadius: 4 }} />
</MainCard>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<MainCard title="Route Efficiency" noPadding>
<Stack divider={<Divider />}>
{lanePerformance.slice(0, 5).map((l) => (
<Box key={l.lane} sx={{ px: 2, py: 1.1 }}>
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.4 }}>
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.800' }} noWrap>{l.lane}</Typography>
<Typography variant="caption" color="text.secondary">{l.onTime}% · {inr(l.costPer)}</Typography>
</Stack>
<LinearProgress variant="determinate" value={l.onTime} color={l.onTime >= 98 ? 'success' : l.onTime >= 96 ? 'warning' : 'error'} sx={{ height: 5, borderRadius: 3 }} />
</Box>
))}
</Stack>
</MainCard>
</Grid>
</Grid> </Grid>
</Grid> </Box>
{/* End-to-end flow + volume trend */}
<Box sx={{ mt: 3.5 }}>
<SectionLabel>Network Flow & Volume</SectionLabel>
<Grid container spacing={2.5}>
<Grid item xs={12} lg={5}>
<ProcessTracker />
</Grid>
<Grid item xs={12} lg={7}>
<MainCard
title="Order Volume Trends"
action={<Stack direction="row" spacing={2}><Legend color="#C01227" label="Orders" /><Legend color="#00A854" label="Delivered" /></Stack>}
sx={{ height: '100%' }}
>
<Box sx={{ py: 1 }}>
<AreaChart
height={320}
labels={ordersTrend.map((d) => d.m)}
series={[
{ name: 'Orders', color: '#C01227', data: ordersTrend.map((d) => d.orders) },
{ name: 'Delivered', color: '#00A854', data: ordersTrend.map((d) => d.delivered) }
]}
/>
</Box>
</MainCard>
</Grid>
</Grid>
</Box>
{/* AI impact */}
<Box sx={{ mt: 3.5 }}>
<AiImpactSummary />
</Box>
<Toast {...toast} /> <Toast {...toast} />
</> </>
); );
} }
function AiStat({ icon: Icon, color, value, label }) {
return (
<Stack direction="row" spacing={1.25} alignItems="center">
<Avatar variant="rounded" sx={{ bgcolor: hexA(color, 0.12), color, width: 38, height: 38 }}><Icon fontSize="small" /></Avatar>
<Box>
<Typography variant="h4" sx={{ fontWeight: 700 }}>{value}</Typography>
<Typography variant="caption" color="text.secondary">{label}</Typography>
</Box>
</Stack>
);
}
function Legend({ color, label }) { function Legend({ color, label }) {
return ( return (
<Stack direction="row" spacing={0.75} alignItems="center"> <Stack direction="row" spacing={0.75} alignItems="center">
@@ -228,8 +240,3 @@ function Legend({ color, label }) {
</Stack> </Stack>
); );
} }
const hexA = (hex, a) => {
const n = parseInt(hex.replace('#', ''), 16);
return `rgba(${n >> 16}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
};

View File

@@ -22,8 +22,11 @@ import StatCard from '@/components/StatCard';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import UserAvatar from '@/components/UserAvatar'; import UserAvatar from '@/components/UserAvatar';
import TabLabelCount from '@/components/TabLabelCount'; import TabLabelCount from '@/components/TabLabelCount';
import { deliveries, locations, tenantsList, riders } from '@/data/mock'; import { deliveries, tenantsList, riders } from '@/data/mock';
import { inr } from '@/utils/format'; import { inr } from '@/utils/format';
import DateRangeFilter from '@/components/DateRangeFilter';
import FilterSummary from '@/components/FilterSummary';
import { useFilters, inRange } from '@/store/Filters';
const TABS = [ const TABS = [
{ key: 'assigned', label: 'Assigned' }, { key: 'assigned', label: 'Assigned' },
@@ -140,12 +143,11 @@ function DeliveryRow({ row, index }) {
} }
export default function Deliveries() { export default function Deliveries() {
const { location, range } = useFilters(); // global location + date range
const [tab, setTab] = useState(0); const [tab, setTab] = useState(0);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [tenant, setTenant] = useState('all'); const [tenant, setTenant] = useState('all');
const [location, setLocation] = useState('all');
const [rider, setRider] = useState('all'); const [rider, setRider] = useState('all');
const [headerLocation, setHeaderLocation] = useState('all');
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5); const [rpp, setRpp] = useState(5);
@@ -170,13 +172,14 @@ export default function Deliveries() {
const matchTab = d.status === tabKey; const matchTab = d.status === tabKey;
const matchTenant = tenant === 'all' || d.tenant === tenant; const matchTenant = tenant === 'all' || d.tenant === tenant;
const matchLocation = location === 'all' || d.location === location; const matchLocation = location === 'all' || d.location === location;
const matchDate = d.date ? inRange(d.date, range) : true;
const matchRider = rider === 'all' || d.rider === rider; const matchRider = rider === 'all' || d.rider === rider;
const matchSearch = const matchSearch =
!search || !search ||
[d.id, d.tenant, d.pickup, d.drop, d.rider, d.location].join(' ').toLowerCase().includes(search.toLowerCase()); [d.id, d.tenant, d.pickup, d.drop, d.rider, d.location].join(' ').toLowerCase().includes(search.toLowerCase());
return matchTab && matchTenant && matchLocation && matchRider && matchSearch; return matchTab && matchTenant && matchLocation && matchDate && matchRider && matchSearch;
}), }),
[tabKey, tenant, location, rider, search] [tabKey, tenant, location, range, rider, search]
); );
const paged = filtered.slice(page * rpp, page * rpp + rpp); const paged = filtered.slice(page * rpp, page * rpp + rpp);
@@ -186,15 +189,7 @@ export default function Deliveries() {
<PageHeader <PageHeader
title="Deliveries" title="Deliveries"
breadcrumbs={[{ label: 'Deliveries' }]} breadcrumbs={[{ label: 'Deliveries' }]}
action={ action={<DateRangeFilter />}
<TextField
select size="small" value={headerLocation} onChange={(e) => setHeaderLocation(e.target.value)}
sx={{ minWidth: 180 }} label="Location"
>
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/> />
<Grid container spacing={2.5} sx={{ mb: 1 }}> <Grid container spacing={2.5} sx={{ mb: 1 }}>
@@ -206,17 +201,10 @@ export default function Deliveries() {
<Card sx={{ mt: 1.5 }}> <Card sx={{ mt: 1.5 }}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }} flexWrap="wrap" useFlexGap> <Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }} flexWrap="wrap" useFlexGap>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<TextField select size="small" value={tenant} onChange={(e) => { setTenant(e.target.value); setPage(0); }} sx={{ minWidth: 160 }} label="Tenant"> <TextField select size="small" value={tenant} onChange={(e) => { setTenant(e.target.value); setPage(0); }} sx={{ minWidth: 160 }} label="Tenant">
<MenuItem value="all">All Tenants</MenuItem> <MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)} {tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField> </TextField>
<TextField select size="small" value={location} onChange={(e) => { setLocation(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<TextField select size="small" value={rider} onChange={(e) => { setRider(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Rider"> <TextField select size="small" value={rider} onChange={(e) => { setRider(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Rider">
<MenuItem value="all">All Riders</MenuItem> <MenuItem value="all">All Riders</MenuItem>
{riders.map((r) => <MenuItem key={r.id} value={r.name}>{r.name}</MenuItem>)} {riders.map((r) => <MenuItem key={r.id} value={r.name}>{r.name}</MenuItem>)}
@@ -229,6 +217,10 @@ export default function Deliveries() {
/> />
</Stack> </Stack>
<Box sx={{ px: 2, pb: 1.5 }}>
<FilterSummary count={filtered.length} />
</Box>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto"> <Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
{TABS.map((t, i) => ( {TABS.map((t, i) => (

View File

@@ -1,206 +1,259 @@
import { useState } from 'react'; import { useState } from 'react';
import { import {
Grid, Box, Stack, Typography, Card, Tabs, Tab, TextField, MenuItem, Switch, Button, Grid,
Tabs, Snackbar, Alert
Tab,
Box,
Stack,
TextField,
MenuItem,
Switch,
FormControlLabel,
Button,
Typography,
Divider,
Snackbar,
Alert
} from '@mui/material'; } from '@mui/material';
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
import RestartAltOutlinedIcon from '@mui/icons-material/RestartAltOutlined';
import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined'; import TuneOutlinedIcon from '@mui/icons-material/TuneOutlined';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import ApiOutlinedIcon from '@mui/icons-material/ApiOutlined';
import GppGoodOutlinedIcon from '@mui/icons-material/GppGoodOutlined';
// section / row icons
import BusinessOutlinedIcon from '@mui/icons-material/BusinessOutlined';
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import CampaignOutlinedIcon from '@mui/icons-material/CampaignOutlined';
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined';
import VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined';
import DevicesOutlinedIcon from '@mui/icons-material/DevicesOutlined';
import GroupOutlinedIcon from '@mui/icons-material/GroupOutlined';
import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined';
import WhatsAppIcon from '@mui/icons-material/WhatsApp';
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
import WebhookIcon from '@mui/icons-material/Webhook';
import HistoryToggleOffOutlinedIcon from '@mui/icons-material/HistoryToggleOffOutlined';
import PolicyOutlinedIcon from '@mui/icons-material/PolicyOutlined';
import CloudDownloadOutlinedIcon from '@mui/icons-material/CloudDownloadOutlined';
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
import PageHeader from '@/components/PageHeader'; import StatusChip from '@/components/StatusChip';
import MainCard from '@/components/MainCard';
const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)']; const RETENTION = ['90 days', '180 days', '1 year', '3 years', 'Indefinite'];
const LANGUAGES = ['English', 'हिन्दी (Hindi)', 'العربية (Arabic)'];
function TabPanel({ value, index, children }) { const TABS = [
if (value !== index) return null; { key: 'general', label: 'General', icon: TuneOutlinedIcon },
return <Box sx={{ pt: 1 }}>{children}</Box>; { key: 'operations', label: 'Operations', icon: AltRouteOutlinedIcon },
} { key: 'security', label: 'Security', icon: LockOutlinedIcon },
{ key: 'integrations', label: 'Integrations', icon: ApiOutlinedIcon },
{ key: 'compliance', label: 'Compliance', icon: GppGoodOutlinedIcon }
];
export default function Settings() { const INITIAL = {
const [tab, setTab] = useState(0); general: { orgName: 'Doormile Logistics Pvt. Ltd.', supportEmail: 'support@doormile.in', contact: '+91 98450 11223' },
const [toast, setToast] = useState(false); notif: { customer: true, rider: true, delay: true, admin: true },
security: { twoFactor: true, sessionTimeout: true }
// General };
const [general, setGeneral] = useState({
orgName: 'Doormile Logistics Pvt. Ltd.',
supportEmail: 'support@doormile.in',
contact: '+91 98450 11223',
timezone: TIMEZONES[0],
language: LANGUAGES[0]
});
// Notifications
const [notify, setNotify] = useState({
newOrders: true,
riderStatus: true,
invoicePaid: true,
weeklyDigest: false,
emailAlerts: true,
smsAlerts: false
});
// Security
const [security, setSecurity] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
twoFactor: false
});
const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value }));
const setN = (k) => (e) => setNotify((p) => ({ ...p, [k]: e.target.checked }));
const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.value ?? e.target.checked }));
const save = () => setToast(true);
// ---- building blocks ----
function SectionCard({ title, description, icon: Icon, action, children, sx }) {
return ( return (
<> <Card variant="outlined" sx={{ borderColor: 'grey.200', boxShadow: '0 1px 2px rgba(16,24,40,0.04)', height: '100%', ...sx }}>
<PageHeader <Box sx={{ p: 2.5 }}>
title="Settings" <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1.5} sx={{ mb: children ? 2.25 : 0 }}>
breadcrumbs={[{ label: 'Settings' }]} <Stack direction="row" spacing={1.5} alignItems="center" sx={{ minWidth: 0 }}>
action={ {Icon && (
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}> <Box sx={{ width: 38, height: 38, borderRadius: 1.5, bgcolor: 'grey.100', color: 'grey.700', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
Save Changes <Icon sx={{ fontSize: 20 }} />
</Button> </Box>
} )}
/> <Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.25 }}>{title}</Typography>
<Grid container spacing={2.5}> {description && <Typography variant="caption" color="text.secondary">{description}</Typography>}
<Grid item xs={12} md={3}> </Box>
<MainCard noPadding> </Stack>
<Tabs {action && <Box sx={{ flexShrink: 0 }}>{action}</Box>}
orientation="vertical" </Stack>
value={tab} {children}
onChange={(_, v) => setTab(v)} </Box>
sx={{ </Card>
'& .MuiTab-root': { alignItems: 'flex-start', textTransform: 'none', minHeight: 52, fontWeight: 600 } );
}} }
>
<Tab icon={<TuneOutlinedIcon fontSize="small" />} iconPosition="start" label="General" /> function ToggleRow({ icon: Icon, title, desc, checked, onChange, divider }) {
<Tab icon={<NotificationsNoneIcon fontSize="small" />} iconPosition="start" label="Notifications" /> return (
<Tab icon={<LockOutlinedIcon fontSize="small" />} iconPosition="start" label="Security" /> <Stack direction="row" alignItems="center" justifyContent="space-between" spacing={2} sx={{ py: 1.75, borderTop: divider ? '1px solid' : 'none', borderColor: 'grey.100' }}>
</Tabs> <Stack direction="row" spacing={1.5} alignItems="center" sx={{ minWidth: 0 }}>
</MainCard> {Icon && (
</Grid> <Box sx={{ width: 36, height: 36, borderRadius: 1.5, bgcolor: 'grey.100', color: 'grey.700', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Icon sx={{ fontSize: 19 }} />
<Grid item xs={12} md={9}> </Box>
{/* General */} )}
<TabPanel value={tab} index={0}> <Box sx={{ minWidth: 0 }}>
<MainCard title="Organisation"> <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{title}</Typography>
<Grid container spacing={3}> <Typography variant="caption" color="text.secondary">{desc}</Typography>
<Grid item xs={12} sm={6}> </Box>
<TextField fullWidth label="Organisation Name" value={general.orgName} onChange={setG('orgName')} /> </Stack>
</Grid> <Switch checked={checked} onChange={onChange} />
<Grid item xs={12} sm={6}> </Stack>
<TextField fullWidth label="Support Email" value={general.supportEmail} onChange={setG('supportEmail')} /> );
</Grid> }
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Contact Number" value={general.contact} onChange={setG('contact')} /> const INTEGRATIONS = [
</Grid> { key: 'emailjs', name: 'EmailJS', desc: 'Transactional email delivery', icon: EmailOutlinedIcon, status: 'connected', sync: '2 min ago' },
<Grid item xs={12} sm={6}> { key: 'whatsapp', name: 'WhatsApp', desc: 'Customer tracking updates', icon: WhatsAppIcon, status: 'connected', sync: '5 min ago' },
<TextField select fullWidth label="Timezone" value={general.timezone} onChange={setG('timezone')}> { key: 'gmaps', name: 'Google Maps', desc: 'Geocoding & route tiles', icon: MapOutlinedIcon, status: 'connected', sync: '1 min ago' },
{TIMEZONES.map((t) => ( { key: 'razorpay', name: 'Razorpay', desc: 'Payments & COD settlement', icon: PaymentsOutlinedIcon, status: 'degraded', sync: '38 min ago' },
<MenuItem key={t} value={t}>{t}</MenuItem> { key: 'webhooks', name: 'Webhooks', desc: 'Event push to partner systems', icon: WebhookIcon, status: 'pending', sync: 'Never' }
))} ];
</TextField>
</Grid> export default function Settings() {
<Grid item xs={12} sm={6}> const [tab, setTab] = useState('general');
<TextField select fullWidth label="Language" value={general.language} onChange={setG('language')}> const [toast, setToast] = useState('');
{LANGUAGES.map((l) => ( const [general, setGeneral] = useState(INITIAL.general);
<MenuItem key={l} value={l}>{l}</MenuItem> const [notif, setNotif] = useState(INITIAL.notif);
))} const [security, setSecurity] = useState(INITIAL.security);
</TextField>
</Grid> const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value }));
</Grid> const setN = (k) => (e) => setNotif((p) => ({ ...p, [k]: e.target.checked }));
</MainCard> const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.checked }));
</TabPanel>
const save = () => setToast('Settings saved successfully.');
{/* Notifications */} const reset = () => {
<TabPanel value={tab} index={1}> setGeneral(INITIAL.general); setNotif(INITIAL.notif); setSecurity(INITIAL.security);
<MainCard title="Notification Preferences"> setToast('Changes reset to last saved state.');
<Stack divider={<Divider flexItem />} spacing={0}> };
{[
{ k: 'newOrders', t: 'New orders', d: 'Notify when a new order is placed' }, return (
{ k: 'riderStatus', t: 'Rider status', d: 'When a rider goes online or offline' }, <Box>
{ k: 'invoicePaid', t: 'Invoice paid', d: 'When a client settles an invoice' }, {/* header */}
{ k: 'weeklyDigest', t: 'Weekly digest', d: 'A summary of operations every Monday' } <Stack direction={{ xs: 'column', md: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', md: 'center' }} spacing={2} sx={{ mb: 2.5 }}>
].map((row) => ( <Box>
<Stack key={row.k} direction="row" alignItems="center" justifyContent="space-between" sx={{ py: 1.5 }}> <Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.1 }}>Settings</Typography>
<Box> <Typography variant="body2" color="text.secondary" sx={{ mt: 0.5, maxWidth: 560 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{row.t}</Typography> Manage organization preferences, notifications, integrations, and security settings.
<Typography variant="caption" color="text.secondary">{row.d}</Typography> </Typography>
</Box> </Box>
<Switch checked={notify[row.k]} onChange={setN(row.k)} /> <Stack direction="row" spacing={1.5} sx={{ flexShrink: 0 }}>
</Stack> <Button variant="outlined" color="inherit" startIcon={<RestartAltOutlinedIcon />} onClick={reset} sx={{ borderColor: 'grey.300', color: 'grey.700' }}>Reset Changes</Button>
))} <Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>Save Changes</Button>
</Stack> </Stack>
<Divider sx={{ my: 2 }} /> </Stack>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>Channels</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}> {/* sticky tabs */}
<FormControlLabel control={<Switch checked={notify.emailAlerts} onChange={setN('emailAlerts')} />} label="Email alerts" /> <Box sx={{ position: 'sticky', top: 64, zIndex: 5, bgcolor: 'background.default', borderBottom: '1px solid', borderColor: 'grey.200', mb: 3 }}>
<FormControlLabel control={<Switch checked={notify.smsAlerts} onChange={setN('smsAlerts')} />} label="SMS alerts" /> <Tabs value={tab} onChange={(_, v) => setTab(v)} variant="scrollable" scrollButtons="auto" sx={{ minHeight: 48, '& .MuiTab-root': { minHeight: 48, textTransform: 'none', fontWeight: 600, fontSize: '0.9rem' } }}>
</Stack> {TABS.map((t) => (
</MainCard> <Tab key={t.key} value={t.key} icon={<t.icon sx={{ fontSize: 18 }} />} iconPosition="start" label={t.label} />
</TabPanel> ))}
</Tabs>
{/* Security */} </Box>
<TabPanel value={tab} index={2}>
<Stack spacing={2.5}> {/* ===================== GENERAL ===================== */}
<MainCard title="Change Password"> {tab === 'general' && (
<Grid container spacing={3}> <Box sx={{ maxWidth: 1080 }}>
<Grid item xs={12} sm={6}> <SectionCard title="Organization Profile" description="Your company's core identity and contact details" icon={BusinessOutlinedIcon}>
<TextField fullWidth type="password" label="Current Password" value={security.currentPassword} onChange={setS('currentPassword')} /> <Grid container spacing={2.5}>
</Grid> <Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Organization Name" value={general.orgName} onChange={setG('orgName')} /></Grid>
<Grid item xs={12} /> <Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Support Email" value={general.supportEmail} onChange={setG('supportEmail')} /></Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Contact Number" value={general.contact} onChange={setG('contact')} /></Grid>
<TextField fullWidth type="password" label="New Password" value={security.newPassword} onChange={setS('newPassword')} /> </Grid>
</Grid> </SectionCard>
<Grid item xs={12} sm={6}> </Box>
<TextField fullWidth type="password" label="Confirm New Password" value={security.confirmPassword} onChange={setS('confirmPassword')} /> )}
</Grid>
</Grid> {/* ===================== OPERATIONS ===================== */}
</MainCard> {tab === 'operations' && (
<MainCard title="Two-Factor Authentication"> <Grid container spacing={2.5} sx={{ maxWidth: 1080 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between"> {/* Notification Controls */}
<Box> <Grid item xs={12} md={6}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>Authenticator app</Typography> <SectionCard title="Notification Controls" description="Who gets alerted across the operation" icon={NotificationsNoneIcon}>
<Typography variant="caption" color="text.secondary"> <Box>
Require a one-time code at sign-in for extra security. <ToggleRow icon={SupportAgentOutlinedIcon} title="Customer Notifications" desc="Order, tracking link & delivery updates to customers" checked={notif.customer} onChange={setN('customer')} />
</Typography> <ToggleRow icon={TwoWheelerOutlinedIcon} title="Rider Notifications" desc="Assignment, pickup & route changes to riders" checked={notif.rider} onChange={setN('rider')} divider />
</Box> <ToggleRow icon={WarningAmberOutlinedIcon} title="Delay Alerts" desc="Proactive alerts when a shipment trends late" checked={notif.delay} onChange={setN('delay')} divider />
<Switch checked={security.twoFactor} onChange={(e) => setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} /> <ToggleRow icon={CampaignOutlinedIcon} title="Admin Escalations" desc="Critical exceptions routed to ops managers" checked={notif.admin} onChange={setN('admin')} divider />
</Stack> </Box>
</MainCard> </SectionCard>
</Stack> </Grid>
</TabPanel> </Grid>
</Grid> )}
</Grid>
{/* ===================== SECURITY ===================== */}
<Snackbar {tab === 'security' && (
open={toast} <Grid container spacing={2.5} sx={{ maxWidth: 1080 }}>
autoHideDuration={2500} <Grid item xs={12} md={6}>
onClose={() => setToast(false)} <SectionCard title="Two-Factor Authentication" description="Require a one-time code at sign-in" icon={ShieldOutlinedIcon} action={<Switch checked={security.twoFactor} onChange={setS('twoFactor')} />}>
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} <Typography variant="body2" color="text.secondary">
> {security.twoFactor ? 'Enabled for all admin accounts via authenticator app.' : 'Currently disabled — enable to add a second verification step.'}
<Alert severity="success" variant="filled" onClose={() => setToast(false)} sx={{ width: '100%' }}> </Typography>
Settings saved successfully. </SectionCard>
</Alert> </Grid>
</Snackbar> <Grid item xs={12} md={6}>
</> <SectionCard title="Password Policy" description="Strength and rotation rules" icon={VpnKeyOutlinedIcon} action={<Button size="small" variant="outlined" color="inherit" sx={{ borderColor: 'grey.300', color: 'grey.700' }}>Configure</Button>}>
<Typography variant="body2" color="text.secondary">Minimum 12 characters · mixed case · expires every 90 days.</Typography>
</SectionCard>
</Grid>
<Grid item xs={12} md={6}>
<SectionCard title="Session Management" description="Active sessions and auto-logout" icon={DevicesOutlinedIcon} action={<Button size="small" variant="outlined" color="inherit" sx={{ borderColor: 'grey.300', color: 'grey.700' }}>View Sessions</Button>}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Typography variant="body2" color="text.secondary">Auto sign-out after 30 min idle</Typography>
<Switch checked={security.sessionTimeout} onChange={setS('sessionTimeout')} />
</Stack>
</SectionCard>
</Grid>
<Grid item xs={12} md={6}>
<SectionCard title="Role Permissions" description="Access control across the org" icon={GroupOutlinedIcon} action={<Button size="small" variant="outlined" color="inherit" sx={{ borderColor: 'grey.300', color: 'grey.700' }}>Manage Roles</Button>}>
<Typography variant="body2" color="text.secondary">4 roles · 12 members · 3 pending invites.</Typography>
</SectionCard>
</Grid>
</Grid>
)}
{/* ===================== INTEGRATIONS ===================== */}
{tab === 'integrations' && (
<Grid container spacing={2.5}>
{INTEGRATIONS.map((it) => (
<Grid item xs={12} sm={6} lg={4} key={it.key}>
<SectionCard title={it.name} description={it.desc} icon={it.icon} action={<StatusChip status={it.status} />}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ pt: 0.5 }}>
<Box>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>Last sync</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{it.sync}</Typography>
</Box>
<Button size="small" variant="outlined" color="inherit" sx={{ borderColor: 'grey.300', color: 'grey.700' }}>Configure</Button>
</Stack>
</SectionCard>
</Grid>
))}
</Grid>
)}
{/* ===================== COMPLIANCE ===================== */}
{tab === 'compliance' && (
<Grid container spacing={2.5} sx={{ maxWidth: 1080 }}>
<Grid item xs={12} md={6}>
<SectionCard title="Data Retention" description="How long operational records are kept" icon={HistoryToggleOffOutlinedIcon}>
<TextField select fullWidth size="small" defaultValue={RETENTION[2]} label="Retention period">
{RETENTION.map((r) => <MenuItem key={r} value={r}>{r}</MenuItem>)}
</TextField>
</SectionCard>
</Grid>
<Grid item xs={12} md={6}>
<SectionCard title="Audit Logs" description="Immutable record of every admin action" icon={ReceiptLongOutlinedIcon} action={<Button size="small" variant="outlined" color="inherit" sx={{ borderColor: 'grey.300', color: 'grey.700' }}>View Logs</Button>}>
<Typography variant="body2" color="text.secondary">1,284 events recorded in the last 30 days.</Typography>
</SectionCard>
</Grid>
<Grid item xs={12} md={6}>
<SectionCard title="Privacy Controls" description="Consent, masking and PII handling" icon={PolicyOutlinedIcon} action={<Button size="small" variant="outlined" color="inherit" sx={{ borderColor: 'grey.300', color: 'grey.700' }}>Manage</Button>}>
<Typography variant="body2" color="text.secondary">Customer PII masked in exports · GDPR & DPDP aligned.</Typography>
</SectionCard>
</Grid>
<Grid item xs={12} md={6}>
<SectionCard title="Export Data" description="Download a full copy of your workspace data" icon={CloudDownloadOutlinedIcon} action={<Button size="small" variant="contained" disableElevation startIcon={<CloudDownloadOutlinedIcon sx={{ fontSize: 16 }} />}>Export</Button>}>
<Typography variant="body2" color="text.secondary">Orders, riders, invoices and logs as CSV / JSON.</Typography>
</SectionCard>
</Grid>
</Grid>
)}
<Snackbar open={Boolean(toast)} autoHideDuration={2500} onClose={() => setToast('')} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}>
<Alert severity="success" variant="filled" onClose={() => setToast('')} sx={{ width: '100%' }}>{toast}</Alert>
</Snackbar>
</Box>
); );
} }

View File

@@ -0,0 +1,17 @@
import TabbedWorkspace from '@/components/TabbedWorkspace';
import Customers from '@/pages/customers/Customers';
import Tenants from '@/pages/tenants/Tenants';
// ==============================|| CUSTOMERS — directory workspace ||============================== //
// Merges the consumer Customers directory and the Business Clients (tenants) directory.
export default function CustomersHub() {
return (
<TabbedWorkspace
tabs={[
{ key: 'customers', label: 'Customers', element: <Customers /> },
{ key: 'clients', label: 'Business Clients', element: <Tenants /> }
]}
/>
);
}

View File

@@ -0,0 +1,67 @@
import { Card, Table, TableBody, TableCell, TableHead, TableRow, Typography, Box } from '@mui/material';
import TabbedWorkspace from '@/components/TabbedWorkspace';
import PageHeader from '@/components/PageHeader';
import StatusChip from '@/components/StatusChip';
import Invoices from '@/pages/invoice/Invoices';
import Pricing from '@/pages/Pricing';
import { invoices } from '@/data/mock';
import { inr } from '@/utils/format';
// ==============================|| FINANCE — billing workspace ||============================== //
// Merges Invoices, Pricing and a Payments ledger (settled invoices) into one screen.
const METHODS = ['NEFT', 'UPI', 'RTGS', 'Corporate Card', 'Cheque'];
function Payments() {
const paid = invoices.filter((i) => i.status === 'paid');
return (
<>
<PageHeader title="Payments" breadcrumbs={[{ label: 'Finance' }, { label: 'Payments' }]} />
<Card>
<Table size="small" sx={{ '& .MuiTableRow-root:hover': { backgroundColor: 'grey.50' } }}>
<TableHead>
<TableRow>
<TableCell>Invoice</TableCell>
<TableCell>Client</TableCell>
<TableCell>Period</TableCell>
<TableCell>Method</TableCell>
<TableCell>Received</TableCell>
<TableCell align="right">Amount</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paid.map((p, i) => (
<TableRow key={p.id} hover>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{p.invoiceId}</TableCell>
<TableCell>{p.client}</TableCell>
<TableCell><Typography variant="caption" color="text.secondary">{p.period}</Typography></TableCell>
<TableCell><Typography variant="caption" color="text.secondary">{METHODS[i % METHODS.length]}</Typography></TableCell>
<TableCell><Typography variant="caption" color="text.secondary">{p.dueDate}</Typography></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(p.amount)}</TableCell>
<TableCell><StatusChip status="paid" /></TableCell>
</TableRow>
))}
{paid.length === 0 && (
<TableRow><TableCell colSpan={7} sx={{ textAlign: 'center', py: 4, color: 'text.secondary' }}>No payments recorded.</TableCell></TableRow>
)}
</TableBody>
</Table>
</Card>
<Box sx={{ height: 8 }} />
</>
);
}
export default function FinanceHub() {
return (
<TabbedWorkspace
tabs={[
{ key: 'invoices', label: 'Invoices', element: <Invoices /> },
{ key: 'pricing', label: 'Pricing', element: <Pricing /> },
{ key: 'payments', label: 'Payments', element: <Payments /> }
]}
/>
);
}

View File

@@ -17,6 +17,7 @@ import MainCard from '@/components/MainCard';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import UserAvatar from '@/components/UserAvatar'; import UserAvatar from '@/components/UserAvatar';
import { customers, locations } from '@/data/mock'; import { customers, locations } from '@/data/mock';
import { useFilters } from '@/store/Filters';
const EMPTY_FORM = { const EMPTY_FORM = {
name: '', phone: '', address: '', location: '', city: '', state: '', name: '', phone: '', address: '', location: '', city: '', state: '',
@@ -26,7 +27,7 @@ const EMPTY_FORM = {
export default function Customers() { export default function Customers() {
const navigate = useNavigate(); const navigate = useNavigate();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [location, setLocation] = useState('all'); const { location } = useFilters(); // global location — single source of truth
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5); const [rpp, setRpp] = useState(5);
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
@@ -74,13 +75,6 @@ export default function Customers() {
breadcrumbs={[{ label: 'Customers' }]} breadcrumbs={[{ label: 'Customers' }]}
action={ action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
<TextField
select size="small" value={location} onChange={(e) => { setLocation(e.target.value); setPage(0); }}
sx={{ minWidth: 160 }} label="Location"
>
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<TextField <TextField
size="small" placeholder="Search customers…" value={search} size="small" placeholder="Search customers…" value={search}
onChange={(e) => { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }} onChange={(e) => { setSearch(e.target.value); setPage(0); }} sx={{ minWidth: 220 }}

View File

@@ -0,0 +1,256 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, Stack, Box, Typography, Button, Divider, Chip, MenuItem, Select, IconButton, Tooltip
} from '@mui/material';
import BoltOutlinedIcon from '@mui/icons-material/BoltOutlined';
import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
import PersonAddAlt1OutlinedIcon from '@mui/icons-material/PersonAddAlt1Outlined';
import UndoOutlinedIcon from '@mui/icons-material/UndoOutlined';
import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';
import PageHeader from '@/components/PageHeader';
import StatusChip from '@/components/StatusChip';
import UserAvatar from '@/components/UserAvatar';
import Toast, { useToast } from '@/components/Toast';
import { dispatchQueue, orders, riders } from '@/data/mock';
import { inr } from '@/utils/format';
import { useOps } from '@/store/OpsStore';
const PRIORITY = { high: { fg: '#A82216', bg: '#FEEAE9' }, express: { fg: '#8A6500', bg: '#FFF7E0' }, standard: { fg: '#595959', bg: '#F0F0F0' } };
const confColor = (c) => (c >= 90 ? '#00773B' : c >= 80 ? '#8A6500' : '#595959');
const orderById = (id) => orders.find((o) => o.id === id);
const riderByName = (name) => riders.find((r) => r.name === name);
const availableRiders = riders.filter((r) => r.status !== 'offline');
function KpiCard({ label, value, subtitle, color }) {
return (
<Card
sx={{
height: '100%',
minHeight: 116,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 2.5,
py: 2,
border: '1px solid',
borderColor: 'grey.200',
boxShadow: '0 1px 3px rgba(16,24,40,0.06)',
transition: 'box-shadow .15s ease, transform .15s ease',
'&:hover': { boxShadow: '0 6px 20px rgba(16,24,40,0.10)', transform: 'translateY(-2px)' }
}}
>
<Typography sx={{ fontSize: '1rem', fontWeight: 600, color: 'grey.700', lineHeight: 1.2 }}>{label}</Typography>
<Typography sx={{ fontSize: { xs: '2.25rem', lg: '2.75rem' }, fontWeight: 800, lineHeight: 1.05, mt: 0.25, color: color || 'grey.900' }}>
{value}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5 }}>{subtitle}</Typography>
</Card>
);
}
export default function DispatchBoard() {
const navigate = useNavigate();
const [toast, showToast] = useToast();
const { assignments, assignOrder, unassignOrder, riderLoad, assignedCount } = useOps();
// per-card chosen rider (defaults to AI suggestion)
const [choice, setChoice] = useState({});
const queue = useMemo(() => dispatchQueue.filter((q) => !assignments[q.id]), [assignments]);
const assignedList = useMemo(() => Object.entries(assignments).map(([id, a]) => ({ id, ...a })).sort((x, y) => y.at - x.at), [assignments]);
const onlineCount = availableRiders.length;
const avgConfidence = queue.length ? Math.round(queue.reduce((s, q) => s + q.confidence, 0) / queue.length) : 0;
const doAssign = (q, riderName) => {
const rider = riderByName(riderName) || availableRiders[0];
if (!rider) return showToast('No available rider', 'warning');
assignOrder(q.id, rider);
showToast(`${q.id} assigned to ${rider.name}`);
};
const autoAssignAll = () => {
if (!queue.length) return showToast('Queue is already clear', 'info');
queue.forEach((q) => {
const rider = riderByName(q.suggestedRider) || availableRiders[0];
if (rider) assignOrder(q.id, rider);
});
showToast(`${queue.length} orders auto-assigned by MileTruth AI`);
};
return (
<>
<PageHeader
title="Dispatch Board"
breadcrumbs={[{ label: 'Operations' }, { label: 'Dispatch' }]}
action={
<Button variant="contained" startIcon={<AutoAwesomeOutlinedIcon />} onClick={autoAssignAll} disabled={!queue.length}>
Auto-assign all
</Button>
}
/>
{/* primary KPI block — large dashboard cards */}
<Grid container spacing={2.5} sx={{ mb: 3 }}>
<Grid item xs={6} lg={3}>
<KpiCard label="Awaiting Dispatch" value={queue.length} color={queue.length ? '#A82216' : '#00773B'} subtitle="Orders waiting for assignment" />
</Grid>
<Grid item xs={6} lg={3}>
<KpiCard label="Assigned (Session)" value={assignedCount} color="#00773B" subtitle="Assigned in current session" />
</Grid>
<Grid item xs={6} lg={3}>
<KpiCard label="Riders Available" value={onlineCount} subtitle="Ready for dispatch" />
</Grid>
<Grid item xs={6} lg={3}>
<KpiCard label="Avg AI Confidence" value={queue.length ? `${avgConfidence}%` : '—'} color="#00773B" subtitle="Assignment confidence score" />
</Grid>
</Grid>
<Grid container spacing={2.5}>
{/* LEFT · unassigned queue */}
<Grid item xs={12} lg={8}>
<Card sx={{ height: '100%' }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ p: 2, pb: 1 }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}>Awaiting Dispatch</Typography>
<Typography variant="caption" color="text.secondary">{queue.length} orders</Typography>
</Stack>
<Divider />
<Stack spacing={1.5} sx={{ p: 2 }}>
{queue.map((q) => {
const o = orderById(q.id) || {};
const pr = PRIORITY[q.priority] || PRIORITY.standard;
const selected = choice[q.id] ?? q.suggestedRider;
return (
<Box key={q.id} sx={{ p: 1.75, border: '1px solid', borderColor: 'grey.200', borderLeft: '4px solid', borderLeftColor: pr.fg, borderRadius: 2 }}>
<Stack direction={{ xs: 'column', md: 'row' }} justifyContent="space-between" spacing={1.5}>
{/* order facts */}
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main', cursor: 'pointer' }} onClick={() => navigate(`/orders/${q.id}`)}>{q.id}</Typography>
<Box component="span" sx={{ px: 0.75, py: 0.25, borderRadius: 1, bgcolor: pr.bg, color: pr.fg, fontSize: 10, fontWeight: 700, textTransform: 'uppercase' }}>{q.priority}</Box>
{o.tenant && <Typography variant="caption" color="text.secondary">{o.tenant}</Typography>}
</Stack>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ minWidth: 0 }}>
<Typography variant="caption" color="text.secondary" noWrap>{q.pickup}</Typography>
<ArrowRightAltRoundedIcon sx={{ fontSize: 16, color: 'grey.400' }} />
<Typography variant="caption" sx={{ fontWeight: 600 }} noWrap>{q.drop}</Typography>
</Stack>
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mt: 0.75 }}>
<Stack direction="row" spacing={0.5} alignItems="center">
<ScheduleOutlinedIcon sx={{ fontSize: 14, color: 'grey.500' }} />
<Typography variant="caption" color="text.secondary">SLA {q.sla} · ETA {q.etaMin}m</Typography>
</Stack>
{o.charges != null && <Typography variant="caption" sx={{ fontWeight: 600 }}>{inr(o.charges)}</Typography>}
{o.kms != null && <Typography variant="caption" color="text.secondary">{o.kms} km</Typography>}
</Stack>
{/* AI suggestion */}
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 0.75 }}>
<AutoAwesomeOutlinedIcon sx={{ fontSize: 14, color: '#EA580C' }} />
<Typography variant="caption" color="text.secondary">AI suggests</Typography>
<Typography variant="caption" sx={{ fontWeight: 700 }}>{q.suggestedRider}</Typography>
<Chip size="small" label={`${q.confidence}%`} sx={{ height: 18, bgcolor: 'grey.100', color: confColor(q.confidence), fontWeight: 700, '& .MuiChip-label': { px: 0.75, fontSize: 10 } }} />
</Stack>
</Box>
{/* assign controls */}
<Stack direction="row" spacing={1} alignItems="center" sx={{ flexShrink: 0 }}>
<Select
size="small"
value={selected}
onChange={(e) => setChoice((c) => ({ ...c, [q.id]: e.target.value }))}
sx={{ minWidth: 150, '& .MuiSelect-select': { py: 0.75, fontSize: '0.8125rem' } }}
>
{availableRiders.map((r) => (
<MenuItem key={r.id} value={r.name}>
{r.name} {riderLoad(r.id) > 0 ? `· ${riderLoad(r.id)} load` : ''}
</MenuItem>
))}
</Select>
<Button size="small" variant="contained" startIcon={<PersonAddAlt1OutlinedIcon />} onClick={() => doAssign(q, selected)}>
Assign
</Button>
</Stack>
</Stack>
</Box>
);
})}
{queue.length === 0 && (
<Stack alignItems="center" spacing={1} sx={{ py: 6, color: 'text.secondary' }}>
<CheckCircleRoundedIcon sx={{ fontSize: 40, color: 'success.main' }} />
<Typography variant="subtitle2">Queue clear every order is dispatched.</Typography>
</Stack>
)}
</Stack>
</Card>
</Grid>
{/* RIGHT · riders + assigned-this-session */}
<Grid item xs={12} lg={4}>
<Stack spacing={2.5}>
<Card>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ p: 2, pb: 1 }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}>Riders</Typography>
<Typography variant="caption" color="text.secondary">{onlineCount} available</Typography>
</Stack>
<Divider />
<Stack divider={<Divider />}>
{riders.map((r) => {
const load = riderLoad(r.id);
return (
<Stack key={r.id} direction="row" spacing={1.25} alignItems="center" sx={{ px: 2, py: 1.25 }}>
<UserAvatar name={r.name} size={32} />
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }} noWrap>{r.name}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>{r.vehicle} · {r.address.split(',').pop().trim()}</Typography>
</Box>
<Stack alignItems="flex-end" spacing={0.25} sx={{ flexShrink: 0 }}>
<StatusChip status={r.status} />
<Typography variant="caption" color="text.secondary">{r.deliveries + load} today{load > 0 ? ` (+${load})` : ''}</Typography>
</Stack>
</Stack>
);
})}
</Stack>
</Card>
<Card>
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ p: 2, pb: 1 }}>
<Typography variant="h5" sx={{ fontWeight: 700 }}>Assigned this session</Typography>
<Typography variant="caption" color="text.secondary">{assignedList.length}</Typography>
</Stack>
<Divider />
{assignedList.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ p: 2, textAlign: 'center' }}>
Nothing assigned yet. Use Assign or Auto-assign.
</Typography>
) : (
<Stack divider={<Divider />}>
{assignedList.map((a) => (
<Stack key={a.id} direction="row" spacing={1} alignItems="center" sx={{ px: 2, py: 1.25 }}>
<CheckCircleRoundedIcon sx={{ fontSize: 18, color: 'success.main', flexShrink: 0 }} />
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main' }}>{a.id}</Typography>
<Typography variant="caption" color="text.secondary" noWrap> {a.riderName} · {a.vehicle}</Typography>
</Box>
<Tooltip title="Unassign">
<IconButton size="small" onClick={() => { unassignOrder(a.id); showToast(`${a.id} returned to queue`, 'info'); }}>
<UndoOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
))}
</Stack>
)}
</Card>
</Stack>
</Grid>
</Grid>
<Toast {...toast} />
</>
);
}

View File

@@ -1,5 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Tooltip, TextField, MenuItem } from '@mui/material'; import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Tooltip, TextField, MenuItem } from '@mui/material';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined'; import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined';
import BatteryChargingFullOutlinedIcon from '@mui/icons-material/BatteryChargingFullOutlined'; import BatteryChargingFullOutlinedIcon from '@mui/icons-material/BatteryChargingFullOutlined';
import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined'; import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined';
@@ -21,6 +23,7 @@ import { fleet, fleetSummary } from '@/data/mock';
const BLANK = { id: '', model: '', type: 'EV 4W', powertrain: 'EV', capacityKg: '', hub: 'Koramangala Micro Hub' }; const BLANK = { id: '', model: '', type: 'EV 4W', powertrain: 'EV', capacityKg: '', hub: 'Koramangala Micro Hub' };
export default function Fleet() { export default function Fleet() {
const navigate = useNavigate();
const [rows, setRows] = useState(fleet); const [rows, setRows] = useState(fleet);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [form, setForm] = useState(BLANK); const [form, setForm] = useState(BLANK);
@@ -56,7 +59,12 @@ export default function Fleet() {
<PageHeader <PageHeader
title="Fleet & Rider Operating System" title="Fleet & Rider Operating System"
breadcrumbs={[{ label: 'Fleet' }]} breadcrumbs={[{ label: 'Fleet' }]}
action={<Button variant="contained" startIcon={<AddOutlinedIcon />} onClick={() => setOpen(true)}>Add Vehicle</Button>} action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
<Button variant="outlined" startIcon={<TwoWheelerOutlinedIcon />} onClick={() => navigate('/riders')}>Riders</Button>
<Button variant="contained" startIcon={<AddOutlinedIcon />} onClick={() => setOpen(true)}>Add Vehicle</Button>
</Stack>
}
/> />
<LayerBanner <LayerBanner
@@ -176,7 +184,7 @@ export default function Fleet() {
<Grid item xs={12} sm={6}><TextField fullWidth size="small" type="number" label="Capacity (kg)" value={form.capacityKg} onChange={set('capacityKg')} /></Grid> <Grid item xs={12} sm={6}><TextField fullWidth size="small" type="number" label="Capacity (kg)" value={form.capacityKg} onChange={set('capacityKg')} /></Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField select fullWidth size="small" label="Home Hub" value={form.hub} onChange={set('hub')}> <TextField select fullWidth size="small" label="Home Hub" value={form.hub} onChange={set('hub')}>
{['Koramangala Micro Hub', 'Whitefield City Hub', 'Hoskote Regional Hub', 'Andheri City Hub', 'Hitech Cross Dock', 'Bilaspur Regional Hub'].map((h) => <MenuItem key={h} value={h}>{h}</MenuItem>)} {['Coimbatore Regional Hub', 'Peelamedu Micro Hub', 'Hoskote Regional Hub', 'Koramangala Micro Hub', 'Hyderabad Regional Hub', 'Gachibowli Micro Hub'].map((h) => <MenuItem key={h} value={h}>{h}</MenuItem>)}
</TextField> </TextField>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -1,199 +1,192 @@
import { useState } from 'react'; import { Grid, Card, Stack, Typography, Box, LinearProgress, Button, Divider } from '@mui/material';
import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Divider, TextField, MenuItem } from '@mui/material';
import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined'; import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import AddLocationAltOutlinedIcon from '@mui/icons-material/AddLocationAltOutlined'; import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import MainCard from '@/components/MainCard'; import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip'; import AreaChart from '@/components/charts/AreaChart';
import LayerBanner from '@/components/LayerBanner';
import MapPlaceholder from '@/components/MapPlaceholder';
import FormDialog from '@/components/FormDialog';
import Toast, { useToast } from '@/components/Toast'; import Toast, { useToast } from '@/components/Toast';
import { hubs, hubNetworkTypes, lineHauls } from '@/data/mock'; import { hubCityStats, hubThroughput, hubActivity, maintenanceNotices, exceptionQueue } from '@/data/mock';
const BLANK = { name: '', type: 'Micro Hub', city: 'Bengaluru', capacity: '', dock: '' }; const HEALTH = { healthy: { bg: '#E3F6EC', fg: '#00773B', label: 'Healthy' }, watch: { bg: '#FFF7E0', fg: '#8A6500', label: 'Watch' }, critical: { bg: '#FEEAE9', fg: '#A82216', label: 'Critical' } };
const SEV = { high: '#F04134', medium: '#FFBF00', low: '#00A2AE' };
function Stat({ icon: Icon, label, value }) {
return (
<Stack direction="row" spacing={1} alignItems="center">
<Icon sx={{ fontSize: 16, color: 'grey.500' }} />
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.1 }}>{value}</Typography>
<Typography variant="caption" color="text.secondary">{label}</Typography>
</Box>
</Stack>
);
}
function HubCard({ h }) {
const health = HEALTH[h.health] || HEALTH.healthy;
const utilColor = h.utilization > 80 ? 'error' : h.utilization > 70 ? 'warning' : 'success';
return (
<Card sx={{ p: 2.5, height: '100%', borderTop: '3px solid', borderTopColor: 'primary.main' }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" sx={{ mb: 1.5 }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700 }}>{h.city}</Typography>
<Typography variant="caption" color="text.secondary">{h.code} · {h.processed.toLocaleString('en-IN')} processed today</Typography>
</Box>
<Box component="span" sx={{ px: 1, py: 0.25, borderRadius: 1, bgcolor: health.bg, color: health.fg, fontSize: 11, fontWeight: 700 }}>{health.label}</Box>
</Stack>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: 0.5 }}>
<Typography variant="caption" color="text.secondary">Capacity utilization</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{h.utilization}%</Typography>
</Stack>
<LinearProgress variant="determinate" value={h.utilization} color={utilColor} sx={{ height: 8, borderRadius: 4, mb: 2 }} />
<Grid container spacing={1.5}>
<Grid item xs={6}><Stat icon={WarehouseOutlinedIcon} label="Capacity" value={h.capacity.toLocaleString('en-IN')} /></Grid>
<Grid item xs={6}><Stat icon={Inventory2OutlinedIcon} label="Processed" value={h.processed.toLocaleString('en-IN')} /></Grid>
<Grid item xs={6}><Stat icon={TwoWheelerOutlinedIcon} label="Active Riders" value={h.riders} /></Grid>
<Grid item xs={6}><Stat icon={TaskAltOutlinedIcon} label="SLA" value={`${h.sla}%`} /></Grid>
</Grid>
</Card>
);
}
export default function HubNetwork() { export default function HubNetwork() {
const [rows, setRows] = useState(hubs);
const [open, setOpen] = useState(false);
const [form, setForm] = useState(BLANK);
const [toast, showToast] = useToast(); const [toast, showToast] = useToast();
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
const addHub = () => {
if (!form.name.trim()) return showToast('Enter a hub name', 'warning');
const cityCode = (form.city.slice(0, 3) || 'HUB').toUpperCase();
setRows((r) => [
...r,
{ id: `HUB-${cityCode}-${String(10 + r.length).slice(-2)}`, name: form.name, type: form.type, city: form.city, lat: 0, lng: 0, capacity: Number(form.capacity) || 1000, load: 0, inbound: 0, outbound: 0, dock: Number(form.dock) || 4, status: 'online' }
]);
setForm(BLANK);
setOpen(false);
showToast(`${form.name} added to network`);
};
const totalCap = rows.reduce((s, h) => s + h.capacity, 0);
const totalLoad = rows.reduce((s, h) => s + h.load, 0);
const util = Math.round((totalLoad / totalCap) * 100);
return ( return (
<> <>
<PageHeader <PageHeader
title="Hub & Network Orchestration" title="Hub Operations Center"
breadcrumbs={[{ label: 'Hub Network' }]} breadcrumbs={[{ label: 'Network' }, { label: 'Hubs' }]}
action={<Button variant="contained" startIcon={<AddLocationAltOutlinedIcon />} onClick={() => setOpen(true)}>Add Hub</Button>} action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('Hub report exported')}>Export</Button>}
/>
<LayerBanner
no={3}
icon={HubOutlinedIcon}
color="#0E7C7B"
title="Intelligent Hub Network"
subtitle="MileTruth AI selects the optimal origin hub, then sorts, line-hauls and routes to destination."
steps={['Nearest Hub', 'Pickup Assignment', 'Hub Operations', 'Line-Haul Transfer', 'Destination Hub']}
/> />
{/* Top — hub summary cards */}
<Grid container spacing={2.5}> <Grid container spacing={2.5}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active Hubs" value={hubs.length} icon={WarehouseOutlinedIcon} color="info" caption="across 4 cities" /></Grid> {hubCityStats.map((h) => (
<Grid item xs={12} sm={6} lg={3}><StatCard title="Network Utilisation" value={`${util}%`} icon={HubOutlinedIcon} color="warning" trend={3.1} caption="vs yesterday" /></Grid> <Grid key={h.city} item xs={12} md={4}><HubCard h={h} /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Line-Hauls Running" value={lineHauls.filter((l) => l.status === 'in-transit').length} icon={LocalShippingOutlinedIcon} color="primary" caption="inter-city" /></Grid> ))}
<Grid item xs={12} sm={6} lg={3}><StatCard title="Throughput Today" value={totalLoad.toLocaleString('en-IN')} icon={RouteOutlinedIcon} color="success" trend={6.4} caption="parcels sorted" /></Grid>
<Grid item xs={12} lg={8}>
<MainCard title="Hub Load & Capacity" noPadding>
<Table>
<TableHead>
<TableRow>
<TableCell>Hub</TableCell>
<TableCell>Type</TableCell>
<TableCell>City</TableCell>
<TableCell>Load / Capacity</TableCell>
<TableCell align="center">Docks</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((h) => {
const pct = Math.round((h.load / h.capacity) * 100);
return (
<TableRow key={h.id} hover>
<TableCell>
<Typography variant="subtitle2">{h.name}</Typography>
<Typography variant="caption" color="text.secondary">{h.id}</Typography>
</TableCell>
<TableCell><Typography variant="caption">{h.type}</Typography></TableCell>
<TableCell>{h.city}</TableCell>
<TableCell sx={{ minWidth: 160 }}>
<Stack direction="row" justifyContent="space-between">
<Typography variant="caption" color="text.secondary">{h.load.toLocaleString('en-IN')} / {h.capacity.toLocaleString('en-IN')}</Typography>
<Typography variant="caption" sx={{ fontWeight: 600 }}>{pct}%</Typography>
</Stack>
<LinearProgress variant="determinate" value={pct} color={pct > 90 ? 'error' : pct > 75 ? 'warning' : 'success'} sx={{ height: 6, borderRadius: 3, mt: 0.5 }} />
</TableCell>
<TableCell align="center">{h.dock}</TableCell>
<TableCell><StatusChip status={h.status} /></TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</MainCard>
</Grid>
<Grid item xs={12} lg={4}>
<MainCard title="Network Types">
<Stack divider={<Divider />} spacing={0}>
{hubNetworkTypes.map((t) => (
<Stack key={t.type} direction="row" spacing={2} alignItems="center" sx={{ py: 1.4 }}>
<Avatar variant="rounded" sx={{ bgcolor: 'info.lighter', color: 'info.main', width: 40, height: 40 }}>
<WarehouseOutlinedIcon fontSize="small" />
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle2">{t.type}</Typography>
<Typography variant="caption" color="text.secondary">{t.desc}</Typography>
</Box>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.800' }}>{t.count}</Typography>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} lg={7}>
<MainCard title="Line-Haul Corridors" noPadding>
<Table>
<TableHead>
<TableRow>
<TableCell>Corridor</TableCell>
<TableCell>Vehicle</TableCell>
<TableCell align="right">Distance</TableCell>
<TableCell>Load</TableCell>
<TableCell>ETA</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{lineHauls.map((l) => (
<TableRow key={l.id} hover>
<TableCell>
<Typography variant="subtitle2">{l.corridor}</Typography>
<Typography variant="caption" color="text.secondary">{l.from} {l.to}</Typography>
</TableCell>
<TableCell><Typography variant="caption">{l.vehicle}</Typography></TableCell>
<TableCell align="right">{l.distance.toLocaleString('en-IN')} km</TableCell>
<TableCell sx={{ minWidth: 90 }}>
<LinearProgress variant="determinate" value={l.load} sx={{ height: 6, borderRadius: 3 }} />
<Typography variant="caption" color="text.secondary">{l.load}%</Typography>
</TableCell>
<TableCell>{l.eta}</TableCell>
<TableCell><StatusChip status={l.status} /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</MainCard>
</Grid>
<Grid item xs={12} lg={5}>
<MainCard title="Network Map">
<MapPlaceholder
height={320}
label="Hub Network"
showRoute={false}
pins={[
{ x: '24%', y: '60%', label: 'BLR', color: '#0E7C7B' },
{ x: '14%', y: '38%', label: 'MUM', color: '#1D4ED8' },
{ x: '40%', y: '34%', label: 'HYD', color: '#EA580C' },
{ x: '46%', y: '14%', label: 'DEL', color: '#C01227' }
]}
/>
</MainCard>
</Grid>
</Grid> </Grid>
<FormDialog open={open} onClose={() => setOpen(false)} title="Add Hub" onSubmit={addHub} submitLabel="Add Hub"> {/* Middle — throughput trends + capacity comparison */}
<Grid container spacing={2} sx={{ mt: 0 }}> <Box sx={{ mt: 3.5 }}>
<Grid item xs={12}><TextField fullWidth size="small" label="Hub Name" value={form.name} onChange={set('name')} placeholder="e.g. Electronic City Micro Hub" /></Grid> <Grid container spacing={2.5}>
<Grid item xs={12} sm={6}> <Grid item xs={12} lg={8}>
<TextField select fullWidth size="small" label="Type" value={form.type} onChange={set('type')}> <MainCard
{['Micro Hub', 'City Hub', 'Regional Hub', 'Cross Dock'].map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)} title="Throughput Trends"
</TextField> action={
<Stack direction="row" spacing={1.5}>
<Legend color="#EA580C" label="Coimbatore" />
<Legend color="#1D4ED8" label="Bengaluru" />
<Legend color="#0E7C7B" label="Hyderabad" />
</Stack>
}
>
<Box sx={{ py: 1 }}>
<AreaChart
height={300}
labels={hubThroughput.map((d) => d.m)}
series={[
{ name: 'Coimbatore', color: '#EA580C', data: hubThroughput.map((d) => d.cbe) },
{ name: 'Bengaluru', color: '#1D4ED8', data: hubThroughput.map((d) => d.blr) },
{ name: 'Hyderabad', color: '#0E7C7B', data: hubThroughput.map((d) => d.hyd) }
]}
/>
</Box>
</MainCard>
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} lg={4}>
<TextField select fullWidth size="small" label="City" value={form.city} onChange={set('city')}> <MainCard title="Hub Performance Comparison" sx={{ height: '100%' }}>
{['Bengaluru', 'Mumbai', 'Delhi NCR', 'Hyderabad', 'Chennai', 'Pune'].map((c) => <MenuItem key={c} value={c}>{c}</MenuItem>)} <Stack spacing={2.25} sx={{ py: 0.5 }}>
</TextField> {hubCityStats.map((h) => (
<Box key={h.city}>
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.4 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{h.city}</Typography>
<Typography variant="caption" color="text.secondary">{h.utilization}% · SLA {h.sla}%</Typography>
</Stack>
<LinearProgress variant="determinate" value={h.utilization} color={h.utilization > 80 ? 'error' : h.utilization > 70 ? 'warning' : 'success'} sx={{ height: 7, borderRadius: 4 }} />
</Box>
))}
</Stack>
</MainCard>
</Grid> </Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" type="number" label="Capacity (parcels)" value={form.capacity} onChange={set('capacity')} /></Grid>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" type="number" label="Docks" value={form.dock} onChange={set('dock')} /></Grid>
</Grid> </Grid>
</FormDialog> </Box>
{/* Bottom — activity / alerts / maintenance */}
<Box sx={{ mt: 3.5 }}>
<Grid container spacing={2.5}>
<Grid item xs={12} lg={4}>
<MainCard title="Recent Hub Activity" noPadding>
<Stack divider={<Divider />}>
{hubActivity.map((a, i) => (
<Stack key={i} direction="row" spacing={1.25} alignItems="flex-start" sx={{ px: 2, py: 1.25 }}>
<Box sx={{ mt: 0.6, width: 8, height: 8, borderRadius: '50%', bgcolor: 'info.main', flexShrink: 0 }} />
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Stack direction="row" justifyContent="space-between" spacing={1}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }} noWrap>{a.hub}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>{a.time}</Typography>
</Stack>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{a.text}</Typography>
</Box>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<MainCard title="Operational Alerts" noPadding>
<Stack divider={<Divider />}>
{exceptionQueue.slice(0, 5).map((e, i) => (
<Stack key={`${e.id}-${i}`} direction="row" spacing={1.25} alignItems="flex-start" sx={{ px: 2, py: 1.25 }}>
<Box sx={{ mt: 0.6, width: 8, height: 8, borderRadius: '50%', bgcolor: SEV[e.severity] || '#8C8C8C', flexShrink: 0 }} />
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: SEV[e.severity] === '#F04134' ? 'error.dark' : 'grey.800' }}>{e.category}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }} noWrap>{e.detail}</Typography>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>{e.age}</Typography>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<MainCard title="Maintenance Notices" noPadding>
<Stack divider={<Divider />}>
{maintenanceNotices.map((m, i) => (
<Stack key={i} direction="row" spacing={1.25} alignItems="center" sx={{ px: 2, py: 1.4 }}>
<BuildOutlinedIcon sx={{ fontSize: 18, color: SEV[m.severity] || 'grey.500', flexShrink: 0 }} />
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }} noWrap>{m.item}</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{m.hub}</Typography>
</Box>
<Typography variant="caption" sx={{ fontWeight: 700, color: m.severity === 'high' ? 'error.main' : 'grey.600', flexShrink: 0 }}>{m.due}</Typography>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
</Grid>
</Box>
<Toast {...toast} /> <Toast {...toast} />
</> </>
); );
} }
function Legend({ color, label }) {
return (
<Stack direction="row" spacing={0.5} alignItems="center">
<Box sx={{ width: 9, height: 9, borderRadius: '3px', bgcolor: color }} />
<Typography variant="caption" color="text.secondary">{label}</Typography>
</Stack>
);
}

View File

@@ -0,0 +1,22 @@
import TabbedWorkspace from '@/components/TabbedWorkspace';
import LiveTracking from '@/pages/tracking/LiveTracking';
import DispatchBoard from '@/pages/dispatch/DispatchBoard';
import ShipmentJourney from '@/pages/tracking/ShipmentJourney';
import ColdChain from '@/pages/coldchain/ColdChain';
// ==============================|| DISPATCH & TRACKING — operations workspace ||============================== //
// Merges Live Tracking (flagship map), Dispatch board (with AI suggestions), Shipment Journey
// and Cold Chain into one screen. AI recommendations live inside the Dispatch tab.
export default function DispatchTracking() {
return (
<TabbedWorkspace
tabs={[
{ key: 'live', label: 'Live Tracking', element: <LiveTracking /> },
{ key: 'dispatch', label: 'Dispatch', element: <DispatchBoard /> },
{ key: 'journey', label: 'Journey', element: <ShipmentJourney /> },
{ key: 'cold-chain', label: 'Cold Chain', element: <ColdChain /> }
]}
/>
);
}

View File

@@ -1,136 +1,138 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { import {
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab, Card, Stack, Button, Box, Tabs, Tab, Table, TableBody, TableCell, TableContainer, TableHead,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox, IconButton, TableRow, Checkbox, IconButton, Tooltip, TablePagination, Typography
Tooltip, TablePagination, Typography, SpeedDial, SpeedDialAction, Chip
} from '@mui/material'; } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined'; import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import PersonAddAltOutlinedIcon from '@mui/icons-material/PersonAddAltOutlined';
import TuneIcon from '@mui/icons-material/Tune';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import StatusChip from '@/components/StatusChip';
import TabLabelCount from '@/components/TabLabelCount';
import { orders } from '@/data/mock';
import { inr } from '@/utils/format';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined'; import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import PageHeader from '@/components/PageHeader';
import KpiStrip from '@/components/KpiStrip';
import PageToolbar from '@/components/PageToolbar';
import StatusChip from '@/components/StatusChip';
import TabLabelCount from '@/components/TabLabelCount';
import { orders } from '@/data/mock';
import { inr } from '@/utils/format';
import { useOps } from '@/store/OpsStore';
import { useFilters } from '@/store/Filters';
const ACTIVE = ['created', 'pending', 'picked', 'active'];
const TABS = [ const TABS = [
{ key: 'created', label: 'Created' }, { key: 'all', label: 'All' },
{ key: 'pending', label: 'Pending' }, { key: 'active', label: 'Active' },
{ key: 'delivered', label: 'Delivered' }, { key: 'delivered', label: 'Delivered' },
{ key: 'cancelled', label: 'Cancelled' } { key: 'delayed', label: 'Delayed' },
{ key: 'exceptions', label: 'Exceptions' }
]; ];
const STICKY_HEAD = {
'& .MuiTableCell-head': { position: 'sticky', top: 0, zIndex: 2, bgcolor: 'grey.50' },
'& .MuiTableRow-root:hover': { backgroundColor: 'grey.50' }
};
export default function OrdersList() { export default function OrdersList() {
const navigate = useNavigate(); const navigate = useNavigate();
const { exceptions } = useOps();
const { location } = useFilters(); // global location — single source of truth
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [tab, setTab] = useState(0); const [tab, setTab] = useState(0);
const [search, setSearch] = useState(searchParams.get('q') || ''); const [search, setSearch] = useState(searchParams.get('q') || '');
const [tenant, setTenant] = useState('all'); const [tenant, setTenant] = useState('all');
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5); const [rpp, setRpp] = useState(10);
const [selected, setSelected] = useState([]); const [selected, setSelected] = useState([]);
const tabKey = TABS[tab].key; const tabKey = TABS[tab].key;
const inTab = (o, key) => {
if (key === 'all') return true;
if (key === 'active') return ACTIVE.includes(o.status);
if (key === 'delivered') return o.status === 'delivered';
if (key === 'delayed') return o.status === 'pending'; // at-risk / awaiting action
if (key === 'exceptions') return Boolean(exceptions[o.id]);
return true;
};
const filtered = useMemo( const filtered = useMemo(
() => () =>
orders.filter((o) => { orders.filter((o) => {
const matchTab = tabKey === 'created' ? true : o.status === tabKey;
const matchTenant = tenant === 'all' || o.tenant === tenant; const matchTenant = tenant === 'all' || o.tenant === tenant;
const matchLoc = location === 'all' || o.location === location;
const matchSearch = const matchSearch =
!search || !search || [o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase());
[o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase()); return inTab(o, tabKey) && matchTenant && matchLoc && matchSearch;
return matchTab && matchTenant && matchSearch;
}), }),
[tabKey, tenant, search] // eslint-disable-next-line react-hooks/exhaustive-deps
[tabKey, tenant, location, search, exceptions]
); );
const paged = filtered.slice(page * rpp, page * rpp + rpp); const paged = filtered.slice(page * rpp, page * rpp + rpp);
const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id])); const toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
const counts = { const count = (k) => orders.filter((o) => inTab(o, k)).length;
created: orders.length,
pending: orders.filter((o) => o.status === 'pending').length, const codTotal = orders.reduce((s, o) => s + (o.cod || 0), 0);
delivered: orders.filter((o) => o.status === 'delivered').length, const kpis = [
cancelled: orders.filter((o) => o.status === 'cancelled').length { label: 'Total Orders', value: orders.length, icon: Inventory2OutlinedIcon },
}; { label: 'Active', value: count('active'), color: '#1D4ED8', icon: HourglassEmptyOutlinedIcon },
{ label: 'Delivered', value: count('delivered'), color: '#00773B', icon: CheckCircleOutlineIcon },
{ label: 'Exceptions', value: count('exceptions'), color: '#A82216', icon: CancelOutlinedIcon },
{ label: 'COD to Collect', value: inr(codTotal), color: '#00727B', icon: CurrencyRupeeIcon }
];
return ( return (
<> <>
<PageHeader <PageHeader
title="Orders" title="Shipments"
breadcrumbs={[{ label: 'Orders' }]} breadcrumbs={[{ label: 'Orders' }]}
action={ action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.25}>
<Button variant="outlined" startIcon={<PostAddOutlinedIcon />} onClick={() => navigate('/orders/create-multiple')}> <Button variant="text" startIcon={<AutoAwesomeOutlinedIcon />} onClick={() => navigate('/orders/assign')}>Assign</Button>
Create Multiple <Button variant="outlined" startIcon={<PostAddOutlinedIcon />} onClick={() => navigate('/orders/create-multiple')}>Bulk</Button>
</Button> <Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders/create')}>Create Order</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders/create')}>
Create Order
</Button>
</Stack> </Stack>
} }
/> />
<Grid container spacing={2.5} sx={{ mb: 1 }}> <KpiStrip items={kpis} />
<Grid item xs={12} sm={6} lg={3}><StatCard title="Created Orders" value={counts.created} icon={Inventory2OutlinedIcon} caption="100%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Pending Orders" value={counts.pending} icon={HourglassEmptyOutlinedIcon} color="warning" caption="25%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delivered Orders" value={counts.delivered} icon={CheckCircleOutlineIcon} color="success" caption="25%" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cancelled Orders" value={counts.cancelled} icon={CancelOutlinedIcon} color="error" caption="12.5%" /></Grid>
</Grid>
<Card sx={{ mt: 1.5 }}> <Card>
{/* filter toolbar */} <Box sx={{ px: 1.5, borderBottom: 1, borderColor: 'divider' }}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}> <Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
<TextField
size="small" placeholder="Search orders…" value={search} onChange={(e) => setSearch(e.target.value)}
sx={{ minWidth: 240 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
<Button variant="outlined" size="medium" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
<MenuItem value="all">All Tenants</MenuItem>
{[...new Set(orders.map((o) => o.tenant))].map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
<TextField select size="small" defaultValue="all" sx={{ minWidth: 150 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{[...new Set(orders.map((o) => o.location))].map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
{TABS.map((t, i) => ( {TABS.map((t, i) => (
<Tab key={t.key} label={<TabLabelCount label={t.label} count={counts[t.key]} active={tab === i} />} /> <Tab key={t.key} label={<TabLabelCount label={t.label} count={count(t.key)} active={tab === i} />} />
))} ))}
</Tabs> </Tabs>
</Box> </Box>
{selected.length > 0 && ( <PageToolbar
<Stack direction="row" alignItems="center" spacing={2} sx={{ px: 2, py: 1, bgcolor: 'primary.lighter' }}> search={search}
<Typography variant="subtitle2" color="primary.dark">{selected.length} selected</Typography> onSearch={(v) => { setSearch(v); setPage(0); }}
<Button size="small" color="error" startIcon={<DeleteOutlineIcon />}>Delete</Button> searchPlaceholder="Search ID, customer, pickup, drop…"
</Stack> filters={[
)} {
label: 'Client', value: tenant, onChange: (v) => { setTenant(v); setPage(0); }, width: 170,
options: [{ value: 'all', label: 'All Clients' }, ...[...new Set(orders.map((o) => o.tenant))].map((t) => ({ value: t, label: t }))]
}
]}
actions={
selected.length > 0 && (
<Button size="small" color="error" variant="outlined" startIcon={<DeleteOutlineIcon />}>
Delete ({selected.length})
</Button>
)
}
/>
<TableContainer> <TableContainer sx={{ maxHeight: { md: 'calc(100vh - 360px)' } }}>
<Table> <Table stickyHeader size="small" sx={STICKY_HEAD}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell padding="checkbox"> <TableCell padding="checkbox">
@@ -140,56 +142,56 @@ export default function OrdersList() {
onChange={(e) => setSelected(e.target.checked ? paged.map((o) => o.id) : [])} onChange={(e) => setSelected(e.target.checked ? paged.map((o) => o.id) : [])}
/> />
</TableCell> </TableCell>
<TableCell>#</TableCell> <TableCell>Order ID</TableCell>
<TableCell>Tenant</TableCell> <TableCell>Client</TableCell>
<TableCell>Location</TableCell> <TableCell>Location</TableCell>
<TableCell>Pickup</TableCell> <TableCell>Route</TableCell>
<TableCell>Drop</TableCell> <TableCell align="center">Qty</TableCell>
<TableCell align="center">QTY</TableCell>
<TableCell align="right">COD</TableCell> <TableCell align="right">COD</TableCell>
<TableCell align="right">KMS</TableCell> <TableCell align="right">KM</TableCell>
<TableCell align="right">Charges</TableCell> <TableCell align="right">Charges</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Status</TableCell> <TableCell>Status</TableCell>
<TableCell align="center">Actions</TableCell> <TableCell align="right">Actions</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{paged.map((o) => ( {paged.map((o) => (
<TableRow key={o.id} hover selected={selected.includes(o.id)}> <TableRow key={o.id} hover selected={selected.includes(o.id)}>
<TableCell padding="checkbox"><Checkbox checked={selected.includes(o.id)} onChange={() => toggle(o.id)} /></TableCell> <TableCell padding="checkbox"><Checkbox checked={selected.includes(o.id)} onChange={() => toggle(o.id)} /></TableCell>
<TableCell sx={{ fontWeight: 600, color: 'primary.main', cursor: 'pointer' }} onClick={() => navigate(`/orders/${o.id}`)}>{o.id}</TableCell> <TableCell sx={{ fontWeight: 700, color: 'primary.main', cursor: 'pointer' }} onClick={() => navigate(`/orders/${o.id}`)}>{o.id}</TableCell>
<TableCell>{o.tenant}</TableCell> <TableCell>{o.tenant}</TableCell>
<TableCell>{o.location}</TableCell> <TableCell><Typography variant="caption" color="text.secondary">{o.location}</Typography></TableCell>
<TableCell>{o.pickup}</TableCell> <TableCell>
<TableCell>{o.drop}</TableCell> <Typography variant="caption" color="text.secondary" noWrap>{o.pickup} {o.drop}</Typography>
</TableCell>
<TableCell align="center">{o.qty}</TableCell> <TableCell align="center">{o.qty}</TableCell>
<TableCell align="right">{o.cod ? inr(o.cod) : '—'}</TableCell> <TableCell align="right">{o.cod ? inr(o.cod) : '—'}</TableCell>
<TableCell align="right">{o.kms}</TableCell> <TableCell align="right">{o.kms}</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell> <TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
<TableCell><Typography variant="caption" color="text.secondary" noWrap>{o.notes || '—'}</Typography></TableCell>
<TableCell><StatusChip status={o.status} /></TableCell> <TableCell><StatusChip status={o.status} /></TableCell>
<TableCell align="center"> <TableCell align="right">
<Tooltip title="View"><IconButton size="small" onClick={() => navigate(`/orders/${o.id}`)}><VisibilityOutlinedIcon fontSize="small" /></IconButton></Tooltip> <Tooltip title="View"><IconButton size="small" onClick={() => navigate(`/orders/${o.id}`)}><VisibilityOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Edit"><IconButton size="small"><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip> <Tooltip title="Edit"><IconButton size="small"><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
<Tooltip title="Delete"><IconButton size="small" color="error"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip> <Tooltip title="Delete"><IconButton size="small" color="error"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{paged.length === 0 && (
<TableRow>
<TableCell colSpan={11} sx={{ textAlign: 'center', py: 5, color: 'text.secondary' }}>
No orders match these filters.
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
<TablePagination <TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)} component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]} rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[10, 25, 50]}
/> />
</Card> </Card>
<SpeedDial ariaLabel="Order actions" icon={<TuneIcon />} sx={{ position: 'fixed', bottom: 28, right: 28 }} FabProps={{ color: 'primary' }}>
<SpeedDialAction icon={<AutoAwesomeOutlinedIcon />} tooltipTitle="AI Optimisation" onClick={() => navigate('/orders/assign')} />
<SpeedDialAction icon={<PersonAddAltOutlinedIcon />} tooltipTitle="Manual Assign" onClick={() => navigate('/orders/assign')} />
<SpeedDialAction icon={<DeleteOutlineIcon />} tooltipTitle="Delete" />
</SpeedDial>
</> </>
); );
} }

View File

@@ -13,15 +13,19 @@ import CloseIcon from '@mui/icons-material/Close';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import StatusChip from '@/components/StatusChip'; import StatusChip from '@/components/StatusChip';
import MapPlaceholder from '@/components/MapPlaceholder'; import MapPlaceholder from '@/components/MapPlaceholder';
import { ordersDetailReport, locations, tenantsList } from '@/data/mock'; import DateRangeFilter from '@/components/DateRangeFilter';
import FilterSummary from '@/components/FilterSummary';
import { ordersDetailReport, orders, tenantsList } from '@/data/mock';
import { inr } from '@/utils/format'; import { inr } from '@/utils/format';
import { useFilters, inRange } from '@/store/Filters';
const STATUSES = ['all', 'created', 'pending', 'picked', 'active', 'delivered', 'cancelled']; const STATUSES = ['all', 'created', 'pending', 'picked', 'active', 'delivered', 'cancelled'];
// join the report to raw orders for the date + location each row actually belongs to
const ORDER_META = Object.fromEntries(orders.map((o) => [o.id, { date: o.date, location: o.location }]));
export default function OrdersDetails() { export default function OrdersDetails() {
const [location, setLocation] = useState('all'); const { location, range } = useFilters(); // global location + date range
const [tenant, setTenant] = useState('all'); const [tenant, setTenant] = useState('all');
const [loc2, setLoc2] = useState('all');
const [status, setStatus] = useState('all'); const [status, setStatus] = useState('all');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@@ -32,16 +36,21 @@ export default function OrdersDetails() {
const filtered = useMemo( const filtered = useMemo(
() => () =>
ordersDetailReport.filter((o) => { ordersDetailReport.filter((o) => {
const meta = ORDER_META[o.id] || {};
const matchStatus = status === 'all' || o.status === status; const matchStatus = status === 'all' || o.status === status;
const matchTenant = tenant === 'all' || o.client === tenant; const matchTenant = tenant === 'all' || o.client === tenant;
const matchLoc = location === 'all' || meta.location === location;
const matchDate = meta.date ? inRange(meta.date, range) : true;
const matchSearch = const matchSearch =
!search || !search ||
[o.id, o.client, o.pickup, o.drop].join(' ').toLowerCase().includes(search.toLowerCase()); [o.id, o.client, o.pickup, o.drop].join(' ').toLowerCase().includes(search.toLowerCase());
return matchStatus && matchTenant && matchSearch; return matchStatus && matchTenant && matchLoc && matchDate && matchSearch;
}), }),
[status, tenant, search] [status, tenant, search, location, range]
); );
// reset to first page whenever the result set changes
useMemo(() => setPage(0), [status, tenant, search, location, range]); // eslint-disable-line react-hooks/exhaustive-deps
const paged = filtered.slice(page * rpp, page * rpp + rpp); const paged = filtered.slice(page * rpp, page * rpp + rpp);
return ( return (
@@ -49,12 +58,7 @@ export default function OrdersDetails() {
<PageHeader <PageHeader
title="Orders Details" title="Orders Details"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Details' }]} breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Details' }]}
action={ action={<DateRangeFilter />}
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/> />
<Card> <Card>
@@ -63,13 +67,6 @@ export default function OrdersDetails() {
<MenuItem value="all">All Tenants</MenuItem> <MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)} {tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField> </TextField>
<TextField select size="small" value={loc2} onChange={(e) => setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<TextField select size="small" value={status} onChange={(e) => { setStatus(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Status"> <TextField select size="small" value={status} onChange={(e) => { setStatus(e.target.value); setPage(0); }} sx={{ minWidth: 150 }} label="Status">
{STATUSES.map((s) => <MenuItem key={s} value={s}>{s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)}</MenuItem>)} {STATUSES.map((s) => <MenuItem key={s} value={s}>{s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)}</MenuItem>)}
</TextField> </TextField>
@@ -83,6 +80,10 @@ export default function OrdersDetails() {
</Button> </Button>
</Stack> </Stack>
<Box sx={{ px: 2, pb: 1.5 }}>
<FilterSummary count={filtered.length} />
</Box>
<TableContainer> <TableContainer>
<Table size="small"> <Table size="small">
<TableHead> <TableHead>
@@ -130,6 +131,13 @@ export default function OrdersDetails() {
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell> <TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
</TableRow> </TableRow>
))} ))}
{filtered.length === 0 && (
<TableRow>
<TableCell colSpan={16} sx={{ textAlign: 'center', py: 5, color: 'text.secondary' }}>
No orders in this date range / location. Try widening the range or clearing the location.
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
@@ -163,11 +171,10 @@ export default function OrdersDetails() {
The export will include {filtered.length} record(s) matching the current filters: The export will include {filtered.length} record(s) matching the current filters:
</Typography> </Typography>
<Grid container spacing={1.5}> <Grid container spacing={1.5}>
<Filter label="Date Range" value={range.label} />
<Filter label="Location" value={location === 'all' ? 'All Locations' : location} /> <Filter label="Location" value={location === 'all' ? 'All Locations' : location} />
<Filter label="Tenant" value={tenant === 'all' ? 'All Tenants' : tenant} /> <Filter label="Tenant" value={tenant === 'all' ? 'All Tenants' : tenant} />
<Filter label="Location (2)" value={loc2 === 'all' ? 'All Locations' : loc2} />
<Filter label="Status" value={status === 'all' ? 'All Status' : status} /> <Filter label="Status" value={status === 'all' ? 'All Status' : status} />
<Filter label="Date Range" value="Jun 01 Jun 05" />
<Filter label="Search" value={search || '—'} /> <Filter label="Search" value={search || '—'} />
</Grid> </Grid>
</DialogContent> </DialogContent>

View File

@@ -9,8 +9,11 @@ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import { ordersSummary, locations, tenantsList } from '@/data/mock'; import DateRangeFilter from '@/components/DateRangeFilter';
import FilterSummary from '@/components/FilterSummary';
import { ordersSummary, tenantsList } from '@/data/mock';
import { inr } from '@/utils/format'; import { inr } from '@/utils/format';
import { useFilters } from '@/store/Filters';
// Show 0 in red, anything else normally. // Show 0 in red, anything else normally.
function NumCell({ value, align = 'center', bold = false }) { function NumCell({ value, align = 'center', bold = false }) {
@@ -23,14 +26,16 @@ function NumCell({ value, align = 'center', bold = false }) {
} }
export default function OrdersSummary() { export default function OrdersSummary() {
const { location } = useFilters(); // global location — single source of truth
const [open, setOpen] = useState({}); const [open, setOpen] = useState({});
const [location, setLocation] = useState('all');
const [tenant, setTenant] = useState('all'); const [tenant, setTenant] = useState('all');
const [loc2, setLoc2] = useState('all');
const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] })); const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] }));
const totals = ordersSummary.reduce( // location is the global filter; tenant is this report's own (distinct purpose)
const rows = ordersSummary.filter((r) => (location === 'all' || r.location === location) && (tenant === 'all' || r.tenant === tenant));
const totals = rows.reduce(
(a, r) => ({ (a, r) => ({
oPending: a.oPending + r.orders.pending, oPending: a.oPending + r.orders.pending,
oCancelled: a.oCancelled + r.orders.cancelled, oCancelled: a.oCancelled + r.orders.cancelled,
@@ -46,35 +51,24 @@ export default function OrdersSummary() {
{ oPending: 0, oCancelled: 0, oCompleted: 0, dPending: 0, dCancelled: 0, dCompleted: 0, collection: 0, kms: 0, actualKms: 0, amount: 0 } { oPending: 0, oCancelled: 0, oCompleted: 0, dPending: 0, dCancelled: 0, dCompleted: 0, collection: 0, kms: 0, actualKms: 0, amount: 0 }
); );
const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' }; const headSx = { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', whiteSpace: 'nowrap', borderBottom: '2px solid', borderColor: 'grey.200' };
return ( return (
<> <>
<PageHeader <PageHeader
title="Orders Summary" title="Orders Summary"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Summary' }]} breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Summary' }]}
action={ action={<DateRangeFilter />}
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/> />
<Card> <Card>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}> <Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}>
Jun 01 Jun 05
</Button>
<Box sx={{ flexGrow: 1 }} />
<TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant"> <TextField select size="small" value={tenant} onChange={(e) => setTenant(e.target.value)} sx={{ minWidth: 170 }} label="Tenant">
<MenuItem value="all">All Tenants</MenuItem> <MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)} {tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField> </TextField>
<TextField select size="small" value={loc2} onChange={(e) => setLoc2(e.target.value)} sx={{ minWidth: 160 }} label="Location"> <Box sx={{ flexGrow: 1 }} />
<MenuItem value="all">All Locations</MenuItem> <FilterSummary count={rows.length} />
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
</Stack> </Stack>
<TableContainer> <TableContainer>
@@ -101,7 +95,10 @@ export default function OrdersSummary() {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{ordersSummary.map((r, idx) => ( {rows.length === 0 && (
<TableRow><TableCell colSpan={13} sx={{ textAlign: 'center', py: 5, color: 'text.secondary' }}>No tenants match this location.</TableCell></TableRow>
)}
{rows.map((r, idx) => (
<Fragment key={r.id}> <Fragment key={r.id}>
<TableRow hover> <TableRow hover>
<TableCell padding="checkbox"> <TableCell padding="checkbox">
@@ -172,7 +169,7 @@ export default function OrdersSummary() {
))} ))}
{/* totals */} {/* totals */}
<TableRow sx={{ '& td': { bgcolor: 'primary.lighter', borderTop: 2, borderColor: 'primary.light' } }}> <TableRow sx={{ '& td': { bgcolor: 'grey.100', borderTop: 2, borderColor: 'grey.300' } }}>
<TableCell /> <TableCell />
<TableCell colSpan={2} sx={{ fontWeight: 700 }}>Totals</TableCell> <TableCell colSpan={2} sx={{ fontWeight: 700 }}>Totals</TableCell>
<NumCell value={totals.oPending} bold /> <NumCell value={totals.oPending} bold />

View File

@@ -0,0 +1,21 @@
import TabbedWorkspace from '@/components/TabbedWorkspace';
import OrdersSummary from '@/pages/reports/OrdersSummary';
import RidersSummary from '@/pages/reports/RidersSummary';
import Analytics from '@/pages/analytics/Analytics';
import OrdersDetails from '@/pages/reports/OrdersDetails';
// ==============================|| REPORTS — reporting center ||============================== //
// Merges the report surfaces into one tabbed center: Orders, Riders, Revenue, Performance.
export default function ReportsHub() {
return (
<TabbedWorkspace
tabs={[
{ key: 'orders', label: 'Orders', element: <OrdersSummary /> },
{ key: 'riders', label: 'Riders', element: <RidersSummary /> },
{ key: 'revenue', label: 'Revenue', element: <Analytics /> },
{ key: 'performance', label: 'Performance', element: <OrdersDetails /> }
]}
/>
);
}

View File

@@ -13,7 +13,9 @@ import CloseIcon from '@mui/icons-material/Close';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import UserAvatar from '@/components/UserAvatar'; import UserAvatar from '@/components/UserAvatar';
import MapPlaceholder from '@/components/MapPlaceholder'; import MapPlaceholder from '@/components/MapPlaceholder';
import { ridersSummary, locations } from '@/data/mock'; import DateRangeFilter from '@/components/DateRangeFilter';
import FilterSummary from '@/components/FilterSummary';
import { ridersSummary } from '@/data/mock';
import { inr } from '@/utils/format'; import { inr } from '@/utils/format';
function NumCell({ value, align = 'center' }) { function NumCell({ value, align = 'center' }) {
@@ -32,32 +34,24 @@ function KmsChips({ kms, actual }) {
export default function RidersSummary() { export default function RidersSummary() {
const [open, setOpen] = useState({}); const [open, setOpen] = useState({});
const [location, setLocation] = useState('all');
const [mapRider, setMapRider] = useState(null); const [mapRider, setMapRider] = useState(null);
const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] })); const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] }));
const totalAmount = ridersSummary.reduce((a, r) => a + r.amount, 0); const totalAmount = ridersSummary.reduce((a, r) => a + r.amount, 0);
const headSx = { bgcolor: 'primary.lighter', fontWeight: 700, color: 'primary.dark', whiteSpace: 'nowrap' }; const headSx = { bgcolor: 'grey.50', fontWeight: 700, color: 'grey.700', whiteSpace: 'nowrap', borderBottom: '2px solid', borderColor: 'grey.200' };
return ( return (
<> <>
<PageHeader <PageHeader
title="Riders Summary" title="Riders Summary"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Riders Summary' }]} breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Riders Summary' }]}
action={ action={<DateRangeFilter />}
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 160 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
}
/> />
<Card> <Card>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}> <Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<Button variant="outlined" startIcon={<CalendarTodayOutlinedIcon />} sx={{ color: 'text.secondary', borderColor: 'grey.300' }}> <FilterSummary count={ridersSummary.length} />
Jun 01 Jun 05
</Button>
<Box sx={{ flexGrow: 1 }} /> <Box sx={{ flexGrow: 1 }} />
</Stack> </Stack>

View File

@@ -20,7 +20,8 @@ import StatCard from '@/components/StatCard';
import StatusChip from '@/components/StatusChip'; import StatusChip from '@/components/StatusChip';
import UserAvatar from '@/components/UserAvatar'; import UserAvatar from '@/components/UserAvatar';
import TabLabelCount from '@/components/TabLabelCount'; import TabLabelCount from '@/components/TabLabelCount';
import { riders, riderLogs, locations } from '@/data/mock'; import { riders, riderLogs } from '@/data/mock';
import { useFilters } from '@/store/Filters';
import { inr } from '@/utils/format'; import { inr } from '@/utils/format';
const TABS = [ const TABS = [
@@ -32,7 +33,7 @@ export default function Riders() {
const navigate = useNavigate(); const navigate = useNavigate();
const [tab, setTab] = useState(0); const [tab, setTab] = useState(0);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [location, setLocation] = useState('all'); const { location } = useFilters(); // global location — single source of truth
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5); const [rpp, setRpp] = useState(5);
const [expanded, setExpanded] = useState(null); const [expanded, setExpanded] = useState(null);
@@ -72,10 +73,6 @@ export default function Riders() {
breadcrumbs={[{ label: 'Riders' }]} breadcrumbs={[{ label: 'Riders' }]}
action={ action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}> <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 170 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/riders/create')}> <Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/riders/create')}>
Add Rider Add Rider
</Button> </Button>

View File

@@ -1,186 +1,224 @@
import { useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Grid, Stack, Typography, Box, Avatar, Table, TableBody, TableCell, TableHead, TableRow, Button, Chip, TextField, InputAdornment, Divider } from '@mui/material'; import { Box, Stack, Typography, Button, TextField, InputAdornment } from '@mui/material';
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined';
import PhotoCameraOutlinedIcon from '@mui/icons-material/PhotoCameraOutlined';
import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined'; import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'; import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
import PageHeader from '@/components/PageHeader'; import TrackingControlBar from '@/components/tracking/TrackingControlBar';
import StatCard from '@/components/StatCard'; import ActiveRidersList from '@/components/tracking/ActiveRidersList';
import MainCard from '@/components/MainCard'; import FleetMap from '@/components/tracking/FleetMap';
import StatusChip from '@/components/StatusChip'; import RiderTimeline from '@/components/tracking/RiderTimeline';
import LayerBanner from '@/components/LayerBanner';
import MapPlaceholder from '@/components/MapPlaceholder';
import FormDialog from '@/components/FormDialog'; import FormDialog from '@/components/FormDialog';
import Toast, { useToast } from '@/components/Toast'; import Toast, { useToast } from '@/components/Toast';
import { executionStages, executionFeed, ridersLive, orderTimeline } from '@/data/mock'; import { activeDeliveries, fleetVehicles, riders } from '@/data/mock';
import { snapRoutes } from '@/utils/osrm';
import { useOps } from '@/store/OpsStore';
const now = () => new Date().toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true });
// Heavy children only re-render when their own props change (list runs at ~1fps, map at ~30fps).
const MemoFleetMap = memo(FleetMap);
const MemoActiveRiders = memo(ActiveRidersList);
const smoothstep = (x) => { const t = Math.max(0, Math.min(1, x)); return t * t * (3 - 2 * t); };
const STOP_WINDOW = 0.08; // how far ahead a vehicle starts braking for a stop (route fraction)
// Speed envelope (0..1): eases away from the pickup, into the drop, and dips toward each
// in-route stop (signals / waypoints) so motion reads as real traffic rather than a constant glide.
function speedEnvelope(p, stops) {
let e = 0.22 + 0.78 * smoothstep(p / 0.12) * smoothstep((1 - p) / 0.12);
for (let i = 0; i < stops.length; i += 1) {
e *= 0.12 + 0.88 * smoothstep(Math.abs(p - stops[i]) / STOP_WINDOW);
}
return e;
}
// Per-vehicle motion character, derived deterministically from the shipment id so every
// vehicle has its own cruising speed, traffic rhythm, stops and dwell — no two move identically.
const MOTION = activeDeliveries.reduce((acc, d) => {
let h = 0;
for (let i = 0; i < d.id.length; i += 1) h = (h * 31 + d.id.charCodeAt(i)) >>> 0;
const r = (h % 1000) / 1000;
const r2 = ((h >>> 10) % 1000) / 1000;
const r3 = ((h >>> 20) % 1000) / 1000;
acc[d.id] = {
speed: 0.5 + r * 0.65, // base cruising %/s
freq: 0.14 + r2 * 0.16, // traffic-rhythm frequency
phase: r * Math.PI * 2,
stops: [0.28 + r2 * 0.1, 0.62 + r3 * 0.12], // in-route halts (signals / drops)
dwell: [1.2 + r * 1.8, 1.0 + r3 * 1.8] // seconds paused at each halt
};
return acc;
}, {});
// Build the live delivery view-model from a progress map (shared by the map & queue lanes).
function buildDeliveries(progress, assignments, exceptions, snapped) {
return activeDeliveries.map((d) => {
const a = assignments[d.id];
const ex = exceptions[d.id];
const pe = progress[d.id] ?? d.progress;
return {
...d,
route: snapped[d.id] || d.route,
progress: Math.round(pe),
progressExact: pe,
rider: a ? a.riderName : d.rider,
status: ex ? 'Exception' : d.status,
etaStatus: ex ? 'delayed' : d.etaStatus,
flagged: Boolean(ex)
};
});
}
export default function LiveTracking() { export default function LiveTracking() {
const navigate = useNavigate(); const navigate = useNavigate();
const [track, setTrack] = useState(null);
const [share, setShare] = useState(false); const [share, setShare] = useState(false);
const [toast, showToast] = useToast(); const [toast, showToast] = useToast();
const trackLink = track ? `https://track.doormile.com/${track.id}` : 'https://track.doormile.com'; const [selectedId, setSelectedId] = useState(null);
const { assignments, exceptions, assignOrder, rerouteOrder, raiseException } = useOps();
const availableRiders = useMemo(() => riders.filter((r) => r.status !== 'offline'), []);
// snap every shipment's route to real streets via OSRM (falls back to the drawn path on failure)
const [snapped, setSnapped] = useState({});
useEffect(() => {
const ctrl = new AbortController();
snapRoutes(activeDeliveries, { signal: ctrl.signal }).then(setSnapped).catch(() => {});
return () => ctrl.abort();
}, []);
// live simulation — advance in-flight shipments along their routes with natural, eased motion.
// Two cadences: `progress` drives the map at ~30fps; `queueProgress` refreshes the list ~1×/s.
const [progress, setProgress] = useState(() => Object.fromEntries(activeDeliveries.map((d) => [d.id, d.progress])));
const [queueProgress, setQueueProgress] = useState(progress);
const [updated, setUpdated] = useState(now);
const baseProgress = useRef(Object.fromEntries(activeDeliveries.map((d) => [d.id, d.progress])));
useEffect(() => {
const work = { ...baseProgress.current }; // local accumulator advanced every frame
const runtime = {}; // per-vehicle dwell state (stop timer + next-stop pointer)
let raf;
let last = performance.now();
let elapsed = 0;
let sinceMap = 0;
let sinceQueue = 0;
let sinceClock = 0;
const step = (t) => {
const dt = Math.min((t - last) / 1000, 0.05);
last = t;
elapsed += dt;
sinceMap += dt;
sinceQueue += dt;
sinceClock += dt;
activeDeliveries.forEach((d) => {
if (d.status === 'Delivered') return;
const m = MOTION[d.id];
const rt = runtime[d.id] || (runtime[d.id] = { dwell: 0, stopIdx: 0 });
const cur = work[d.id] ?? d.progress;
if (rt.dwell > 0) { rt.dwell -= dt; return; } // paused at a halt — hold position
const p = cur / 100;
const traffic = 0.82 + 0.18 * Math.sin(elapsed * m.freq + m.phase); // gentle ±18% breathing
const v = m.speed * traffic * speedEnvelope(p, m.stops); // %/s with accel/decel + stop braking
let next = cur + v * dt;
// arrive at the next scheduled halt → clamp and start its dwell
if (rt.stopIdx < m.stops.length && next / 100 >= m.stops[rt.stopIdx]) {
next = m.stops[rt.stopIdx] * 100;
rt.dwell = m.dwell[rt.stopIdx];
rt.stopIdx += 1;
}
work[d.id] = d.status === 'Exception'
? Math.min(next, Math.min(baseProgress.current[d.id] + 6, 92))
: Math.min(next, 100); // arrive and hold — vehicles never teleport back
});
if (sinceMap >= 0.033) { sinceMap = 0; setProgress({ ...work }); }
if (sinceQueue >= 1) { sinceQueue = 0; setQueueProgress({ ...work }); }
if (sinceClock >= 1) { sinceClock = 0; setUpdated(now()); }
raf = requestAnimationFrame(step);
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, []);
// map lane (fast) and queue lane (slow) view-models — each recomputed only when its inputs change
const mapDeliveries = useMemo(() => buildDeliveries(progress, assignments, exceptions, snapped), [progress, assignments, exceptions, snapped]);
const queueDeliveries = useMemo(() => buildDeliveries(queueProgress, assignments, exceptions, snapped), [queueProgress, assignments, exceptions, snapped]);
// operator actions — every one commits to OpsStore
const actions = useMemo(
() => ({
reassign: (id, rider) => { assignOrder(id, rider); showToast(`${id} reassigned to ${rider.name}`); },
reroute: (id) => { rerouteOrder(id); showToast(`${id} rerouted · ETA protected`); },
call: (d) => showToast(`Calling ${d.rider}`),
message: (d) => showToast(`Message sent to ${d.rider}`),
flag: (id) => { raiseException(id, 'Flagged from control tower'); showToast(`${id} flagged as exception`, 'warning'); },
open: (id) => navigate(`/orders/${id}`)
}),
[assignOrder, rerouteOrder, raiseException, showToast, navigate]
);
const toggleSelect = useCallback((id) => setSelectedId((cur) => (cur === id ? null : id)), []);
const selectedDelivery = useMemo(() => queueDeliveries.find((d) => d.id === selectedId) || null, [queueDeliveries, selectedId]);
const tracking = Boolean(selectedId); // Selected Tracking Mode
const trackLink = selectedId ? `https://track.doormile.com/${selectedId}` : 'https://track.doormile.com';
const copyLink = () => { const copyLink = () => {
if (navigator.clipboard) navigator.clipboard.writeText(trackLink).catch(() => {}); if (navigator.clipboard) navigator.clipboard.writeText(trackLink).catch(() => {});
showToast('Tracking link copied'); showToast('Tracking link copied');
}; };
return ( return (
<> <Box sx={{ display: 'flex', flexDirection: 'column', height: { md: 'calc(100vh - 112px)' } }}>
<PageHeader {/* compact header */}
title="Execution & Visibility Layer" <Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={1} sx={{ mb: 1.5, flexShrink: 0 }}>
breadcrumbs={[{ label: 'Live Tracking' }]} <Box>
action={<Button variant="outlined" startIcon={<ShareOutlinedIcon />} onClick={() => setShare(true)}>Share Tracking</Button>} <Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.800', lineHeight: 1.1 }}>Live Tracking</Typography>
/> <Typography variant="caption" color="text.secondary">Fleet control tower · real-time operations</Typography>
</Box>
<Stack direction="row" spacing={1}>
<Button size="small" variant="text" startIcon={<AltRouteOutlinedIcon />} onClick={() => navigate('/tracking/journey')}>Trip Journey</Button>
<Button size="small" variant="text" startIcon={<AcUnitOutlinedIcon />} onClick={() => navigate('/cold-chain')}>Cold Chain</Button>
<Button size="small" variant="outlined" startIcon={<ShareOutlinedIcon />} onClick={() => setShare(true)}>Share</Button>
</Stack>
</Stack>
<LayerBanner <TrackingControlBar />
no={6}
icon={MyLocationOutlinedIcon}
color="#1D4ED8"
title="Execution & Visibility"
subtitle="Live GPS, photo proof of pickup & delivery, dynamic re-routing and exception handling."
steps={['Pickup Execution', 'In-Transit Tracking', 'Dynamic Re-routing', 'Delivery Execution', 'Exception Handling']}
/>
<Grid container spacing={2.5}> {/* control tower → Selected Tracking Mode: queue collapses, map expands, feed → rider timeline */}
{executionStages.map((s) => ( <Box sx={{ flexGrow: 1, minHeight: 0, display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 2 }}>
<Grid item xs={6} md={2.4} key={s.key}> {/* map — the primary surface; every rider marker stays visible & clickable */}
<Box sx={{ bgcolor: 'background.paper', border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 2, height: '100%' }}> <Box sx={{ flexGrow: 1, minWidth: 0, minHeight: { xs: 460, md: 0 } }}>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>{s.count.toLocaleString('en-IN')}</Typography> <MemoFleetMap
<Typography variant="subtitle2" sx={{ mt: 0.25 }}>{s.title}</Typography> vehicles={fleetVehicles}
<Stack spacing={0.25} sx={{ mt: 1 }}> deliveries={mapDeliveries}
{s.items.slice(0, 3).map((it) => ( selectedId={selectedId}
<Typography key={it} variant="caption" color="text.secondary"> {it}</Typography> onSelect={setSelectedId}
))} lastUpdated={updated}
</Stack> actions={actions}
riders={availableRiders}
/>
</Box>
{/* right rail — Active Riders roster (top) + Selected Rider timeline (bottom) */}
<Box sx={{ width: { xs: '100%', md: 340, lg: 376 }, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 2, minHeight: { xs: 'auto', md: 0 } }}>
<Box sx={{ flex: tracking ? '1 1 42%' : 1, minHeight: { xs: 340, md: 0 } }}>
<MemoActiveRiders deliveries={queueDeliveries} selectedId={selectedId} onSelect={setSelectedId} />
</Box>
{tracking && (
<Box sx={{ flex: '1 1 58%', minHeight: { xs: 380, md: 0 } }}>
<RiderTimeline delivery={selectedDelivery} onClose={() => setSelectedId(null)} />
</Box> </Box>
</Grid> )}
))} </Box>
</Box>
<Grid item xs={12} lg={7}>
<MainCard title="Live Fleet Map">
<MapPlaceholder
height={380}
label="Live Tracking"
riders={ridersLive.slice(0, 4).map((r, i) => ({
x: ['28%', '52%', '70%', '40%'][i],
y: ['44%', '30%', '58%', '66%'][i],
active: r.active
}))}
/>
</MainCard>
</Grid>
<Grid item xs={12} lg={5}>
<MainCard title="Live Execution Feed" contentSx={{ p: 0 }}>
<Stack>
{executionFeed.map((e, i) => (
<Box key={e.id} sx={{ p: 2, borderTop: i === 0 ? 'none' : '1px solid', borderColor: 'grey.100' }}>
<Stack direction="row" spacing={1.5} alignItems="flex-start">
<Avatar variant="rounded" sx={{ width: 38, height: 38, ...stageStyle(e.stage) }}>
{stageIcon(e.stage)}
</Avatar>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="subtitle2" sx={{ color: 'primary.main' }}>{e.id}</Typography>
<Typography variant="caption" color="text.secondary">{e.time}</Typography>
</Stack>
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ my: 0.25 }}>
<StatusChip status={mapStage(e.stage)} label={e.stage} />
{e.proof && <Chip size="small" icon={<PhotoCameraOutlinedIcon sx={{ fontSize: 14 }} />} label="Proof" sx={{ bgcolor: 'grey.100' }} />}
</Stack>
<Typography variant="caption" color="text.secondary">{e.rider} · {e.loc}</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>{e.detail}</Typography>
</Box>
</Stack>
</Box>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12}>
<MainCard title="Customer & Business Visibility" noPadding>
<Table>
<TableHead>
<TableRow>
<TableCell>Shipment</TableCell>
<TableCell>Rider</TableCell>
<TableCell>Current Stage</TableCell>
<TableCell>Location</TableCell>
<TableCell>Update</TableCell>
<TableCell align="right">Tracking</TableCell>
</TableRow>
</TableHead>
<TableBody>
{executionFeed.map((e) => (
<TableRow key={e.id} hover>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{e.id}</TableCell>
<TableCell>{e.rider}</TableCell>
<TableCell><StatusChip status={mapStage(e.stage)} label={e.stage} /></TableCell>
<TableCell>{e.loc}</TableCell>
<TableCell><Typography variant="caption" color="text.secondary">{e.detail}</Typography></TableCell>
<TableCell align="right">
<Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => setTrack(e)}>Track</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</MainCard>
</Grid>
</Grid>
{/* Tracking detail */}
<FormDialog
open={Boolean(track)}
onClose={() => setTrack(null)}
title={track ? `Tracking · ${track.id}` : 'Tracking'}
hideActions
>
{track && (
<Stack spacing={2}>
<Stack direction="row" spacing={1} alignItems="center">
<StatusChip status={mapStage(track.stage)} label={track.stage} />
<Typography variant="caption" color="text.secondary">Updated {track.time}</Typography>
</Stack>
<Stack direction="row" spacing={3}>
<Box><Typography variant="caption" color="text.secondary">Rider</Typography><Typography variant="subtitle2">{track.rider}</Typography></Box>
<Box><Typography variant="caption" color="text.secondary">Location</Typography><Typography variant="subtitle2">{track.loc}</Typography></Box>
</Stack>
<Typography variant="body2" color="text.secondary">{track.detail}</Typography>
<MapPlaceholder height={180} label={track.id} />
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Journey</Typography>
<Stack spacing={0}>
{orderTimeline.map((t, i) => (
<Stack key={t.label} direction="row" spacing={1.5} alignItems="center" sx={{ py: 0.75 }}>
{t.done ? <CheckCircleIcon sx={{ color: 'success.main', fontSize: 20 }} /> : <RadioButtonUncheckedIcon sx={{ color: 'grey.400', fontSize: 20 }} />}
<Typography variant="body2" sx={{ flexGrow: 1, fontWeight: t.done ? 600 : 400, color: t.done ? 'text.primary' : 'text.secondary' }}>{t.label}</Typography>
<Typography variant="caption" color="text.secondary">{t.time}</Typography>
</Stack>
))}
</Stack>
</Box>
<Divider />
<Stack direction="row" spacing={1.5} justifyContent="flex-end">
<Button startIcon={<ShareOutlinedIcon />} onClick={() => { setShare(true); }}>Share</Button>
<Button variant="contained" startIcon={<AltRouteOutlinedIcon />} onClick={() => navigate('/tracking/journey')}>View full journey</Button>
</Stack>
</Stack>
)}
</FormDialog>
{/* Share tracking */} {/* Share tracking */}
<FormDialog open={share} onClose={() => setShare(false)} title="Share Live Tracking" hideActions> <FormDialog open={share} onClose={() => setShare(false)} title="Share Live Tracking" hideActions>
@@ -207,28 +245,6 @@ export default function LiveTracking() {
</FormDialog> </FormDialog>
<Toast {...toast} /> <Toast {...toast} />
</> </Box>
); );
} }
const mapStage = (stage) => {
const k = { 'In-Transit': 'in-transit', 'Picked Up': 'picked-up', Delivered: 'delivered', Exception: 'exception', Dispatched: 'dispatched' };
return k[stage] || 'active';
};
function stageStyle(stage) {
const m = {
'In-Transit': { bgcolor: 'info.lighter', color: 'info.main' },
'Picked Up': { bgcolor: 'primary.lighter', color: 'primary.main' },
Delivered: { bgcolor: 'success.lighter', color: 'success.main' },
Exception: { bgcolor: 'error.lighter', color: 'error.main' },
Dispatched: { bgcolor: 'grey.100', color: 'grey.700' }
};
return m[stage] || m.Dispatched;
}
function stageIcon(stage) {
if (stage === 'Exception') return <WarningAmberOutlinedIcon fontSize="small" />;
if (stage === 'In-Transit' || stage === 'Dispatched') return <AltRouteOutlinedIcon fontSize="small" />;
return <PhotoCameraOutlinedIcon fontSize="small" />;
}

View File

@@ -1,136 +1,179 @@
import { Grid, Stack, Typography, Box, Avatar, Button, Chip, LinearProgress, Divider } from '@mui/material'; import { useNavigate } from 'react-router-dom';
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined'; import {
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; Grid, Stack, Typography, Box, Avatar, Button, Chip, LinearProgress, Divider,
import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined'; Table, TableBody, TableCell, TableHead, TableRow
} from '@mui/material';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined'; import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined'; import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined'; import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import PersonOutlineRoundedIcon from '@mui/icons-material/PersonOutlineRounded';
import OpenInNewRoundedIcon from '@mui/icons-material/OpenInNewRounded';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import MainCard from '@/components/MainCard'; import MainCard from '@/components/MainCard';
import MapPlaceholder from '@/components/MapPlaceholder'; import MiniMap from '@/components/tracking/MiniMap';
import Toast, { useToast } from '@/components/Toast'; import Toast, { useToast } from '@/components/Toast';
import { shipmentJourney as j } from '@/data/mock'; import { trip, cityCenters } from '@/data/mock';
const HOP_ICONS = { order: ReceiptLongOutlinedIcon, agent: TwoWheelerOutlinedIcon, hub: WarehouseOutlinedIcon, truck: LocalShippingOutlinedIcon, done: HomeOutlinedIcon }; const FROM = cityCenters[trip.from.city] || cityCenters.Coimbatore;
const TO = cityCenters[trip.to.city] || cityCenters.Bengaluru;
const ROUTE = [[FROM.lat, FROM.lng], [TO.lat, TO.lng]];
function Meta({ label, value, sub }) {
return (
<Box>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.25 }}>{value}</Typography>
{sub && <Typography variant="caption" color="text.secondary">{sub}</Typography>}
</Box>
);
}
export default function ShipmentJourney() { export default function ShipmentJourney() {
const navigate = useNavigate();
const [toast, showToast] = useToast(); const [toast, showToast] = useToast();
const doneCount = j.hops.filter((h) => h.status === 'done').length; const doneCount = trip.milestones.filter((m) => m.status === 'done').length;
return ( return (
<> <>
<PageHeader <PageHeader
title="Shipment Journey" title="Trip Journey"
breadcrumbs={[{ label: 'Live Tracking', to: '/tracking' }, { label: j.id }]} breadcrumbs={[{ label: 'Live Tracking', to: '/tracking' }, { label: trip.id }]}
action={ action={
<Stack direction="row" spacing={1.5}> <Stack direction="row" spacing={1.5}>
<Button variant="outlined" startIcon={<AltRouteOutlinedIcon />} onClick={() => showToast('MileTruth AI re-evaluated the route — ETA protected')}>Re-optimize</Button> <Button variant="outlined" startIcon={<AltRouteOutlinedIcon />} onClick={() => showToast('MileTruth AI re-evaluated the corridor — ETA protected')}>Re-optimize</Button>
<Button variant="contained" startIcon={<ShareOutlinedIcon />} onClick={() => showToast('Tracking link shared with customer')}>Share</Button> <Button variant="contained" startIcon={<ShareOutlinedIcon />} onClick={() => showToast('Trip tracking link shared')}>Share</Button>
</Stack> </Stack>
} }
/> />
{/* Summary */} {/* Trip summary — one vehicle, one corridor, one manifest */}
<Box sx={{ borderRadius: 2, border: '1px solid', borderColor: 'grey.200', bgcolor: 'background.paper', p: { xs: 2, md: 2.5 }, mb: 2.5 }}> <Box sx={{ borderRadius: 2, border: '1px solid', borderColor: 'grey.200', bgcolor: 'background.paper', p: { xs: 2, md: 2.5 }, mb: 2.5 }}>
<Grid container spacing={2} alignItems="center"> <Grid container spacing={2} alignItems="center">
<Grid item xs={12} md={5}> <Grid item xs={12} md={4}>
<Typography variant="overline" color="text.secondary">{j.id} · {j.client}</Typography> <Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}>
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mt: 0.5 }}> <Typography variant="overline" color="text.secondary">{trip.id}</Typography>
<Chip size="small" label={trip.status} sx={{ height: 20, bgcolor: 'info.lighter', color: 'info.dark', fontWeight: 700 }} />
</Stack>
<Stack direction="row" spacing={1.5} alignItems="center">
<Box> <Box>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{j.from.city}</Typography> <Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{trip.from.city}</Typography>
<Typography variant="caption" color="text.secondary">{j.from.area}</Typography> <Typography variant="caption" color="text.secondary">{trip.from.hub}</Typography>
</Box> </Box>
<Box sx={{ flexGrow: 1, height: 2, bgcolor: 'grey.200', position: 'relative', mx: 1, maxWidth: 90 }}> <Box sx={{ flexGrow: 1, height: 2, bgcolor: 'grey.200', position: 'relative', mx: 1, maxWidth: 90 }}>
<LocalShippingOutlinedIcon sx={{ fontSize: 18, color: 'primary.main', position: 'absolute', top: -9, left: `${j.progress}%`, transform: 'translateX(-50%)' }} /> <LocalShippingOutlinedIcon sx={{ fontSize: 18, color: 'primary.main', position: 'absolute', top: -9, left: `${trip.progress}%`, transform: 'translateX(-50%)' }} />
</Box> </Box>
<Box> <Box>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{j.to.city}</Typography> <Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{trip.to.city}</Typography>
<Typography variant="caption" color="text.secondary">{j.to.area}</Typography> <Typography variant="caption" color="text.secondary">{trip.to.hub}</Typography>
</Box> </Box>
</Stack> </Stack>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>{j.product} · {j.mode}</Typography>
</Grid> </Grid>
<Grid item xs={6} md={2}><Summary label="Current stage" value={j.currentStage} small /></Grid> <Grid item xs={6} md={2}><Meta label="Vehicle" value={trip.vehicle.reg} sub={`${trip.vehicle.type} · ${trip.vehicle.battery}%`} /></Grid>
<Grid item xs={6} md={2}><Summary label="ETA" value={j.eta} /></Grid> <Grid item xs={6} md={2}><Meta label="Driver" value={trip.driver.name} sub={trip.driver.phone} /></Grid>
<Grid item xs={6} md={1.5}><Summary label="Distance" value={`${j.distance} km`} /></Grid> <Grid item xs={4} md={1.3}><Meta label="Load" value={`${trip.loadKg} kg`} sub={`${trip.pieces} pcs`} /></Grid>
<Grid item xs={6} md={1.5}> <Grid item xs={4} md={1.2}><Meta label="ETA" value={trip.eta.split(', ')[1]} sub={trip.eta.split(', ')[0]} /></Grid>
<Grid item xs={4} md={1.3}>
<Typography variant="caption" color="text.secondary">Progress</Typography> <Typography variant="caption" color="text.secondary">Progress</Typography>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{j.progress}%</Typography> <Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{trip.progress}%</Typography>
<LinearProgress variant="determinate" value={j.progress} color="primary" sx={{ height: 5, borderRadius: 3, mt: 0.5 }} /> <LinearProgress variant="determinate" value={trip.progress} color="primary" sx={{ height: 5, borderRadius: 3, mt: 0.5 }} />
</Grid> </Grid>
</Grid> </Grid>
</Box> </Box>
<Grid container spacing={2.5}> <Grid container spacing={2.5}>
{/* Hop-by-hop timeline */} {/* Manifest — every shipment on this trip (the scalable centerpiece) */}
<Grid item xs={12} lg={7}> <Grid item xs={12} lg={7}>
<MainCard title={`Journey · ${doneCount}/${j.hops.length} stages complete`}> <MainCard
<Box> title={
{j.hops.map((h, i) => { <Stack direction="row" spacing={1} alignItems="center">
const Icon = HOP_ICONS[h.icon] || WarehouseOutlinedIcon; <Inventory2OutlinedIcon fontSize="small" sx={{ color: 'grey.600' }} />
const last = i === j.hops.length - 1; <Typography variant="h5">Manifest</Typography>
const newMile = i === 0 || j.hops[i - 1].mile !== h.mile; <Chip size="small" label={`${trip.manifest.length} shipments`} sx={{ height: 20, bgcolor: 'grey.100', fontWeight: 700 }} />
return ( </Stack>
<Box key={h.key}> }
{newMile && ( action={<Typography variant="caption" color="text.secondary">{trip.pieces} pieces · {trip.loadKg} kg</Typography>}
<Typography variant="overline" color="text.secondary" sx={{ display: 'block', mt: i === 0 ? 0 : 1.5, mb: 0.5, letterSpacing: '0.1em' }}> noPadding
{h.mile} >
</Typography> <Box sx={{ maxHeight: 460, overflowY: 'auto' }}>
)} <Table size="small" stickyHeader sx={{ '& .MuiTableRow-root:hover': { backgroundColor: 'grey.50' }, '& .MuiTableCell-head': { bgcolor: 'grey.50' } }}>
<Stack direction="row" spacing={1.75}> <TableHead>
{/* rail */} <TableRow>
<Stack alignItems="center" sx={{ width: 36 }}> <TableCell>Shipment</TableCell>
<Avatar <TableCell>Customer</TableCell>
sx={{ <TableCell>Drop Area</TableCell>
width: 34, <TableCell align="right">Pcs</TableCell>
height: 34, <TableCell align="right">Weight</TableCell>
bgcolor: h.status === 'done' ? 'success.main' : h.status === 'active' ? 'primary.main' : 'grey.100', <TableCell>Status</TableCell>
color: h.status === 'pending' ? 'grey.500' : '#fff', <TableCell align="right" />
border: h.status === 'pending' ? '1px solid' : 'none', </TableRow>
borderColor: 'grey.300' </TableHead>
}} <TableBody>
> {trip.manifest.map((s) => (
{h.status === 'done' ? <CheckIcon sx={{ fontSize: 18 }} /> : <Icon sx={{ fontSize: 18 }} />} <TableRow key={s.id} hover sx={{ cursor: 'pointer' }} onClick={() => navigate(`/orders/${s.id}`)}>
</Avatar> <TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{s.id}</TableCell>
{!last && <Box sx={{ flexGrow: 1, width: 2, minHeight: 26, bgcolor: h.status === 'done' ? 'success.light' : 'grey.200', my: 0.25 }} />} <TableCell>
</Stack> <Typography variant="caption" sx={{ fontWeight: 600 }}>{s.customer}</Typography>
{/* content */} <Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{s.client}</Typography>
<Box sx={{ pb: last ? 0 : 2, flexGrow: 1, minWidth: 0 }}> </TableCell>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}> <TableCell><Typography variant="caption" color="text.secondary">{s.dropArea}</Typography></TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: h.status === 'pending' ? 'text.secondary' : 'grey.900' }}>{h.title}</Typography> <TableCell align="right">{s.pieces}</TableCell>
{h.status === 'active' && <Chip size="small" label="In progress" sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }} />} <TableCell align="right"><Typography variant="caption">{s.weightKg} kg</Typography></TableCell>
</Stack> <TableCell><Chip size="small" label={s.status} sx={{ height: 20, bgcolor: 'info.lighter', color: 'info.dark', fontWeight: 600 }} /></TableCell>
<Typography variant="caption" sx={{ display: 'block', color: 'grey.800', fontWeight: 600 }}>{h.node}</Typography> <TableCell align="right"><OpenInNewRoundedIcon sx={{ fontSize: 15, color: 'grey.400' }} /></TableCell>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{h.handler}</Typography> </TableRow>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25 }}>{h.detail}</Typography> ))}
<Typography variant="caption" sx={{ color: h.status === 'pending' ? 'grey.400' : 'text.secondary', fontStyle: h.status === 'pending' ? 'italic' : 'normal' }}>{h.time}</Typography> </TableBody>
</Box> </Table>
</Stack>
</Box>
);
})}
</Box> </Box>
</MainCard> </MainCard>
</Grid> </Grid>
{/* Map + live monitoring */} {/* Map + trip milestones + monitoring — all describe the TRIP */}
<Grid item xs={12} lg={5}> <Grid item xs={12} lg={5}>
<Stack spacing={2.5}> <Stack spacing={2.5}>
<MainCard title="Live Route"> <MainCard title="Trip Route" noPadding>
<MapPlaceholder <Box sx={{ p: 1.5 }}>
height={240} <MiniMap route={ROUTE} progress={trip.progress} vehicle="Truck" pickup={FROM} drop={TO} height={220} />
label={`${j.from.city}${j.to.city}`} </Box>
pins={[ </MainCard>
{ x: '16%', y: '74%', label: 'Chennai', color: '#00A854' },
{ x: '48%', y: '50%', label: 'Line-haul', color: '#C01227' }, <MainCard title={`Trip Milestones · ${doneCount}/${trip.milestones.length}`}>
{ x: '80%', y: '24%', label: 'Bengaluru', color: '#595959' } <Box>
]} {trip.milestones.map((m, i) => {
/> const last = i === trip.milestones.length - 1;
return (
<Stack key={m.key} direction="row" spacing={1.75}>
<Stack alignItems="center" sx={{ width: 32 }}>
<Avatar
sx={{
width: 30, height: 30,
bgcolor: m.status === 'done' ? 'success.main' : m.status === 'active' ? 'primary.main' : 'grey.100',
color: m.status === 'pending' ? 'grey.500' : '#fff',
border: m.status === 'pending' ? '1px solid' : 'none', borderColor: 'grey.300'
}}
>
{m.status === 'done' ? <CheckIcon sx={{ fontSize: 16 }} /> : <LocalShippingOutlinedIcon sx={{ fontSize: 16 }} />}
</Avatar>
{!last && <Box sx={{ flexGrow: 1, width: 2, minHeight: 22, bgcolor: m.status === 'done' ? 'success.light' : 'grey.200', my: 0.25 }} />}
</Stack>
<Box sx={{ pb: last ? 0 : 1.75, flexGrow: 1, minWidth: 0 }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: m.status === 'pending' ? 'text.secondary' : 'grey.900' }}>{m.title}</Typography>
{m.status === 'active' && <Chip size="small" label="In progress" sx={{ bgcolor: 'primary.lighter', color: 'primary.dark', fontWeight: 600 }} />}
</Stack>
<Typography variant="caption" sx={{ display: 'block', color: 'grey.800', fontWeight: 600 }}>{m.node}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{m.detail}</Typography>
<Typography variant="caption" sx={{ color: m.status === 'pending' ? 'grey.400' : 'text.secondary', fontStyle: m.status === 'pending' ? 'italic' : 'normal' }}>{m.time}</Typography>
</Box>
</Stack>
);
})}
</Box>
</MainCard> </MainCard>
<MainCard <MainCard
@@ -142,7 +185,7 @@ export default function ShipmentJourney() {
} }
> >
<Stack divider={<Divider />} spacing={0}> <Stack divider={<Divider />} spacing={0}>
{j.events.map((e, i) => ( {trip.events.map((e, i) => (
<Stack key={i} direction="row" spacing={1.5} alignItems="flex-start" sx={{ py: 1.25 }}> <Stack key={i} direction="row" spacing={1.5} alignItems="flex-start" sx={{ py: 1.25 }}>
<Avatar variant="rounded" sx={{ width: 32, height: 32, bgcolor: e.type === 'reroute' ? 'warning.lighter' : 'grey.100', color: e.type === 'reroute' ? 'warning.dark' : 'grey.600' }}> <Avatar variant="rounded" sx={{ width: 32, height: 32, bgcolor: e.type === 'reroute' ? 'warning.lighter' : 'grey.100', color: e.type === 'reroute' ? 'warning.dark' : 'grey.600' }}>
{e.type === 'reroute' ? <AltRouteOutlinedIcon sx={{ fontSize: 18 }} /> : <VisibilityOutlinedIcon sx={{ fontSize: 18 }} />} {e.type === 'reroute' ? <AltRouteOutlinedIcon sx={{ fontSize: 18 }} /> : <VisibilityOutlinedIcon sx={{ fontSize: 18 }} />}
@@ -163,12 +206,3 @@ export default function ShipmentJourney() {
</> </>
); );
} }
function Summary({ label, value, small }) {
return (
<Box>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant={small ? 'subtitle2' : 'h5'} sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.2 }}>{value}</Typography>
</Box>
);
}

75
src/store/Filters.jsx Normal file
View File

@@ -0,0 +1,75 @@
import { createContext, useContext, useState, useMemo, useEffect } from 'react';
import dayjs from 'dayjs';
// ==============================|| GLOBAL FILTERS — single source of truth ||============================== //
// One place for the application-wide Location and Date Range. The header Location selector and every
// report read/write these, so there is exactly one source of truth (no duplicate, conflicting filters).
// Persisted to localStorage so the selection survives refresh, tab switches and navigation.
//
// Presets are anchored to the dataset's reference day (not the wall clock) so they filter the demo
// data sensibly — e.g. Last 7 Days ≠ Last 30 Days, Today ≠ This Month.
export const DATA_TODAY = dayjs('2026-06-05');
const fmt = (d) => d.format('MMM DD');
export function buildRange(key) {
const t = DATA_TODAY;
switch (key) {
case 'today': return { key, label: 'Today', start: t.startOf('day'), end: t.endOf('day') };
case 'yesterday': { const y = t.subtract(1, 'day'); return { key, label: 'Yesterday', start: y.startOf('day'), end: y.endOf('day') }; }
case 'last7': return { key, label: 'Last 7 Days', start: t.subtract(6, 'day').startOf('day'), end: t.endOf('day') };
case 'last30': return { key, label: 'Last 30 Days', start: t.subtract(29, 'day').startOf('day'), end: t.endOf('day') };
case 'thisMonth': return { key, label: 'This Month', start: t.startOf('month'), end: t.endOf('month') };
case 'prevMonth': { const p = t.subtract(1, 'month'); return { key, label: 'Previous Month', start: p.startOf('month'), end: p.endOf('month') }; }
default: return { key: 'last30', label: 'Last 30 Days', start: t.subtract(29, 'day').startOf('day'), end: t.endOf('day') };
}
}
export function customRange(start, end) {
return { key: 'custom', label: `${fmt(start)} ${fmt(end)}`, start: start.startOf('day'), end: end.endOf('day') };
}
// inclusive, day-granular date-in-range test
export function inRange(dateStr, range) {
if (!range?.start || !range?.end) return true;
const d = dayjs(dateStr).startOf('day');
return !d.isBefore(range.start.startOf('day')) && !d.isAfter(range.end.startOf('day'));
}
const FilterContext = createContext(null);
const STORE_KEY = 'dm.filters';
export function FilterProvider({ children }) {
const [location, setLocation] = useState('all');
const [range, setRange] = useState(() => buildRange('last30'));
// hydrate once
useEffect(() => {
try {
const raw = localStorage.getItem(STORE_KEY);
if (!raw) return;
const s = JSON.parse(raw);
if (s.location) setLocation(s.location);
if (s.rangeKey === 'custom' && s.start && s.end) setRange(customRange(dayjs(s.start), dayjs(s.end)));
else if (s.rangeKey) setRange(buildRange(s.rangeKey));
} catch { /* ignore corrupt storage */ }
}, []);
// persist on change
useEffect(() => {
try {
localStorage.setItem(STORE_KEY, JSON.stringify({
location, rangeKey: range.key, start: range.start?.toISOString(), end: range.end?.toISOString()
}));
} catch { /* storage unavailable */ }
}, [location, range]);
const value = useMemo(() => ({ location, setLocation, range, setRange }), [location, range]);
return <FilterContext.Provider value={value}>{children}</FilterContext.Provider>;
}
export function useFilters() {
const ctx = useContext(FilterContext);
if (!ctx) throw new Error('useFilters must be used within a FilterProvider');
return ctx;
}

75
src/store/OpsStore.jsx Normal file
View File

@@ -0,0 +1,75 @@
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
// ==============================|| OPS STORE — SESSION PERSISTENCE ||============================== //
// A lightweight in-memory store so operational actions actually COMMIT during a session (no backend).
// Holds: dispatch assignments (order → rider), reroutes, and exceptions. Future workflows (status
// advance, payments) hang off the same provider. State resets on full reload — intentional for a demo.
const OpsContext = createContext(null);
export function OpsProvider({ children }) {
// assignments: { [orderId]: { riderId, riderName, vehicle, at } }
const [assignments, setAssignments] = useState({});
// reroutes: { [orderId]: at }
const [reroutes, setReroutes] = useState({});
// exceptions: { [orderId]: { reason, at } }
const [exceptions, setExceptions] = useState({});
const assignOrder = useCallback((orderId, rider) => {
setAssignments((prev) => ({
...prev,
[orderId]: { riderId: rider.id, riderName: rider.name, vehicle: rider.vehicle, at: Date.now() }
}));
}, []);
const unassignOrder = useCallback((orderId) => {
setAssignments((prev) => {
const next = { ...prev };
delete next[orderId];
return next;
});
}, []);
const rerouteOrder = useCallback((orderId) => {
setReroutes((prev) => ({ ...prev, [orderId]: Date.now() }));
}, []);
const raiseException = useCallback((orderId, reason) => {
setExceptions((prev) => ({ ...prev, [orderId]: { reason, at: Date.now() } }));
}, []);
const clearException = useCallback((orderId) => {
setExceptions((prev) => {
const next = { ...prev };
delete next[orderId];
return next;
});
}, []);
const assignmentOf = useCallback((orderId) => assignments[orderId] || null, [assignments]);
const exceptionOf = useCallback((orderId) => exceptions[orderId] || null, [exceptions]);
const isRerouted = useCallback((orderId) => Boolean(reroutes[orderId]), [reroutes]);
const riderLoad = useCallback(
(riderId) => Object.values(assignments).filter((a) => a.riderId === riderId).length,
[assignments]
);
const value = useMemo(
() => ({
assignments, reroutes, exceptions,
assignOrder, unassignOrder, rerouteOrder, raiseException, clearException,
assignmentOf, exceptionOf, isRerouted, riderLoad,
assignedCount: Object.keys(assignments).length,
exceptionCount: Object.keys(exceptions).length
}),
[assignments, reroutes, exceptions, assignOrder, unassignOrder, rerouteOrder, raiseException, clearException, assignmentOf, exceptionOf, isRerouted, riderLoad]
);
return <OpsContext.Provider value={value}>{children}</OpsContext.Provider>;
}
export function useOps() {
const ctx = useContext(OpsContext);
if (!ctx) throw new Error('useOps must be used within an OpsProvider');
return ctx;
}

49
src/utils/geo.js Normal file
View File

@@ -0,0 +1,49 @@
// ==============================|| GEO HELPERS — route interpolation & bearing ||============================== //
// Lightweight planar math (good enough for city-scale visualisation) used to place a vehicle
// along its route from a progress %, split a route into completed / remaining paths, and
// compute heading so markers can rotate to face their direction of travel.
const seg = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
export function bearing(a, b) {
const dLng = ((b[1] - a[1]) * Math.PI) / 180;
const lat1 = (a[0] * Math.PI) / 180;
const lat2 = (b[0] * Math.PI) / 180;
const y = Math.sin(dLng) * Math.cos(lat2);
const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLng);
return (((Math.atan2(y, x) * 180) / Math.PI) + 360) % 360;
}
// Returns { point: [lat,lng], bearing, completed: [...], remaining: [...] } for a progress 0..100.
export function pointAlongRoute(route, progress) {
if (!route || route.length === 0) return null;
if (route.length === 1) return { point: route[0], bearing: 0, completed: [route[0]], remaining: [route[0]] };
const lengths = [];
let total = 0;
for (let i = 0; i < route.length - 1; i++) {
const l = seg(route[i], route[i + 1]);
lengths.push(l);
total += l;
}
const target = Math.min(Math.max(progress, 0), 100) / 100 * total;
let acc = 0;
for (let i = 0; i < lengths.length; i++) {
if (acc + lengths[i] >= target || i === lengths.length - 1) {
const t = lengths[i] === 0 ? 0 : (target - acc) / lengths[i];
const a = route[i];
const b = route[i + 1];
const point = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
return {
point,
bearing: bearing(a, b),
completed: [...route.slice(0, i + 1), point],
remaining: [point, ...route.slice(i + 1)]
};
}
acc += lengths[i];
}
const last = route[route.length - 1];
return { point: last, bearing: 0, completed: route, remaining: [last] };
}

43
src/utils/osrm.js Normal file
View File

@@ -0,0 +1,43 @@
// ==============================|| OSRM ROUTING — snap routes to real streets ||============================== //
// Turns a list of [lat,lng] waypoints into road-following geometry via the OSRM route service.
// Results are cached in-memory; any failure (offline / rate-limit) returns null so callers can
// fall back to their hand-drawn polyline. Uses the public demo server — swap OSRM_HOST for a
// self-hosted instance in production (the demo server is not for production traffic).
const OSRM_HOST = 'https://router.project-osrm.org';
const cache = new Map();
const key = (waypoints) => waypoints.map((p) => p.join(',')).join(';');
export async function snapRoute(waypoints, { signal } = {}) {
if (!waypoints || waypoints.length < 2) return null;
const k = key(waypoints);
if (cache.has(k)) return cache.get(k);
const coords = waypoints.map(([lat, lng]) => `${lng},${lat}`).join(';');
const url = `${OSRM_HOST}/route/v1/driving/${coords}?overview=full&geometries=geojson`;
try {
const res = await fetch(url, { signal });
if (!res.ok) throw new Error(`OSRM ${res.status}`);
const data = await res.json();
if (data.code !== 'Ok' || !data.routes?.length) throw new Error(`OSRM ${data.code}`);
const line = data.routes[0].geometry.coordinates.map(([lng, lat]) => [lat, lng]);
cache.set(k, line);
return line;
} catch (err) {
if (err.name !== 'AbortError') cache.set(k, null); // remember the failure, don't hammer
return null;
}
}
// Snap many routes at once. Returns { [id]: [[lat,lng], …] } only for the ones that succeeded.
export async function snapRoutes(items, { signal } = {}) {
const entries = await Promise.all(
items.map(async (it) => {
const line = await snapRoute(it.route, { signal });
return line ? [it.id, line] : null;
})
);
return Object.fromEntries(entries.filter(Boolean));
}

1291
yarn.lock Normal file

File diff suppressed because it is too large Load Diff