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

@@ -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 LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined';
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 EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
import SpeedOutlinedIcon from '@mui/icons-material/SpeedOutlined';
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import KpiStrip from '@/components/KpiStrip';
import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip';
import AreaChart from '@/components/charts/AreaChart';
import DonutChart from '@/components/charts/DonutChart';
import UserAvatar from '@/components/UserAvatar';
import SystemPipeline from '@/components/SystemPipeline';
import ThreeMileStrip from '@/components/ThreeMileStrip';
import ProcessTracker from '@/components/ProcessTracker';
import AiImpactSummary from '@/components/AiImpactSummary';
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';
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() {
const navigate = useNavigate();
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 (
<>
<PageHeader
title="System Overview"
breadcrumbs={[{ label: 'Dashboard' }]}
action={
<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>
}
title="Operations Control Center"
breadcrumbs={[{ label: 'Control Center' }]}
action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('Snapshot exported as CSV')}>Export</Button>}
/>
{/* End-to-end operating-system pipeline */}
<Box sx={{ mb: 1 }}>
<Typography variant="overline" color="text.secondary" sx={{ letterSpacing: '0.08em' }}>
End-to-End Intelligent Logistics Flow
</Typography>
</Box>
<Box sx={{ mb: 3 }}>
<SystemPipeline />
</Box>
{/* Top row — 6 live KPIs */}
<KpiStrip items={kpis} />
{/* Three-Mile model — First → Mid → Last */}
<Box sx={{ mb: 0.5 }}>
<Typography variant="overline" color="text.secondary" sx={{ letterSpacing: '0.08em' }}>
Three-Mile Network · One Connected System
</Typography>
</Box>
<Box sx={{ mb: 3 }}>
<ThreeMileStrip compact />
</Box>
<Grid container spacing={2.5}>
<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>
<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>
{/* Second row — dispatch intelligence */}
<Box sx={{ mt: 1 }}>
<SectionLabel>Dispatch & Exceptions</SectionLabel>
<Grid container spacing={2.5}>
<Grid item xs={12} md={6} lg={3}>
<MainCard title="Live Dispatch Queue" action={<Button size="small" onClick={() => navigate('/orders/assign')} sx={{ fontWeight: 600 }}>Open</Button>} noPadding>
<Stack divider={<Divider />}>
{dispatchQueue.map((d) => (
<Box key={d.id} sx={{ px: 2, py: 1.25 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.main' }}>{d.id}</Typography>
<Chip size="small" label={`${d.confidence}%`} sx={{ height: 18, bgcolor: 'grey.100', fontWeight: 700, '& .MuiChip-label': { px: 0.75, fontSize: 10 } }} />
</Stack>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.900' }}>{v.shipments}</Typography>
<Typography variant="caption" color="text.secondary">{v.onTime}% on-time</Typography>
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block' }}>{d.pickup} {d.drop}</Typography>
<Typography variant="caption" sx={{ color: 'grey.500' }}>AI {d.suggestedRider} · SLA {d.sla}</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>
</Stack>
</Box>
))}
</Stack>
</MainCard>
))}
</Stack>
</MainCard>
</Grid>
</Grid>
</Box>
<Grid item xs={12} lg={8}>
<MainCard title="Recent Shipments" noPadding>
<Table>
<TableHead>
<TableRow>
<TableCell>Order ID</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Vertical</TableCell>
<TableCell>Route</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orders.slice(0, 6).map((o) => {
const v = verticalOf(o.tenant);
return (
<TableRow key={o.id} hover>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{o.id}</TableCell>
<TableCell>{o.customer}</TableCell>
<TableCell>
<Chip size="small" label={v} sx={{ bgcolor: hexA(VERTICAL_COLOR[v], 0.12), color: VERTICAL_COLOR[v], fontWeight: 600 }} />
</TableCell>
<TableCell>
<Typography variant="caption" color="text.secondary">{o.pickup} {o.drop}</Typography>
</TableCell>
<TableCell><StatusChip status={o.status} /></TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</MainCard>
{/* Third row — events / fleet / route efficiency */}
<Box sx={{ mt: 3.5 }}>
<SectionLabel>Operational Intelligence</SectionLabel>
<Grid container spacing={2.5}>
<Grid item xs={12} lg={4}>
<MainCard title="Recent Operational Events" noPadding>
<Stack divider={<Divider />}>
{executionFeed.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: e.stage === 'Exception' ? 'error.main' : e.stage === 'Delivered' ? 'success.main' : 'info.main', flexShrink: 0 }} />
<Box sx={{ minWidth: 0, flexGrow: 1 }}>
<Stack direction="row" justifyContent="space-between" spacing={1}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>{e.stage}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ flexShrink: 0 }}>{e.time}</Typography>
</Stack>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }} noWrap>{e.id} · {e.loc}</Typography>
</Box>
</Stack>
))}
</Stack>
</MainCard>
</Grid>
<Grid item xs={12} md={6} lg={4}>
<MainCard title="Fleet Status Overview">
<Grid container spacing={1.5} sx={{ mb: 1.5 }}>
{[['On trip', fleetSummary.onTrip, '#1D4ED8'], ['Charging', fleetSummary.charging, '#00A2AE'], ['Idle', fleetSummary.idle, '#8C8C8C'], ['Maintenance', fleetSummary.maintenance, '#F04134']].map(([l, v, c]) => (
<Grid item xs={6} key={l}>
<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>
</Box>
</Grid>
))}
</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>
</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} />
</>
);
}
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 }) {
return (
<Stack direction="row" spacing={0.75} alignItems="center">
@@ -228,8 +240,3 @@ function Legend({ color, label }) {
</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 UserAvatar from '@/components/UserAvatar';
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 DateRangeFilter from '@/components/DateRangeFilter';
import FilterSummary from '@/components/FilterSummary';
import { useFilters, inRange } from '@/store/Filters';
const TABS = [
{ key: 'assigned', label: 'Assigned' },
@@ -140,12 +143,11 @@ function DeliveryRow({ row, index }) {
}
export default function Deliveries() {
const { location, range } = useFilters(); // global location + date range
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [tenant, setTenant] = useState('all');
const [location, setLocation] = useState('all');
const [rider, setRider] = useState('all');
const [headerLocation, setHeaderLocation] = useState('all');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5);
@@ -170,13 +172,14 @@ export default function Deliveries() {
const matchTab = d.status === tabKey;
const matchTenant = tenant === 'all' || d.tenant === tenant;
const matchLocation = location === 'all' || d.location === location;
const matchDate = d.date ? inRange(d.date, range) : true;
const matchRider = rider === 'all' || d.rider === rider;
const matchSearch =
!search ||
[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);
@@ -186,15 +189,7 @@ export default function Deliveries() {
<PageHeader
title="Deliveries"
breadcrumbs={[{ label: 'Deliveries' }]}
action={
<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>
}
action={<DateRangeFilter />}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
@@ -206,17 +201,10 @@ export default function Deliveries() {
<Card sx={{ mt: 1.5 }}>
<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">
<MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</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">
<MenuItem value="all">All Riders</MenuItem>
{riders.map((r) => <MenuItem key={r.id} value={r.name}>{r.name}</MenuItem>)}
@@ -229,6 +217,10 @@ export default function Deliveries() {
/>
</Stack>
<Box sx={{ px: 2, pb: 1.5 }}>
<FilterSummary count={filtered.length} />
</Box>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
{TABS.map((t, i) => (

View File

@@ -1,206 +1,259 @@
import { useState } from 'react';
import {
Grid,
Tabs,
Tab,
Box,
Stack,
TextField,
MenuItem,
Switch,
FormControlLabel,
Button,
Typography,
Divider,
Snackbar,
Alert
Box, Stack, Typography, Card, Tabs, Tab, TextField, MenuItem, Switch, Button, Grid,
Snackbar, Alert
} from '@mui/material';
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
import RestartAltOutlinedIcon from '@mui/icons-material/RestartAltOutlined';
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 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 MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip';
const TIMEZONES = ['Asia/Kolkata (IST)', 'Asia/Dubai (GST)', 'UTC', 'America/New_York (EST)'];
const LANGUAGES = ['English', 'हिन्दी (Hindi)', 'العربية (Arabic)'];
const RETENTION = ['90 days', '180 days', '1 year', '3 years', 'Indefinite'];
function TabPanel({ value, index, children }) {
if (value !== index) return null;
return <Box sx={{ pt: 1 }}>{children}</Box>;
}
const TABS = [
{ key: 'general', label: 'General', icon: TuneOutlinedIcon },
{ 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 [tab, setTab] = useState(0);
const [toast, setToast] = useState(false);
// 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);
const INITIAL = {
general: { orgName: 'Doormile Logistics Pvt. Ltd.', supportEmail: 'support@doormile.in', contact: '+91 98450 11223' },
notif: { customer: true, rider: true, delay: true, admin: true },
security: { twoFactor: true, sessionTimeout: true }
};
// ---- building blocks ----
function SectionCard({ title, description, icon: Icon, action, children, sx }) {
return (
<>
<PageHeader
title="Settings"
breadcrumbs={[{ label: 'Settings' }]}
action={
<Button variant="contained" startIcon={<SaveOutlinedIcon />} onClick={save}>
Save Changes
</Button>
}
/>
<Grid container spacing={2.5}>
<Grid item xs={12} md={3}>
<MainCard noPadding>
<Tabs
orientation="vertical"
value={tab}
onChange={(_, v) => setTab(v)}
sx={{
'& .MuiTab-root': { alignItems: 'flex-start', textTransform: 'none', minHeight: 52, fontWeight: 600 }
}}
>
<Tab icon={<TuneOutlinedIcon fontSize="small" />} iconPosition="start" label="General" />
<Tab icon={<NotificationsNoneIcon fontSize="small" />} iconPosition="start" label="Notifications" />
<Tab icon={<LockOutlinedIcon fontSize="small" />} iconPosition="start" label="Security" />
</Tabs>
</MainCard>
</Grid>
<Grid item xs={12} md={9}>
{/* General */}
<TabPanel value={tab} index={0}>
<MainCard title="Organisation">
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<TextField fullWidth label="Organisation Name" value={general.orgName} onChange={setG('orgName')} />
</Grid>
<Grid item xs={12} sm={6}>
<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')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth label="Timezone" value={general.timezone} onChange={setG('timezone')}>
{TIMEZONES.map((t) => (
<MenuItem key={t} value={t}>{t}</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<TextField select fullWidth label="Language" value={general.language} onChange={setG('language')}>
{LANGUAGES.map((l) => (
<MenuItem key={l} value={l}>{l}</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</MainCard>
</TabPanel>
{/* Notifications */}
<TabPanel value={tab} index={1}>
<MainCard title="Notification Preferences">
<Stack divider={<Divider flexItem />} spacing={0}>
{[
{ k: 'newOrders', t: 'New orders', d: 'Notify when a new order is placed' },
{ k: 'riderStatus', t: 'Rider status', d: 'When a rider goes online or offline' },
{ k: 'invoicePaid', t: 'Invoice paid', d: 'When a client settles an invoice' },
{ k: 'weeklyDigest', t: 'Weekly digest', d: 'A summary of operations every Monday' }
].map((row) => (
<Stack key={row.k} direction="row" alignItems="center" justifyContent="space-between" sx={{ py: 1.5 }}>
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{row.t}</Typography>
<Typography variant="caption" color="text.secondary">{row.d}</Typography>
</Box>
<Switch checked={notify[row.k]} onChange={setN(row.k)} />
</Stack>
))}
</Stack>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1 }}>Channels</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<FormControlLabel control={<Switch checked={notify.emailAlerts} onChange={setN('emailAlerts')} />} label="Email alerts" />
<FormControlLabel control={<Switch checked={notify.smsAlerts} onChange={setN('smsAlerts')} />} label="SMS alerts" />
</Stack>
</MainCard>
</TabPanel>
{/* Security */}
<TabPanel value={tab} index={2}>
<Stack spacing={2.5}>
<MainCard title="Change Password">
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="Current Password" value={security.currentPassword} onChange={setS('currentPassword')} />
</Grid>
<Grid item xs={12} />
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="New Password" value={security.newPassword} onChange={setS('newPassword')} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField fullWidth type="password" label="Confirm New Password" value={security.confirmPassword} onChange={setS('confirmPassword')} />
</Grid>
</Grid>
</MainCard>
<MainCard title="Two-Factor Authentication">
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>Authenticator app</Typography>
<Typography variant="caption" color="text.secondary">
Require a one-time code at sign-in for extra security.
</Typography>
</Box>
<Switch checked={security.twoFactor} onChange={(e) => setSecurity((p) => ({ ...p, twoFactor: e.target.checked }))} />
</Stack>
</MainCard>
</Stack>
</TabPanel>
</Grid>
</Grid>
<Snackbar
open={toast}
autoHideDuration={2500}
onClose={() => setToast(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert severity="success" variant="filled" onClose={() => setToast(false)} sx={{ width: '100%' }}>
Settings saved successfully.
</Alert>
</Snackbar>
</>
<Card variant="outlined" sx={{ borderColor: 'grey.200', boxShadow: '0 1px 2px rgba(16,24,40,0.04)', height: '100%', ...sx }}>
<Box sx={{ p: 2.5 }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1.5} sx={{ mb: children ? 2.25 : 0 }}>
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ minWidth: 0 }}>
{Icon && (
<Box sx={{ width: 38, height: 38, borderRadius: 1.5, bgcolor: 'grey.100', color: 'grey.700', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
<Icon sx={{ fontSize: 20 }} />
</Box>
)}
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.25 }}>{title}</Typography>
{description && <Typography variant="caption" color="text.secondary">{description}</Typography>}
</Box>
</Stack>
{action && <Box sx={{ flexShrink: 0 }}>{action}</Box>}
</Stack>
{children}
</Box>
</Card>
);
}
function ToggleRow({ icon: Icon, title, desc, checked, onChange, divider }) {
return (
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={2} sx={{ py: 1.75, borderTop: divider ? '1px solid' : 'none', borderColor: 'grey.100' }}>
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ minWidth: 0 }}>
{Icon && (
<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 }} />
</Box>
)}
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{title}</Typography>
<Typography variant="caption" color="text.secondary">{desc}</Typography>
</Box>
</Stack>
<Switch checked={checked} onChange={onChange} />
</Stack>
);
}
const INTEGRATIONS = [
{ key: 'emailjs', name: 'EmailJS', desc: 'Transactional email delivery', icon: EmailOutlinedIcon, status: 'connected', sync: '2 min ago' },
{ key: 'whatsapp', name: 'WhatsApp', desc: 'Customer tracking updates', icon: WhatsAppIcon, status: 'connected', sync: '5 min ago' },
{ key: 'gmaps', name: 'Google Maps', desc: 'Geocoding & route tiles', icon: MapOutlinedIcon, status: 'connected', sync: '1 min ago' },
{ key: 'razorpay', name: 'Razorpay', desc: 'Payments & COD settlement', icon: PaymentsOutlinedIcon, status: 'degraded', sync: '38 min ago' },
{ key: 'webhooks', name: 'Webhooks', desc: 'Event push to partner systems', icon: WebhookIcon, status: 'pending', sync: 'Never' }
];
export default function Settings() {
const [tab, setTab] = useState('general');
const [toast, setToast] = useState('');
const [general, setGeneral] = useState(INITIAL.general);
const [notif, setNotif] = useState(INITIAL.notif);
const [security, setSecurity] = useState(INITIAL.security);
const setG = (k) => (e) => setGeneral((p) => ({ ...p, [k]: e.target.value }));
const setN = (k) => (e) => setNotif((p) => ({ ...p, [k]: e.target.checked }));
const setS = (k) => (e) => setSecurity((p) => ({ ...p, [k]: e.target.checked }));
const save = () => setToast('Settings saved successfully.');
const reset = () => {
setGeneral(INITIAL.general); setNotif(INITIAL.notif); setSecurity(INITIAL.security);
setToast('Changes reset to last saved state.');
};
return (
<Box>
{/* header */}
<Stack direction={{ xs: 'column', md: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', md: 'center' }} spacing={2} sx={{ mb: 2.5 }}>
<Box>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.1 }}>Settings</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5, maxWidth: 560 }}>
Manage organization preferences, notifications, integrations, and security settings.
</Typography>
</Box>
<Stack direction="row" spacing={1.5} sx={{ flexShrink: 0 }}>
<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>
{/* sticky tabs */}
<Box sx={{ position: 'sticky', top: 64, zIndex: 5, bgcolor: 'background.default', borderBottom: '1px solid', borderColor: 'grey.200', mb: 3 }}>
<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' } }}>
{TABS.map((t) => (
<Tab key={t.key} value={t.key} icon={<t.icon sx={{ fontSize: 18 }} />} iconPosition="start" label={t.label} />
))}
</Tabs>
</Box>
{/* ===================== GENERAL ===================== */}
{tab === 'general' && (
<Box sx={{ maxWidth: 1080 }}>
<SectionCard title="Organization Profile" description="Your company's core identity and contact details" icon={BusinessOutlinedIcon}>
<Grid container spacing={2.5}>
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Organization Name" value={general.orgName} onChange={setG('orgName')} /></Grid>
<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}><TextField fullWidth size="small" label="Contact Number" value={general.contact} onChange={setG('contact')} /></Grid>
</Grid>
</SectionCard>
</Box>
)}
{/* ===================== OPERATIONS ===================== */}
{tab === 'operations' && (
<Grid container spacing={2.5} sx={{ maxWidth: 1080 }}>
{/* Notification Controls */}
<Grid item xs={12} md={6}>
<SectionCard title="Notification Controls" description="Who gets alerted across the operation" icon={NotificationsNoneIcon}>
<Box>
<ToggleRow icon={SupportAgentOutlinedIcon} title="Customer Notifications" desc="Order, tracking link & delivery updates to customers" checked={notif.customer} onChange={setN('customer')} />
<ToggleRow icon={TwoWheelerOutlinedIcon} title="Rider Notifications" desc="Assignment, pickup & route changes to riders" checked={notif.rider} onChange={setN('rider')} divider />
<ToggleRow icon={WarningAmberOutlinedIcon} title="Delay Alerts" desc="Proactive alerts when a shipment trends late" checked={notif.delay} onChange={setN('delay')} divider />
<ToggleRow icon={CampaignOutlinedIcon} title="Admin Escalations" desc="Critical exceptions routed to ops managers" checked={notif.admin} onChange={setN('admin')} divider />
</Box>
</SectionCard>
</Grid>
</Grid>
)}
{/* ===================== SECURITY ===================== */}
{tab === 'security' && (
<Grid container spacing={2.5} sx={{ maxWidth: 1080 }}>
<Grid item xs={12} md={6}>
<SectionCard title="Two-Factor Authentication" description="Require a one-time code at sign-in" icon={ShieldOutlinedIcon} action={<Switch checked={security.twoFactor} onChange={setS('twoFactor')} />}>
<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.'}
</Typography>
</SectionCard>
</Grid>
<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 UserAvatar from '@/components/UserAvatar';
import { customers, locations } from '@/data/mock';
import { useFilters } from '@/store/Filters';
const EMPTY_FORM = {
name: '', phone: '', address: '', location: '', city: '', state: '',
@@ -26,7 +27,7 @@ const EMPTY_FORM = {
export default function Customers() {
const navigate = useNavigate();
const [search, setSearch] = useState('');
const [location, setLocation] = useState('all');
const { location } = useFilters(); // global location — single source of truth
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5);
const [editOpen, setEditOpen] = useState(false);
@@ -74,13 +75,6 @@ export default function Customers() {
breadcrumbs={[{ label: 'Customers' }]}
action={
<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
size="small" placeholder="Search customers…" value={search}
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 { 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 TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined';
import BatteryChargingFullOutlinedIcon from '@mui/icons-material/BatteryChargingFullOutlined';
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' };
export default function Fleet() {
const navigate = useNavigate();
const [rows, setRows] = useState(fleet);
const [open, setOpen] = useState(false);
const [form, setForm] = useState(BLANK);
@@ -56,7 +59,12 @@ export default function Fleet() {
<PageHeader
title="Fleet & Rider Operating System"
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
@@ -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 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>
</Grid>
</Grid>

View File

@@ -1,199 +1,192 @@
import { useState } from 'react';
import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Divider, TextField, MenuItem } from '@mui/material';
import { Grid, Card, Stack, Typography, Box, LinearProgress, Button, Divider } from '@mui/material';
import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined';
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
import AddLocationAltOutlinedIcon from '@mui/icons-material/AddLocationAltOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip';
import LayerBanner from '@/components/LayerBanner';
import MapPlaceholder from '@/components/MapPlaceholder';
import FormDialog from '@/components/FormDialog';
import AreaChart from '@/components/charts/AreaChart';
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() {
const [rows, setRows] = useState(hubs);
const [open, setOpen] = useState(false);
const [form, setForm] = useState(BLANK);
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 (
<>
<PageHeader
title="Hub & Network Orchestration"
breadcrumbs={[{ label: 'Hub Network' }]}
action={<Button variant="contained" startIcon={<AddLocationAltOutlinedIcon />} onClick={() => setOpen(true)}>Add Hub</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']}
title="Hub Operations Center"
breadcrumbs={[{ label: 'Network' }, { label: 'Hubs' }]}
action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('Hub report exported')}>Export</Button>}
/>
{/* Top — hub summary cards */}
<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>
<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 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>
{hubCityStats.map((h) => (
<Grid key={h.city} item xs={12} md={4}><HubCard h={h} /></Grid>
))}
</Grid>
<FormDialog open={open} onClose={() => setOpen(false)} title="Add Hub" onSubmit={addHub} submitLabel="Add Hub">
<Grid container spacing={2} sx={{ mt: 0 }}>
<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 item xs={12} sm={6}>
<TextField select fullWidth size="small" label="Type" value={form.type} onChange={set('type')}>
{['Micro Hub', 'City Hub', 'Regional Hub', 'Cross Dock'].map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</TextField>
{/* Middle — throughput trends + capacity comparison */}
<Box sx={{ mt: 3.5 }}>
<Grid container spacing={2.5}>
<Grid item xs={12} lg={8}>
<MainCard
title="Throughput Trends"
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 item xs={12} sm={6}>
<TextField select fullWidth size="small" label="City" value={form.city} onChange={set('city')}>
{['Bengaluru', 'Mumbai', 'Delhi NCR', 'Hyderabad', 'Chennai', 'Pune'].map((c) => <MenuItem key={c} value={c}>{c}</MenuItem>)}
</TextField>
<Grid item xs={12} lg={4}>
<MainCard title="Hub Performance Comparison" sx={{ height: '100%' }}>
<Stack spacing={2.25} sx={{ py: 0.5 }}>
{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 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>
</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} />
</>
);
}
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 { useNavigate, useSearchParams } from 'react-router-dom';
import {
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Checkbox, IconButton,
Tooltip, TablePagination, Typography, SpeedDial, SpeedDialAction, Chip
Card, Stack, Button, Box, Tabs, Tab, Table, TableBody, TableCell, TableContainer, TableHead,
TableRow, Checkbox, IconButton, Tooltip, TablePagination, Typography
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import PostAddOutlinedIcon from '@mui/icons-material/PostAddOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
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 HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
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 = [
{ key: 'created', label: 'Created' },
{ key: 'pending', label: 'Pending' },
{ key: 'all', label: 'All' },
{ key: 'active', label: 'Active' },
{ 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() {
const navigate = useNavigate();
const { exceptions } = useOps();
const { location } = useFilters(); // global location — single source of truth
const [searchParams] = useSearchParams();
const [tab, setTab] = useState(0);
const [search, setSearch] = useState(searchParams.get('q') || '');
const [tenant, setTenant] = useState('all');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5);
const [rpp, setRpp] = useState(10);
const [selected, setSelected] = useState([]);
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(
() =>
orders.filter((o) => {
const matchTab = tabKey === 'created' ? true : o.status === tabKey;
const matchTenant = tenant === 'all' || o.tenant === tenant;
const matchLoc = location === 'all' || o.location === location;
const matchSearch =
!search ||
[o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase());
return matchTab && matchTenant && matchSearch;
!search || [o.id, o.customer, o.pickup, o.drop, o.tenant].join(' ').toLowerCase().includes(search.toLowerCase());
return inTab(o, tabKey) && matchTenant && matchLoc && 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 toggle = (id) => setSelected((p) => (p.includes(id) ? p.filter((x) => x !== id) : [...p, id]));
const counts = {
created: orders.length,
pending: orders.filter((o) => o.status === 'pending').length,
delivered: orders.filter((o) => o.status === 'delivered').length,
cancelled: orders.filter((o) => o.status === 'cancelled').length
};
const count = (k) => orders.filter((o) => inTab(o, k)).length;
const codTotal = orders.reduce((s, o) => s + (o.cod || 0), 0);
const kpis = [
{ 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 (
<>
<PageHeader
title="Orders"
title="Shipments"
breadcrumbs={[{ label: 'Orders' }]}
action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
<Button variant="outlined" startIcon={<PostAddOutlinedIcon />} onClick={() => navigate('/orders/create-multiple')}>
Create Multiple
</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders/create')}>
Create Order
</Button>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.25}>
<Button variant="text" startIcon={<AutoAwesomeOutlinedIcon />} onClick={() => navigate('/orders/assign')}>Assign</Button>
<Button variant="outlined" startIcon={<PostAddOutlinedIcon />} onClick={() => navigate('/orders/create-multiple')}>Bulk</Button>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/orders/create')}>Create Order</Button>
</Stack>
}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<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>
<KpiStrip items={kpis} />
<Card sx={{ mt: 1.5 }}>
{/* filter toolbar */}
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<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); }}>
<Card>
<Box sx={{ px: 1.5, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }} variant="scrollable" scrollButtons="auto">
{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>
</Box>
{selected.length > 0 && (
<Stack direction="row" alignItems="center" spacing={2} sx={{ px: 2, py: 1, bgcolor: 'primary.lighter' }}>
<Typography variant="subtitle2" color="primary.dark">{selected.length} selected</Typography>
<Button size="small" color="error" startIcon={<DeleteOutlineIcon />}>Delete</Button>
</Stack>
)}
<PageToolbar
search={search}
onSearch={(v) => { setSearch(v); setPage(0); }}
searchPlaceholder="Search ID, customer, pickup, drop…"
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>
<Table>
<TableContainer sx={{ maxHeight: { md: 'calc(100vh - 360px)' } }}>
<Table stickyHeader size="small" sx={STICKY_HEAD}>
<TableHead>
<TableRow>
<TableCell padding="checkbox">
@@ -140,56 +142,56 @@ export default function OrdersList() {
onChange={(e) => setSelected(e.target.checked ? paged.map((o) => o.id) : [])}
/>
</TableCell>
<TableCell>#</TableCell>
<TableCell>Tenant</TableCell>
<TableCell>Order ID</TableCell>
<TableCell>Client</TableCell>
<TableCell>Location</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell align="center">QTY</TableCell>
<TableCell>Route</TableCell>
<TableCell align="center">Qty</TableCell>
<TableCell align="right">COD</TableCell>
<TableCell align="right">KMS</TableCell>
<TableCell align="right">KM</TableCell>
<TableCell align="right">Charges</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Actions</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((o) => (
<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 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.location}</TableCell>
<TableCell>{o.pickup}</TableCell>
<TableCell>{o.drop}</TableCell>
<TableCell><Typography variant="caption" color="text.secondary">{o.location}</Typography></TableCell>
<TableCell>
<Typography variant="caption" color="text.secondary" noWrap>{o.pickup} {o.drop}</Typography>
</TableCell>
<TableCell align="center">{o.qty}</TableCell>
<TableCell align="right">{o.cod ? inr(o.cod) : '—'}</TableCell>
<TableCell align="right">{o.kms}</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 align="center">
<TableCell align="right">
<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="Delete"><IconButton size="small" color="error"><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>
</TableCell>
</TableRow>
))}
{paged.length === 0 && (
<TableRow>
<TableCell colSpan={11} sx={{ textAlign: 'center', py: 5, color: 'text.secondary' }}>
No orders match these filters.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
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>
<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 StatusChip from '@/components/StatusChip';
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 { useFilters, inRange } from '@/store/Filters';
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() {
const [location, setLocation] = useState('all');
const { location, range } = useFilters(); // global location + date range
const [tenant, setTenant] = useState('all');
const [loc2, setLoc2] = useState('all');
const [status, setStatus] = useState('all');
const [search, setSearch] = useState('');
const [page, setPage] = useState(0);
@@ -32,16 +36,21 @@ export default function OrdersDetails() {
const filtered = useMemo(
() =>
ordersDetailReport.filter((o) => {
const meta = ORDER_META[o.id] || {};
const matchStatus = status === 'all' || o.status === status;
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 =
!search ||
[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);
return (
@@ -49,12 +58,7 @@ export default function OrdersDetails() {
<PageHeader
title="Orders Details"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Details' }]}
action={
<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>
}
action={<DateRangeFilter />}
/>
<Card>
@@ -63,13 +67,6 @@ export default function OrdersDetails() {
<MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</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">
{STATUSES.map((s) => <MenuItem key={s} value={s}>{s === 'all' ? 'All Status' : s[0].toUpperCase() + s.slice(1)}</MenuItem>)}
</TextField>
@@ -83,6 +80,10 @@ export default function OrdersDetails() {
</Button>
</Stack>
<Box sx={{ px: 2, pb: 1.5 }}>
<FilterSummary count={filtered.length} />
</Box>
<TableContainer>
<Table size="small">
<TableHead>
@@ -130,6 +131,13 @@ export default function OrdersDetails() {
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(o.charges)}</TableCell>
</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>
</Table>
</TableContainer>
@@ -163,11 +171,10 @@ export default function OrdersDetails() {
The export will include {filtered.length} record(s) matching the current filters:
</Typography>
<Grid container spacing={1.5}>
<Filter label="Date Range" value={range.label} />
<Filter label="Location" value={location === 'all' ? 'All Locations' : location} />
<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="Date Range" value="Jun 01 Jun 05" />
<Filter label="Search" value={search || '—'} />
</Grid>
</DialogContent>

View File

@@ -9,8 +9,11 @@ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
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 { useFilters } from '@/store/Filters';
// Show 0 in red, anything else normally.
function NumCell({ value, align = 'center', bold = false }) {
@@ -23,14 +26,16 @@ function NumCell({ value, align = 'center', bold = false }) {
}
export default function OrdersSummary() {
const { location } = useFilters(); // global location — single source of truth
const [open, setOpen] = useState({});
const [location, setLocation] = useState('all');
const [tenant, setTenant] = useState('all');
const [loc2, setLoc2] = useState('all');
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) => ({
oPending: a.oPending + r.orders.pending,
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 }
);
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 (
<>
<PageHeader
title="Orders Summary"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Orders Summary' }]}
action={
<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>
}
action={<DateRangeFilter />}
/>
<Card>
<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">
<MenuItem value="all">All Tenants</MenuItem>
{tenantsList.map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
</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>
<Box sx={{ flexGrow: 1 }} />
<FilterSummary count={rows.length} />
</Stack>
<TableContainer>
@@ -101,7 +95,10 @@ export default function OrdersSummary() {
</TableRow>
</TableHead>
<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}>
<TableRow hover>
<TableCell padding="checkbox">
@@ -172,7 +169,7 @@ export default function OrdersSummary() {
))}
{/* 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 colSpan={2} sx={{ fontWeight: 700 }}>Totals</TableCell>
<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 UserAvatar from '@/components/UserAvatar';
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';
function NumCell({ value, align = 'center' }) {
@@ -32,32 +34,24 @@ function KmsChips({ kms, actual }) {
export default function RidersSummary() {
const [open, setOpen] = useState({});
const [location, setLocation] = useState('all');
const [mapRider, setMapRider] = useState(null);
const toggle = (id) => setOpen((p) => ({ ...p, [id]: !p[id] }));
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 (
<>
<PageHeader
title="Riders Summary"
breadcrumbs={[{ label: 'Reports', to: '/reports' }, { label: 'Riders Summary' }]}
action={
<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>
}
action={<DateRangeFilter />}
/>
<Card>
<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>
<FilterSummary count={ridersSummary.length} />
<Box sx={{ flexGrow: 1 }} />
</Stack>

View File

@@ -20,7 +20,8 @@ import StatCard from '@/components/StatCard';
import StatusChip from '@/components/StatusChip';
import UserAvatar from '@/components/UserAvatar';
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';
const TABS = [
@@ -32,7 +33,7 @@ export default function Riders() {
const navigate = useNavigate();
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [location, setLocation] = useState('all');
const { location } = useFilters(); // global location — single source of truth
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5);
const [expanded, setExpanded] = useState(null);
@@ -72,10 +73,6 @@ export default function Riders() {
breadcrumbs={[{ label: 'Riders' }]}
action={
<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')}>
Add Rider
</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 { Grid, Stack, Typography, Box, Avatar, Table, TableBody, TableCell, TableHead, TableRow, Button, Chip, TextField, InputAdornment, Divider } from '@mui/material';
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined';
import PhotoCameraOutlinedIcon from '@mui/icons-material/PhotoCameraOutlined';
import { Box, Stack, Typography, Button, TextField, InputAdornment } from '@mui/material';
import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined';
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
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 StatCard from '@/components/StatCard';
import MainCard from '@/components/MainCard';
import StatusChip from '@/components/StatusChip';
import LayerBanner from '@/components/LayerBanner';
import MapPlaceholder from '@/components/MapPlaceholder';
import TrackingControlBar from '@/components/tracking/TrackingControlBar';
import ActiveRidersList from '@/components/tracking/ActiveRidersList';
import FleetMap from '@/components/tracking/FleetMap';
import RiderTimeline from '@/components/tracking/RiderTimeline';
import FormDialog from '@/components/FormDialog';
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() {
const navigate = useNavigate();
const [track, setTrack] = useState(null);
const [share, setShare] = useState(false);
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 = () => {
if (navigator.clipboard) navigator.clipboard.writeText(trackLink).catch(() => {});
showToast('Tracking link copied');
};
return (
<>
<PageHeader
title="Execution & Visibility Layer"
breadcrumbs={[{ label: 'Live Tracking' }]}
action={<Button variant="outlined" startIcon={<ShareOutlinedIcon />} onClick={() => setShare(true)}>Share Tracking</Button>}
/>
<Box sx={{ display: 'flex', flexDirection: 'column', height: { md: 'calc(100vh - 112px)' } }}>
{/* compact header */}
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={1} sx={{ mb: 1.5, flexShrink: 0 }}>
<Box>
<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
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']}
/>
<TrackingControlBar />
<Grid container spacing={2.5}>
{executionStages.map((s) => (
<Grid item xs={6} md={2.4} key={s.key}>
<Box sx={{ bgcolor: 'background.paper', border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 2, height: '100%' }}>
<Typography variant="h3" sx={{ fontWeight: 700, color: 'grey.800' }}>{s.count.toLocaleString('en-IN')}</Typography>
<Typography variant="subtitle2" sx={{ mt: 0.25 }}>{s.title}</Typography>
<Stack spacing={0.25} sx={{ mt: 1 }}>
{s.items.slice(0, 3).map((it) => (
<Typography key={it} variant="caption" color="text.secondary"> {it}</Typography>
))}
</Stack>
{/* control tower → Selected Tracking Mode: queue collapses, map expands, feed → rider timeline */}
<Box sx={{ flexGrow: 1, minHeight: 0, display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 2 }}>
{/* map — the primary surface; every rider marker stays visible & clickable */}
<Box sx={{ flexGrow: 1, minWidth: 0, minHeight: { xs: 460, md: 0 } }}>
<MemoFleetMap
vehicles={fleetVehicles}
deliveries={mapDeliveries}
selectedId={selectedId}
onSelect={setSelectedId}
lastUpdated={updated}
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>
</Grid>
))}
<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>
)}
</Box>
</Box>
{/* Share tracking */}
<FormDialog open={share} onClose={() => setShare(false)} title="Share Live Tracking" hideActions>
@@ -207,28 +245,6 @@ export default function LiveTracking() {
</FormDialog>
<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 ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined';
import { useNavigate } from 'react-router-dom';
import {
Grid, Stack, Typography, Box, Avatar, Button, Chip, LinearProgress, Divider,
Table, TableBody, TableCell, TableHead, TableRow
} from '@mui/material';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
import CheckIcon from '@mui/icons-material/Check';
import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import ShareOutlinedIcon from '@mui/icons-material/ShareOutlined';
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 MainCard from '@/components/MainCard';
import MapPlaceholder from '@/components/MapPlaceholder';
import MiniMap from '@/components/tracking/MiniMap';
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() {
const navigate = useNavigate();
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 (
<>
<PageHeader
title="Shipment Journey"
breadcrumbs={[{ label: 'Live Tracking', to: '/tracking' }, { label: j.id }]}
title="Trip Journey"
breadcrumbs={[{ label: 'Live Tracking', to: '/tracking' }, { label: trip.id }]}
action={
<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="contained" startIcon={<ShareOutlinedIcon />} onClick={() => showToast('Tracking link shared with customer')}>Share</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('Trip tracking link shared')}>Share</Button>
</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 }}>
<Grid container spacing={2} alignItems="center">
<Grid item xs={12} md={5}>
<Typography variant="overline" color="text.secondary">{j.id} · {j.client}</Typography>
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mt: 0.5 }}>
<Grid item xs={12} md={4}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 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>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{j.from.city}</Typography>
<Typography variant="caption" color="text.secondary">{j.from.area}</Typography>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{trip.from.city}</Typography>
<Typography variant="caption" color="text.secondary">{trip.from.hub}</Typography>
</Box>
<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>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{j.to.city}</Typography>
<Typography variant="caption" color="text.secondary">{j.to.area}</Typography>
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{trip.to.city}</Typography>
<Typography variant="caption" color="text.secondary">{trip.to.hub}</Typography>
</Box>
</Stack>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>{j.product} · {j.mode}</Typography>
</Grid>
<Grid item xs={6} md={2}><Summary label="Current stage" value={j.currentStage} small /></Grid>
<Grid item xs={6} md={2}><Summary label="ETA" value={j.eta} /></Grid>
<Grid item xs={6} md={1.5}><Summary label="Distance" value={`${j.distance} km`} /></Grid>
<Grid item xs={6} md={1.5}>
<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}><Meta label="Driver" value={trip.driver.name} sub={trip.driver.phone} /></Grid>
<Grid item xs={4} md={1.3}><Meta label="Load" value={`${trip.loadKg} kg`} sub={`${trip.pieces} pcs`} /></Grid>
<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="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{j.progress}%</Typography>
<LinearProgress variant="determinate" value={j.progress} color="primary" sx={{ height: 5, borderRadius: 3, mt: 0.5 }} />
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{trip.progress}%</Typography>
<LinearProgress variant="determinate" value={trip.progress} color="primary" sx={{ height: 5, borderRadius: 3, mt: 0.5 }} />
</Grid>
</Grid>
</Box>
<Grid container spacing={2.5}>
{/* Hop-by-hop timeline */}
{/* Manifest — every shipment on this trip (the scalable centerpiece) */}
<Grid item xs={12} lg={7}>
<MainCard title={`Journey · ${doneCount}/${j.hops.length} stages complete`}>
<Box>
{j.hops.map((h, i) => {
const Icon = HOP_ICONS[h.icon] || WarehouseOutlinedIcon;
const last = i === j.hops.length - 1;
const newMile = i === 0 || j.hops[i - 1].mile !== h.mile;
return (
<Box key={h.key}>
{newMile && (
<Typography variant="overline" color="text.secondary" sx={{ display: 'block', mt: i === 0 ? 0 : 1.5, mb: 0.5, letterSpacing: '0.1em' }}>
{h.mile}
</Typography>
)}
<Stack direction="row" spacing={1.75}>
{/* rail */}
<Stack alignItems="center" sx={{ width: 36 }}>
<Avatar
sx={{
width: 34,
height: 34,
bgcolor: h.status === 'done' ? 'success.main' : h.status === 'active' ? 'primary.main' : 'grey.100',
color: h.status === 'pending' ? 'grey.500' : '#fff',
border: h.status === 'pending' ? '1px solid' : 'none',
borderColor: 'grey.300'
}}
>
{h.status === 'done' ? <CheckIcon sx={{ fontSize: 18 }} /> : <Icon sx={{ fontSize: 18 }} />}
</Avatar>
{!last && <Box sx={{ flexGrow: 1, width: 2, minHeight: 26, bgcolor: h.status === 'done' ? 'success.light' : 'grey.200', my: 0.25 }} />}
</Stack>
{/* content */}
<Box sx={{ pb: last ? 0 : 2, flexGrow: 1, minWidth: 0 }}>
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: h.status === 'pending' ? 'text.secondary' : 'grey.900' }}>{h.title}</Typography>
{h.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 }}>{h.node}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{h.handler}</Typography>
<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>
</Box>
</Stack>
</Box>
);
})}
<MainCard
title={
<Stack direction="row" spacing={1} alignItems="center">
<Inventory2OutlinedIcon fontSize="small" sx={{ color: 'grey.600' }} />
<Typography variant="h5">Manifest</Typography>
<Chip size="small" label={`${trip.manifest.length} shipments`} sx={{ height: 20, bgcolor: 'grey.100', fontWeight: 700 }} />
</Stack>
}
action={<Typography variant="caption" color="text.secondary">{trip.pieces} pieces · {trip.loadKg} kg</Typography>}
noPadding
>
<Box sx={{ maxHeight: 460, overflowY: 'auto' }}>
<Table size="small" stickyHeader sx={{ '& .MuiTableRow-root:hover': { backgroundColor: 'grey.50' }, '& .MuiTableCell-head': { bgcolor: 'grey.50' } }}>
<TableHead>
<TableRow>
<TableCell>Shipment</TableCell>
<TableCell>Customer</TableCell>
<TableCell>Drop Area</TableCell>
<TableCell align="right">Pcs</TableCell>
<TableCell align="right">Weight</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right" />
</TableRow>
</TableHead>
<TableBody>
{trip.manifest.map((s) => (
<TableRow key={s.id} hover sx={{ cursor: 'pointer' }} onClick={() => navigate(`/orders/${s.id}`)}>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{s.id}</TableCell>
<TableCell>
<Typography variant="caption" sx={{ fontWeight: 600 }}>{s.customer}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{s.client}</Typography>
</TableCell>
<TableCell><Typography variant="caption" color="text.secondary">{s.dropArea}</Typography></TableCell>
<TableCell align="right">{s.pieces}</TableCell>
<TableCell align="right"><Typography variant="caption">{s.weightKg} kg</Typography></TableCell>
<TableCell><Chip size="small" label={s.status} sx={{ height: 20, bgcolor: 'info.lighter', color: 'info.dark', fontWeight: 600 }} /></TableCell>
<TableCell align="right"><OpenInNewRoundedIcon sx={{ fontSize: 15, color: 'grey.400' }} /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</MainCard>
</Grid>
{/* Map + live monitoring */}
{/* Map + trip milestones + monitoring — all describe the TRIP */}
<Grid item xs={12} lg={5}>
<Stack spacing={2.5}>
<MainCard title="Live Route">
<MapPlaceholder
height={240}
label={`${j.from.city}${j.to.city}`}
pins={[
{ x: '16%', y: '74%', label: 'Chennai', color: '#00A854' },
{ x: '48%', y: '50%', label: 'Line-haul', color: '#C01227' },
{ x: '80%', y: '24%', label: 'Bengaluru', color: '#595959' }
]}
/>
<MainCard title="Trip Route" noPadding>
<Box sx={{ p: 1.5 }}>
<MiniMap route={ROUTE} progress={trip.progress} vehicle="Truck" pickup={FROM} drop={TO} height={220} />
</Box>
</MainCard>
<MainCard title={`Trip Milestones · ${doneCount}/${trip.milestones.length}`}>
<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
@@ -142,7 +185,7 @@ export default function ShipmentJourney() {
}
>
<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 }}>
<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 }} />}
@@ -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>
);
}