updated the ui for the doormile
This commit is contained in:
14
src/App.jsx
14
src/App.jsx
@@ -35,6 +35,19 @@ export default function App() {
|
||||
|
||||
<Route path="/deliveries" element={load(() => import('@/pages/Deliveries'))} />
|
||||
|
||||
{/* Logistics Operating System — layer surfaces */}
|
||||
<Route path="/three-mile" element={load(() => import('@/pages/network/ThreeMile'))} />
|
||||
<Route path="/cold-chain" element={load(() => import('@/pages/coldchain/ColdChain'))} />
|
||||
<Route path="/trust" element={load(() => import('@/pages/trust/TrustCompliance'))} />
|
||||
<Route path="/hubs" element={load(() => import('@/pages/hubs/HubNetwork'))} />
|
||||
<Route path="/fleet" element={load(() => import('@/pages/fleet/Fleet'))} />
|
||||
<Route path="/dispatch" element={load(() => import('@/pages/dispatch/AiDispatch'))} />
|
||||
<Route path="/tracking" element={load(() => import('@/pages/tracking/LiveTracking'))} />
|
||||
<Route path="/tracking/journey" element={load(() => import('@/pages/tracking/ShipmentJourney'))} />
|
||||
<Route path="/tracking/journey/:id" element={load(() => import('@/pages/tracking/ShipmentJourney'))} />
|
||||
<Route path="/analytics" element={load(() => import('@/pages/analytics/Analytics'))} />
|
||||
<Route path="/integrations" element={load(() => import('@/pages/integrations/Integrations'))} />
|
||||
|
||||
<Route path="/tenants" element={load(() => import('@/pages/tenants/Tenants'))} />
|
||||
<Route path="/tenants/create" element={load(() => import('@/pages/tenants/CreateClient'))} />
|
||||
|
||||
@@ -55,7 +68,6 @@ export default function App() {
|
||||
<Route path="/invoice" element={load(() => import('@/pages/invoice/Invoices'))} />
|
||||
<Route path="/invoice/:id" element={load(() => import('@/pages/invoice/InvoicePreview'))} />
|
||||
|
||||
<Route path="/requests" element={load(() => import('@/pages/Requests'))} />
|
||||
<Route path="/profile" element={load(() => import('@/pages/Profile'))} />
|
||||
<Route path="/settings" element={load(() => import('@/pages/Settings'))} />
|
||||
</Route>
|
||||
|
||||
24
src/components/FormDialog.jsx
Normal file
24
src/components/FormDialog.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, IconButton } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
// ==============================|| GENERIC FORM / DETAIL DIALOG ||============================== //
|
||||
|
||||
export default function FormDialog({ open, onClose, title, children, onSubmit, submitLabel = 'Save', maxWidth = 'sm', hideActions = false }) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth={maxWidth} fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 700, pr: 6 }}>
|
||||
{title}
|
||||
<IconButton onClick={onClose} size="small" sx={{ position: 'absolute', right: 12, top: 12, color: 'grey.500' }}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>{children}</DialogContent>
|
||||
{!hideActions && (
|
||||
<DialogActions sx={{ px: 3, py: 2 }}>
|
||||
<Button color="inherit" onClick={onClose}>Cancel</Button>
|
||||
{onSubmit && <Button variant="contained" onClick={onSubmit}>{submitLabel}</Button>}
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
83
src/components/LayerBanner.jsx
Normal file
83
src/components/LayerBanner.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
|
||||
|
||||
// ==============================|| LAYER BANNER ||============================== //
|
||||
// Clean section header for each operating-system page. Flat neutral surface with a
|
||||
// single subtle accent on the icon tile (no gradients / hero bands).
|
||||
|
||||
export default function LayerBanner({ no, title, subtitle, color = '#C01227', steps = [], icon: Icon }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
p: { xs: 2, md: 2.5 },
|
||||
mb: 3,
|
||||
bgcolor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200'
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<Box
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
flexShrink: 0,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: hexA(color, 0.1),
|
||||
color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{Icon ? <Icon sx={{ fontSize: 24 }} /> : <Typography variant="h5" sx={{ fontWeight: 800 }}>{no}</Typography>}
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
{no ? (
|
||||
<Typography variant="overline" color="text.secondary" sx={{ letterSpacing: '0.1em' }}>
|
||||
Layer {no}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.2 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{steps.length > 0 && (
|
||||
<Stack direction="row" alignItems="center" sx={{ mt: 2, flexWrap: { xs: 'wrap', lg: 'nowrap' }, gap: 1 }}>
|
||||
{steps.map((s, i) => (
|
||||
<Stack key={s} direction="row" alignItems="center" sx={{ flexShrink: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 0.6,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'grey.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
color: 'text.secondary',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{s}
|
||||
</Box>
|
||||
{i < steps.length - 1 && <ChevronRightRoundedIcon sx={{ color: 'grey.400', mx: 0.25, fontSize: 18 }} />}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const hexA = (hex, a) => {
|
||||
const n = parseInt(hex.replace('#', ''), 16);
|
||||
return `rgba(${n >> 16}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
|
||||
};
|
||||
@@ -28,7 +28,37 @@ const MAP = {
|
||||
open: { color: 'info', label: 'Open' },
|
||||
overdue: { color: 'error', label: 'Overdue' },
|
||||
prepaid: { color: 'success', label: 'Prepaid' },
|
||||
cod: { color: 'warning', label: 'COD' }
|
||||
cod: { color: 'warning', label: 'COD' },
|
||||
// trust & risk (Layer 2)
|
||||
cleared: { color: 'success', label: 'Cleared' },
|
||||
review: { color: 'warning', label: 'Review' },
|
||||
hold: { color: 'error', label: 'On Hold' },
|
||||
low: { color: 'success', label: 'Low' },
|
||||
medium: { color: 'warning', label: 'Medium' },
|
||||
high: { color: 'error', label: 'High' },
|
||||
// hub network (Layer 3)
|
||||
busy: { color: 'warning', label: 'Busy' },
|
||||
scheduled: { color: 'info', label: 'Scheduled' },
|
||||
loading: { color: 'warning', label: 'Loading' },
|
||||
// fleet (Layer 4)
|
||||
'on-trip': { color: 'info', label: 'On Trip' },
|
||||
charging: { color: 'info', label: 'Charging' },
|
||||
idle: { color: 'default', label: 'Idle' },
|
||||
maintenance: { color: 'warning', label: 'Maintenance' },
|
||||
// AI dispatch (Layer 5)
|
||||
matched: { color: 'info', label: 'Matched' },
|
||||
optimizing: { color: 'warning', label: 'Optimizing' },
|
||||
dispatched: { color: 'primary', label: 'Dispatched' },
|
||||
// execution (Layer 6)
|
||||
'picked-up': { color: 'primary', label: 'Picked Up' },
|
||||
exception: { color: 'error', label: 'Exception' },
|
||||
// integrations (Layer 8)
|
||||
connected: { color: 'success', label: 'Connected' },
|
||||
degraded: { color: 'warning', label: 'Degraded' },
|
||||
// cold chain (pharma)
|
||||
'in-range': { color: 'success', label: 'In Range' },
|
||||
'at-risk': { color: 'warning', label: 'At Risk' },
|
||||
breach: { color: 'error', label: 'Breach' }
|
||||
};
|
||||
|
||||
const TONE = {
|
||||
|
||||
77
src/components/SystemPipeline.jsx
Normal file
77
src/components/SystemPipeline.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Box, Stack, Typography } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { systemLayers } from '@/data/mock';
|
||||
|
||||
// ==============================|| SYSTEM PIPELINE (8-layer operating-system strip) ||============================== //
|
||||
// Restrained, monochrome cards — neutral surface, dark metrics, brand-red only on hover,
|
||||
// a single semantic health dot. No rainbow borders / colored badges.
|
||||
|
||||
const ROUTES = {
|
||||
book: '/orders',
|
||||
trust: '/trust',
|
||||
hubs: '/hubs',
|
||||
fleet: '/fleet',
|
||||
dispatch: '/dispatch',
|
||||
execution: '/tracking',
|
||||
analytics: '/analytics',
|
||||
integrations: '/integrations'
|
||||
};
|
||||
|
||||
export default function SystemPipeline() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: 'repeat(2, 1fr)', sm: 'repeat(4, 1fr)', xl: 'repeat(8, 1fr)' },
|
||||
gap: 1.25
|
||||
}}
|
||||
>
|
||||
{systemLayers.map((l) => (
|
||||
<Box
|
||||
key={l.key}
|
||||
onClick={() => navigate(ROUTES[l.key])}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
height: '100%',
|
||||
borderRadius: 1.5,
|
||||
p: 1.75,
|
||||
bgcolor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200',
|
||||
transition: 'border-color .15s ease, box-shadow .15s ease',
|
||||
'&:hover': { borderColor: 'primary.main', boxShadow: '0 1px 2px rgba(16,24,40,0.08)' }
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.25 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'grey.100',
|
||||
color: 'grey.700',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 12,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{l.no}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: l.health === 'optimizing' ? 'warning.main' : 'success.main' }}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.2 }}>{l.title}</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>{l.subtitle}</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.900' }}>{l.metric}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{l.metricLabel}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
68
src/components/ThreeMileStrip.jsx
Normal file
68
src/components/ThreeMileStrip.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Box, Stack, Typography, LinearProgress } from '@mui/material';
|
||||
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
|
||||
import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined';
|
||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
|
||||
|
||||
import { threeMile } from '@/data/mock';
|
||||
|
||||
const ICONS = { first: WarehouseOutlinedIcon, mid: LocalShippingOutlinedIcon, last: HomeOutlinedIcon };
|
||||
|
||||
// ==============================|| THREE-MILE STRIP (First → Mid → Last Mile) ||============================== //
|
||||
// Neutral cards, dark metrics, single grey progress — no per-stage rainbow colors.
|
||||
|
||||
export default function ThreeMileStrip({ compact = false }) {
|
||||
return (
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} alignItems="stretch">
|
||||
{threeMile.map((s, i) => {
|
||||
const Icon = ICONS[s.key];
|
||||
return (
|
||||
<Stack key={s.key} direction={{ xs: 'column', md: 'row' }} spacing={1.5} alignItems="center" sx={{ flex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
borderRadius: 1.5,
|
||||
p: { xs: 1.75, md: 2 },
|
||||
bgcolor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderColor: 'grey.200'
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<Box sx={{ width: 38, height: 38, borderRadius: 1.5, bgcolor: 'grey.100', color: 'grey.700', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Icon fontSize="small" />
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'grey.900', lineHeight: 1.1 }}>{s.title}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{s.subtitle}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="baseline" spacing={0.75} sx={{ mt: 1.5 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'grey.900' }}>{s.metric}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{s.metricLabel}</Typography>
|
||||
</Stack>
|
||||
{!compact && (
|
||||
<>
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ mt: 1.5, mb: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary">On-time</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.800' }}>{s.onTime}%</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" value={s.onTime} sx={{ height: 5, borderRadius: 3, bgcolor: 'grey.100', '& .MuiLinearProgress-bar': { bgcolor: 'grey.800' } }} />
|
||||
<Stack spacing={0.25} sx={{ mt: 1.25 }}>
|
||||
{s.features.map((f) => (
|
||||
<Typography key={f} variant="caption" color="text.secondary">• {f}</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{i < threeMile.length - 1 && (
|
||||
<ChevronRightRoundedIcon sx={{ color: 'grey.300', transform: { xs: 'rotate(90deg)', md: 'none' }, flexShrink: 0 }} />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
23
src/components/Toast.jsx
Normal file
23
src/components/Toast.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Snackbar, Alert } from '@mui/material';
|
||||
|
||||
// ==============================|| TOAST / SNACKBAR ||============================== //
|
||||
// Usage: const [toast, showToast] = useToast(); ... showToast('Saved'); <Toast {...toast} />
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function useToast() {
|
||||
const [state, setState] = useState({ open: false, message: '', severity: 'success' });
|
||||
const show = useCallback((message, severity = 'success') => setState({ open: true, message, severity }), []);
|
||||
const onClose = useCallback(() => setState((s) => ({ ...s, open: false })), []);
|
||||
return [{ ...state, onClose }, show];
|
||||
}
|
||||
|
||||
export default function Toast({ open, message, severity = 'success', onClose }) {
|
||||
return (
|
||||
<Snackbar open={open} autoHideDuration={3000} onClose={onClose} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
|
||||
<Alert onClose={onClose} severity={severity} variant="filled" sx={{ width: '100%' }}>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
250
src/data/mock.js
250
src/data/mock.js
@@ -97,12 +97,6 @@ export const invoiceLineItems = [
|
||||
{ particulars: 'COD handling fee', unit: 'order', qty: 260, rate: 6, other: 0, amount: 1560 }
|
||||
];
|
||||
|
||||
export const requests = [
|
||||
{ id: 1, requestor: 'Freshly Foods', bank: 'HDFC Bank', ifsc: 'HDFC0001234', refNo: 'RQ-88121', amount: 24500, reason: 'Weekly settlement payout', contact: 'Anil Gupta', address: 'Koramangala 4th Block', city: 'Bengaluru', zip: '560034', accountNo: '5012 3344 7788', pricing: [{ category: 'Standard', skill: 'Bike', cost: 9 }, { category: 'Express', skill: 'Bike', cost: 12 }] },
|
||||
{ id: 2, requestor: 'UrbanCart', bank: 'ICICI Bank', ifsc: 'ICIC0004567', refNo: 'RQ-88122', amount: 18900, reason: 'Fuel reimbursement', contact: 'Meera Nair', address: 'Indiranagar', city: 'Bengaluru', zip: '560038', accountNo: '6022 1199 4455', pricing: [{ category: 'Standard', skill: 'Bike', cost: 8 }] },
|
||||
{ id: 3, requestor: 'MediQuick Pharma', bank: 'Axis Bank', ifsc: 'UTIB0007788', refNo: 'RQ-88123', amount: 9700, reason: 'Adjustment - April', contact: 'Rohit Sen', address: 'Andheri West', city: 'Mumbai', zip: '400058', accountNo: '7033 5566 1122', pricing: [{ category: 'Standard', skill: 'Bike', cost: 10 }] }
|
||||
];
|
||||
|
||||
// reports
|
||||
export const ordersSummary = tenants.map((t, i) => ({
|
||||
id: t.id,
|
||||
@@ -179,3 +173,247 @@ export const statusBreakdown = [
|
||||
{ label: 'Pending', value: 48, color: '#FFBF00' },
|
||||
{ label: 'Cancelled', value: 18, color: '#F04134' }
|
||||
];
|
||||
|
||||
// ==============================|| THREE-MILE MODEL (First / Mid / Last Mile) ||============================== //
|
||||
// Mirrors doormile.com's primary narrative: Origin → Hub → Hub → Doorstep.
|
||||
export const threeMile = [
|
||||
{
|
||||
key: 'first', title: 'First Mile', subtitle: 'Origin to Hub', color: '#EA580C',
|
||||
metric: 184, metricLabel: 'pickups today', onTime: 98.1,
|
||||
features: ['AI-scheduled pickups', 'Dynamic load consolidation', 'Yard & dock management', 'Pickup quality checks']
|
||||
},
|
||||
{
|
||||
key: 'mid', title: 'Mid Mile', subtitle: 'Hub to Hub Transit', color: '#0E7C7B',
|
||||
metric: 4, metricLabel: 'line-hauls live', onTime: 97.4,
|
||||
features: ['Optimised line-haul routing', 'Cross-docking & sortation', 'Live SLA monitoring', 'EV-first corridors']
|
||||
},
|
||||
{
|
||||
key: 'last', title: 'Last Mile', subtitle: 'Hub to Doorstep', color: '#1D4ED8',
|
||||
metric: 96, metricLabel: 'out for delivery', onTime: 98.9,
|
||||
features: ['Multi-stop route optimization', 'Precise delivery windows', 'Digital proof of delivery', 'Real-time customer updates']
|
||||
}
|
||||
];
|
||||
|
||||
// ==============================|| END-TO-END SHIPMENT JOURNEY (hop-by-hop) ||============================== //
|
||||
// Follows ONE parcel Chennai → Bengaluru through every node: agent → nearest hub → origin main hub
|
||||
// → line-haul → destination main hub → sub hub → delivery agent → customer. With live monitoring & reroute.
|
||||
export const shipmentJourney = {
|
||||
id: 'DM-CHN-BLR-7741',
|
||||
product: 'Documents & electronics · 3.2 kg',
|
||||
from: { city: 'Chennai', area: 'T. Nagar', name: 'Suresh Kumar' },
|
||||
to: { city: 'Bengaluru', area: 'Koramangala', name: 'Riya Sharma' },
|
||||
client: 'TechNova Retail',
|
||||
distance: 346,
|
||||
mode: 'Standard · EV-first',
|
||||
placed: '08 Jun, 09:12 AM',
|
||||
eta: '09 Jun, 02:30 PM',
|
||||
progress: 52,
|
||||
currentStage: 'Line-Haul · Chennai → Bengaluru',
|
||||
hops: [
|
||||
{ key: 'booked', mile: 'First Mile', title: 'Shipment Booked', node: 'Chennai · T. Nagar', handler: 'Customer · Suresh Kumar', icon: 'order', time: '08 Jun, 09:12 AM', status: 'done', detail: 'Shipment ID generated · digital docs & e-waybill created' },
|
||||
{ key: 'pickup', mile: 'First Mile', title: 'Agent Pickup', node: 'Chennai · T. Nagar', handler: 'Rider · Faisal Khan (EV 2W)', icon: 'agent', time: '08 Jun, 10:05 AM', status: 'done', detail: 'OTP verified · photo proof captured · tamper seal applied' },
|
||||
{ key: 'nearhub', mile: 'First Mile', title: 'Nearest Hub — Check-in', node: 'Nungambakkam Micro Hub', handler: 'Hub operations', icon: 'hub', time: '08 Jun, 10:48 AM', status: 'done', detail: 'Scanned in · sorted for origin main hub' },
|
||||
{ key: 'mainhub-out', mile: 'Mid Mile', title: 'Origin Main Hub — Dispatched', node: 'Guindy Regional Hub (Chennai)', handler: 'Hub operations', icon: 'hub', time: '08 Jun, 01:20 PM', status: 'done', detail: 'Consolidated & loaded onto line-haul EV truck' },
|
||||
{ key: 'linehaul', mile: 'Mid Mile', title: 'Line-Haul In Transit', node: 'NH48 · Chennai → Bengaluru', handler: 'Driver · Imran Sheikh (EV Truck 4W)', icon: 'truck', time: '08 Jun, 01:35 PM', status: 'active', detail: 'En route · live GPS · 178 km to destination hub' },
|
||||
{ key: 'desthub', mile: 'Mid Mile', title: 'Destination Main Hub — Arrival', node: 'Hoskote Regional Hub (Bengaluru)', handler: 'Hub operations', icon: 'hub', time: 'Est. 09 Jun, 06:30 AM', status: 'pending', detail: 'Inbound scan & sortation' },
|
||||
{ key: 'subhub', mile: 'Last Mile', title: 'Sub Hub — Last-Mile Sort', node: 'Koramangala Micro Hub', handler: 'Hub operations', icon: 'hub', time: 'Est. 09 Jun, 10:15 AM', status: 'pending', detail: 'Routed to delivery agent zone' },
|
||||
{ key: 'assigned', mile: 'Last Mile', title: 'Delivery Agent Assigned', node: 'Bengaluru · Koramangala', handler: 'MileTruth AI · auto-assign', icon: 'agent', time: 'Est. 09 Jun, 11:00 AM', status: 'pending', detail: 'Multi-stop route optimized for the agent' },
|
||||
{ key: 'ofd', mile: 'Last Mile', title: 'Out for Delivery', node: 'Bengaluru · Koramangala', handler: 'Rider · auto-assigned (EV 2W)', icon: 'agent', time: 'Est. 09 Jun, 01:40 PM', status: 'pending', detail: 'Live tracking link shared with customer' },
|
||||
{ key: 'delivered', mile: 'Last Mile', title: 'Delivered', node: 'Bengaluru · Koramangala', handler: 'Customer · Riya Sharma', icon: 'done', time: 'Est. 09 Jun, 02:30 PM', status: 'pending', detail: 'OTP + photo proof of delivery' }
|
||||
],
|
||||
events: [
|
||||
{ time: '08 Jun, 03:10 PM', type: 'reroute', title: 'Traffic on NH48 near Krishnagiri', detail: 'MileTruth AI rerouted via Hosur bypass — ETA protected (+0 min)' },
|
||||
{ time: '08 Jun, 05:40 PM', type: 'monitor', title: 'Weather check · clear', detail: 'No disruption forecast on the corridor' },
|
||||
{ time: '08 Jun, 08:15 PM', type: 'monitor', title: 'On schedule', detail: 'Vehicle at 62% battery · charging stop planned at Hosur' }
|
||||
]
|
||||
};
|
||||
|
||||
// ==============================|| INDUSTRY VERTICALS ||============================== //
|
||||
export const verticals = [
|
||||
{ key: 'fmcg', label: 'FMCG', desc: 'High-volume, expiry-sensitive', shipments: 612, onTime: 98.9, color: '#EA580C' },
|
||||
{ key: 'pharma', label: 'Pharma', desc: 'Cold-chain & compliance', shipments: 248, onTime: 99.4, color: '#0E7C7B' },
|
||||
{ key: 'enterprise', label: 'Enterprise & B2B', desc: 'Appointment & white-glove', shipments: 542, onTime: 97.6, color: '#1D4ED8' }
|
||||
];
|
||||
|
||||
// maps each business client to its industry vertical
|
||||
export const tenantVertical = {
|
||||
'Freshly Foods': 'FMCG',
|
||||
'GreenLeaf Organics': 'FMCG',
|
||||
'BloomBox Florists': 'FMCG',
|
||||
'MediQuick Pharma': 'Pharma',
|
||||
UrbanCart: 'Enterprise & B2B',
|
||||
'TechNova Retail': 'Enterprise & B2B'
|
||||
};
|
||||
export const verticalOf = (tenant) => tenantVertical[tenant] || 'Enterprise & B2B';
|
||||
|
||||
// ==============================|| PHARMA COLD-CHAIN ||============================== //
|
||||
export const coldChainSummary = { monitored: 248, inRange: 241, atRisk: 5, breaches: 2, compliance: 99.2, avgTemp: 4.2 };
|
||||
export const coldChainShipments = [
|
||||
{ id: 'DM-CC-2041', product: 'Insulin vials', tenant: 'MediQuick Pharma', range: '2 – 8 °C', temp: 4.6, route: 'Andheri → Bandra', status: 'in-range', excursionMin: 0 },
|
||||
{ id: 'DM-CC-2042', product: 'mRNA vaccine', tenant: 'MediQuick Pharma', range: '-20 – -15 °C', temp: -17.2, route: 'Hub HYD → BLR', status: 'in-range', excursionMin: 0 },
|
||||
{ id: 'DM-CC-2043', product: 'Frozen seafood', tenant: 'Freshly Foods', range: '≤ -18 °C', temp: -12.4, route: 'Koramangala → HSR', status: 'breach', excursionMin: 14 },
|
||||
{ id: 'DM-CC-2044', product: 'Dairy & yogurt', tenant: 'GreenLeaf Organics', range: '2 – 6 °C', temp: 7.1, route: 'Koregaon → Viman Nagar', status: 'at-risk', excursionMin: 3 },
|
||||
{ id: 'DM-CC-2045', product: 'Blood samples', tenant: 'MediQuick Pharma', range: '2 – 8 °C', temp: 5.3, route: 'Connaught Pl → Saket', status: 'in-range', excursionMin: 0 },
|
||||
{ id: 'DM-CC-2046', product: 'Antibiotics', tenant: 'MediQuick Pharma', range: '15 – 25 °C', temp: 21.4, route: 'Hitech City → Gachibowli', status: 'in-range', excursionMin: 0 }
|
||||
];
|
||||
|
||||
// ==============================|| LOGISTICS OPERATING SYSTEM — LAYER DATA ||============================== //
|
||||
// Mirrors the 8-layer end-to-end flow: Book → Trust → Hub → Fleet → MileTruth AI → Execution → Analytics → Integrations.
|
||||
|
||||
// The 8 layers of the operating system — drives the System Overview pipeline strip.
|
||||
export const systemLayers = [
|
||||
{ no: 1, key: 'book', title: 'Book & Create', subtitle: 'Shipment intake', color: '#1E3A8A', metric: '1,402', metricLabel: 'shipments', health: 'healthy' },
|
||||
{ no: 2, key: 'trust', title: 'Trust & Identity', subtitle: 'KYC · fraud · custody', color: '#5B5BD6', metric: '99.2%', metricLabel: 'cleared', health: 'healthy' },
|
||||
{ no: 3, key: 'hubs', title: 'Hub Network', subtitle: 'Sort · line-haul', color: '#0E7C7B', metric: '12', metricLabel: 'hubs live', health: 'healthy' },
|
||||
{ no: 4, key: 'fleet', title: 'Fleet & Riders', subtitle: 'EV-first capacity', color: '#15803D', metric: '48', metricLabel: 'riders online', health: 'healthy' },
|
||||
{ no: 5, key: 'dispatch', title: 'MileTruth AI', subtitle: 'Dispatch · optimize', color: '#EA580C', metric: '34%', metricLabel: 'route savings', health: 'optimizing' },
|
||||
{ no: 6, key: 'execution', title: 'Execution', subtitle: 'Track · proof', color: '#1D4ED8', metric: '96', metricLabel: 'in transit', health: 'healthy' },
|
||||
{ no: 7, key: 'analytics', title: 'Analytics', subtitle: 'Insight · ML loop', color: '#7C3AED', metric: '98.6%', metricLabel: 'on-time', health: 'healthy' },
|
||||
{ no: 8, key: 'integrations', title: 'Integrations', subtitle: 'APIs · partners', color: '#0F766E', metric: '24', metricLabel: 'connectors', health: 'healthy' }
|
||||
];
|
||||
|
||||
// ---- Layer 2: Trust, Security & Compliance ----
|
||||
export const trustChecks = [
|
||||
{ key: 'kyc', title: 'KYC Verification', desc: 'Verify senders & businesses', icon: 'kyc', passRate: 99.2, pending: 6, color: '#5B5BD6' },
|
||||
{ key: 'id', title: 'ID Verification', desc: 'Aadhaar / GST / PAN validation', icon: 'id', passRate: 98.7, pending: 3, color: '#1D4ED8' },
|
||||
{ key: 'fraud', title: 'Fraud Detection', desc: 'ML model flags risky shipments', icon: 'fraud', passRate: 97.4, pending: 9, color: '#C01227' },
|
||||
{ key: 'tamper', title: 'Tamper Protection', desc: 'Secure packaging & seal tracking', icon: 'tamper', passRate: 99.8, pending: 1, color: '#EA580C' },
|
||||
{ key: 'custody', title: 'Chain of Custody', desc: 'Immutable logs at every touchpoint', icon: 'custody', passRate: 100, pending: 0, color: '#0E7C7B' },
|
||||
{ key: 'compliance', title: 'Compliance Engine', desc: 'Legal, tax & regulatory checks', icon: 'compliance', passRate: 99.5, pending: 2, color: '#15803D' }
|
||||
];
|
||||
|
||||
export const trustQueue = [
|
||||
{ id: 'DM-10244', entity: 'Arjun Mehta', type: 'Sender KYC', check: 'Aadhaar', risk: 'low', score: 12, status: 'cleared', flagged: 'ID match · address verified' },
|
||||
{ id: 'DM-10246', entity: 'TechNova Retail', type: 'Business GST', check: 'GST', risk: 'medium', score: 48, status: 'review', flagged: 'High-value COD ₹4,999 · ID required' },
|
||||
{ id: 'DM-10248', entity: 'Suresh Kumar', type: 'Sender KYC', check: 'PAN', risk: 'low', score: 18, status: 'cleared', flagged: 'Repeat customer' },
|
||||
{ id: 'DM-10251', entity: 'QuickMart Express', type: 'Fraud screen', check: 'ML ID3', risk: 'high', score: 82, status: 'hold', flagged: 'Velocity anomaly · 14 orders / 5 min' },
|
||||
{ id: 'DM-10242', entity: 'Karthik Rao', type: 'Tamper seal', check: 'Seal scan', risk: 'low', score: 5, status: 'cleared', flagged: 'Seal intact · photo logged' }
|
||||
];
|
||||
|
||||
// ---- Layer 3: Intelligent Hub Network ----
|
||||
export const hubs = [
|
||||
{ id: 'HUB-BLR-01', name: 'Koramangala Micro Hub', type: 'Micro Hub', city: 'Bengaluru', lat: 12.9352, lng: 77.6245, capacity: 1200, load: 940, inbound: 180, outbound: 210, dock: 4, status: 'online' },
|
||||
{ id: 'HUB-BLR-02', name: 'Whitefield City Hub', type: 'City Hub', city: 'Bengaluru', lat: 12.9698, lng: 77.7499, capacity: 4000, load: 2860, inbound: 520, outbound: 610, dock: 10, status: 'online' },
|
||||
{ id: 'HUB-BLR-RG', name: 'Hoskote Regional Hub', type: 'Regional Hub', city: 'Bengaluru', lat: 13.0707, lng: 77.7980, capacity: 12000, load: 7400, inbound: 1900, outbound: 2100, dock: 24, status: 'online' },
|
||||
{ id: 'HUB-MUM-01', name: 'Andheri City Hub', type: 'City Hub', city: 'Mumbai', lat: 19.1197, lng: 72.8468, capacity: 4500, load: 3950, inbound: 700, outbound: 760, dock: 12, status: 'busy' },
|
||||
{ id: 'HUB-HYD-01', name: 'Hitech Cross Dock', type: 'Cross Dock', city: 'Hyderabad', lat: 17.4435, lng: 78.3772, capacity: 3000, load: 1100, inbound: 340, outbound: 360, dock: 8, status: 'online' },
|
||||
{ id: 'HUB-DEL-RG', name: 'Bilaspur Regional Hub', type: 'Regional Hub', city: 'Delhi NCR', lat: 28.4231, lng: 77.0490, capacity: 14000, load: 11200, inbound: 2400, outbound: 2600, dock: 28, status: 'busy' }
|
||||
];
|
||||
|
||||
export const hubNetworkTypes = [
|
||||
{ type: 'Micro Hubs (Urban)', count: 5, desc: 'Last-mile EV staging' },
|
||||
{ type: 'City Hubs', count: 4, desc: 'Sort & local distribution' },
|
||||
{ type: 'Regional Hubs', count: 2, desc: 'Aggregation & line-haul origin' },
|
||||
{ type: 'Cross Dock Points', count: 1, desc: 'No-store transfer' }
|
||||
];
|
||||
|
||||
export const lineHauls = [
|
||||
{ id: 'LH-001', from: 'Hoskote Regional Hub', to: 'Bilaspur Regional Hub', corridor: 'BLR → DEL', distance: 2150, vehicle: 'EV Truck 4W', load: 86, eta: '34h', status: 'in-transit' },
|
||||
{ id: 'LH-002', from: 'Andheri City Hub', to: 'Hoskote Regional Hub', corridor: 'MUM → BLR', distance: 980, vehicle: 'ICE Truck 6W', load: 72, eta: '18h', status: 'in-transit' },
|
||||
{ id: 'LH-003', from: 'Hitech Cross Dock', to: 'Hoskote Regional Hub', corridor: 'HYD → BLR', distance: 570, vehicle: 'EV Truck 4W', load: 64, eta: '11h', status: 'scheduled' },
|
||||
{ id: 'LH-004', from: 'Bilaspur Regional Hub', to: 'Andheri City Hub', corridor: 'DEL → MUM', distance: 1420, vehicle: 'ICE Truck 6W', load: 91, eta: '24h', status: 'loading' }
|
||||
];
|
||||
|
||||
// ---- Layer 4: Fleet & Rider Operating System ----
|
||||
export const fleet = [
|
||||
{ id: 'VH-EV-018', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 78, range: 96, health: 94, capacityKg: 600, status: 'on-trip', rider: 'Mohan Das', hub: 'Koramangala Micro Hub', uptime: 98.2 },
|
||||
{ id: 'VH-EV-022', model: 'Euler HiLoad EV', type: 'EV 3W', powertrain: 'EV', battery: 41, range: 38, health: 89, capacityKg: 688, status: 'charging', rider: '—', hub: 'Whitefield City Hub', uptime: 96.5 },
|
||||
{ id: 'VH-2W-104', model: 'Ola S1 Pro', type: 'EV 2W', powertrain: 'EV', battery: 63, range: 71, health: 91, capacityKg: 20, status: 'on-trip', rider: 'Ravi Teja', hub: 'Hitech Cross Dock', uptime: 97.1 },
|
||||
{ id: 'VH-IC-211', model: 'Mahindra Bolero', type: 'ICE 4W', powertrain: 'ICE', battery: null, range: 320, health: 82, capacityKg: 1500, status: 'idle', rider: '—', hub: 'Hoskote Regional Hub', uptime: 93.4 },
|
||||
{ id: 'VH-2W-118', model: 'Honda Activa', type: 'ICE 2W', powertrain: 'ICE', battery: null, range: 180, health: 76, capacityKg: 25, status: 'maintenance', rider: '—', hub: 'Andheri City Hub', uptime: 88.9 },
|
||||
{ id: 'VH-EV-031', model: 'Tata Ace EV', type: 'EV 4W', powertrain: 'EV', battery: 88, range: 108, health: 96, capacityKg: 600, status: 'idle', rider: '—', hub: 'Bilaspur Regional Hub', uptime: 99.0 }
|
||||
];
|
||||
|
||||
export const fleetSummary = {
|
||||
total: 124, ev: 86, ice: 38, onTrip: 52, charging: 14, idle: 41, maintenance: 17,
|
||||
evShare: 69, co2SavedKg: 12840, avgBattery: 64, avgHealth: 90
|
||||
};
|
||||
|
||||
// ---- Layer 5: MileTruth AI Engine — dispatch & optimization ----
|
||||
export const dispatchQueue = [
|
||||
{ id: 'DM-10246', pickup: 'Hitech City', drop: 'Gachibowli', priority: 'high', sla: '45 min', suggestedRider: 'Ravi Teja', confidence: 96, status: 'matched', etaMin: 22 },
|
||||
{ id: 'DM-10244', pickup: 'Connaught Place', drop: 'Saket, Block C', priority: 'standard', sla: '90 min', suggestedRider: 'Sandeep Roy', confidence: 88, status: 'optimizing', etaMin: 41 },
|
||||
{ id: 'DM-10248', pickup: 'T. Nagar', drop: 'Adyar', priority: 'standard', sla: '120 min', suggestedRider: 'Faisal Khan', confidence: 91, status: 'matched', etaMin: 33 },
|
||||
{ id: 'DM-10242', pickup: 'Indiranagar Store', drop: 'Whitefield, Phase 1', priority: 'express', sla: '60 min', suggestedRider: 'Mohan Das', confidence: 94, status: 'dispatched', etaMin: 28 }
|
||||
];
|
||||
|
||||
export const aiTriggers = [
|
||||
{ key: 'new-order', label: 'New Order', count: 18, icon: 'order' },
|
||||
{ key: 'rider-delay', label: 'Rider Delay / Exception', count: 3, icon: 'delay' },
|
||||
{ key: 'traffic', label: 'Traffic Change', count: 7, icon: 'traffic' },
|
||||
{ key: 'weather', label: 'Weather Change', count: 1, icon: 'weather' },
|
||||
{ key: 'cancellation', label: 'Cancellation / Return', count: 2, icon: 'cancel' },
|
||||
{ key: 'high-priority', label: 'High Priority Shipment', count: 4, icon: 'priority' }
|
||||
];
|
||||
|
||||
export const aiPipeline = [
|
||||
{ stage: 'Data Ingestion', items: ['Live orders', 'Rider GPS', 'Traffic / weather', 'Hub status'] },
|
||||
{ stage: 'Data Processing', items: ['Cleaning', 'GPS smoothing', 'Geohash encoding', 'Feature eng.'] },
|
||||
{ stage: 'Intelligence', items: ['Demand prediction', 'Delay prediction', 'Risk scoring (ID3)', 'Zone learning'] },
|
||||
{ stage: 'Optimization', items: ['OR-Tools VRP', 'Multi-objective', 'Distance+Time+Cost', 'Re-optimization'] },
|
||||
{ stage: 'Assignment', items: ['Rider matching', 'Batch creation', 'Route sequencing', 'Trip planning'] },
|
||||
{ stage: 'Output', items: ['Optimized routes', 'ETA', 'Rider plans', 'Delivery windows'] }
|
||||
];
|
||||
|
||||
export const aiMetrics = { routeSavings: 34, avgEtaAccuracy: 92, batchRate: 2.6, reoptToday: 41, delaysPredicted: 28, delaysAvoided: 23 };
|
||||
|
||||
// ---- Layer 6: Execution & Visibility ----
|
||||
export const executionFeed = [
|
||||
{ id: 'DM-10242', stage: 'In-Transit', rider: 'Mohan Das', loc: 'HSR Layout', detail: 'On route · 4.2 km to drop', time: '10:42 AM', proof: null },
|
||||
{ id: 'DM-10243', stage: 'Picked Up', rider: 'Imran Sheikh', loc: 'Andheri West', detail: 'OTP verified · photo proof logged', time: '10:38 AM', proof: 'pickup' },
|
||||
{ id: 'DM-10247', stage: 'Delivered', rider: 'Ravi Teja', loc: 'Viman Nagar', detail: 'eSign captured · POD uploaded', time: '10:30 AM', proof: 'delivery' },
|
||||
{ id: 'DM-10251', stage: 'Exception', rider: 'Faisal Khan', loc: 'Adyar', detail: 'Customer unavailable · reattempt scheduled', time: '10:21 AM', proof: null },
|
||||
{ id: 'DM-10246', stage: 'Dispatched', rider: 'Sandeep Roy', loc: 'Hitech City', detail: 'Rider en route to pickup', time: '10:18 AM', proof: null }
|
||||
];
|
||||
|
||||
export const executionStages = [
|
||||
{ key: 'pickup', title: 'Pickup Execution', items: ['Rider reaches pickup', 'OTP / eSign', 'Photo proof', 'Proof of pickup'], count: 12 },
|
||||
{ key: 'transit', title: 'In-Transit Tracking', items: ['Live GPS', 'Route monitoring', 'ETA updates', 'Geofencing'], count: 96 },
|
||||
{ key: 'rerouting', title: 'Dynamic Re-routing', items: ['Avoid delays', 'Optimize live', 'Re-assign if needed', 'SLA protection'], count: 7 },
|
||||
{ key: 'delivery', title: 'Delivery Execution', items: ['OTP / eSign', 'Photo proof', 'Proof of delivery', 'Feedback'], count: 1330 },
|
||||
{ key: 'exception', title: 'Exception Handling', items: ['Delay mgmt', 'Reattempt', 'Damage / loss', 'Escalation'], count: 4 }
|
||||
];
|
||||
|
||||
// ---- Layer 7: Analytics & Intelligence ----
|
||||
export const analyticsKpis = {
|
||||
onTime: 98.6, successRate: 99.1, costPerDelivery: 74, fuelEnergyCost: 21, loadFactor: 82,
|
||||
customerInsightsNps: 71, slaAchievement: 97.4
|
||||
};
|
||||
|
||||
export const lanePerformance = [
|
||||
{ lane: 'BLR → BLR (intra)', shipments: 820, onTime: 99.2, costPer: 68, ev: 92 },
|
||||
{ lane: 'BLR → DEL', shipments: 142, onTime: 96.8, costPer: 188, ev: 64 },
|
||||
{ lane: 'MUM → BLR', shipments: 98, onTime: 97.5, costPer: 162, ev: 58 },
|
||||
{ lane: 'HYD → BLR', shipments: 76, onTime: 98.1, costPer: 121, ev: 71 },
|
||||
{ lane: 'DEL → MUM', shipments: 64, onTime: 95.4, costPer: 174, ev: 49 }
|
||||
];
|
||||
|
||||
export const mlLoop = [
|
||||
{ step: 'Data Logging', detail: 'Every shipment event captured' },
|
||||
{ step: 'Model Retraining', detail: 'Nightly on fresh outcomes' },
|
||||
{ step: 'A/B Testing', detail: 'Routing & pricing variants' },
|
||||
{ step: 'Hyperparameter Tuning', detail: 'Auto-tuned weekly' }
|
||||
];
|
||||
|
||||
export const outcomes = [
|
||||
{ label: 'Faster Deliveries', value: '35%+', caption: 'improvement', color: '#00A854' },
|
||||
{ label: 'Lower Operational Cost', value: '30–40%', caption: 'savings', color: '#0E7C7B' },
|
||||
{ label: 'Higher Reliability', value: '99%+', caption: 'success rate', color: '#1D4ED8' },
|
||||
{ label: 'Full Visibility', value: 'E2E', caption: 'end-to-end', color: '#7C3AED' },
|
||||
{ label: 'Sustainable & Green', value: 'EV-First', caption: 'operations', color: '#15803D' }
|
||||
];
|
||||
|
||||
// ---- Layer 8: Integrations & Ecosystem ----
|
||||
export const integrations = [
|
||||
{ name: 'Order API', group: 'APIs & Integrations', desc: 'Create & manage shipments', status: 'connected', calls: '1.2M / day', icon: 'api' },
|
||||
{ name: 'Tracking API', group: 'APIs & Integrations', desc: 'Real-time status & ETA', status: 'connected', calls: '4.8M / day', icon: 'api' },
|
||||
{ name: 'Webhooks', group: 'APIs & Integrations', desc: 'Event push to partners', status: 'connected', calls: '900K / day', icon: 'api' },
|
||||
{ name: 'SAP ERP / WMS', group: 'Enterprise Systems', desc: 'Warehouse & inventory sync', status: 'connected', calls: 'realtime', icon: 'erp' },
|
||||
{ name: 'TMS', group: 'Enterprise Systems', desc: 'Transport management', status: 'connected', calls: 'realtime', icon: 'erp' },
|
||||
{ name: 'CRM', group: 'Enterprise Systems', desc: 'Customer records', status: 'degraded', calls: 'realtime', icon: 'erp' },
|
||||
{ name: 'Razorpay', group: 'Payment & Billing', desc: 'Prepaid / COD settlement', status: 'connected', calls: '320K / day', icon: 'pay' },
|
||||
{ name: 'Corporate Billing', group: 'Payment & Billing', desc: 'Invoice & reconciliation', status: 'connected', calls: 'daily', icon: 'pay' },
|
||||
{ name: '3PL Partners', group: 'Partners & Ecosystem', desc: 'Capacity overflow routing', status: 'connected', calls: '12 partners', icon: 'partner' },
|
||||
{ name: 'Transport Partners', group: 'Partners & Ecosystem', desc: 'Line-haul carriers', status: 'connected', calls: '8 carriers', icon: 'partner' },
|
||||
{ name: 'Warehouse Partners', group: 'Partners & Ecosystem', desc: 'Fulfilment nodes', status: 'pending', calls: '—', icon: 'partner' }
|
||||
];
|
||||
|
||||
@@ -34,13 +34,14 @@ import DoneAllIcon from '@mui/icons-material/DoneAll';
|
||||
|
||||
import Logo from '@/components/Logo';
|
||||
|
||||
const RED = '#C01227';
|
||||
const RED = '#C01227'; // brand accent (avatars, dots)
|
||||
const BAR = '#8E1F2A'; // muted deep-brick top bar (toned down from vivid #C01227)
|
||||
|
||||
const INITIAL_NOTIFICATIONS = [
|
||||
{ id: 1, icon: Inventory2OutlinedIcon, title: 'New order #ORD-10482 placed', time: '2 min ago', to: '/orders', read: false },
|
||||
{ id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/riders', read: false },
|
||||
{ id: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false },
|
||||
{ id: 4, icon: AssignmentOutlinedIcon, title: '3 new onboarding requests', time: '3 hrs ago', to: '/requests', read: true }
|
||||
{ id: 4, icon: AssignmentOutlinedIcon, title: 'MileTruth AI re-optimized 41 routes', time: '3 hrs ago', to: '/dispatch', read: true }
|
||||
];
|
||||
|
||||
const MESSAGES = [
|
||||
@@ -78,7 +79,7 @@ export default function Header({ onToggle }) {
|
||||
<AppBar
|
||||
position="fixed"
|
||||
elevation={0}
|
||||
sx={{ bgcolor: RED, color: '#fff', zIndex: (t) => t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }}
|
||||
sx={{ bgcolor: BAR, color: '#fff', zIndex: (t) => t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }}
|
||||
>
|
||||
<Toolbar sx={{ minHeight: 64, px: { xs: 1.5, sm: 2.5 }, gap: 1 }}>
|
||||
<IconButton color="inherit" edge="start" onClick={onToggle} sx={{ mr: 0.5 }}>
|
||||
@@ -194,7 +195,7 @@ export default function Header({ onToggle }) {
|
||||
);
|
||||
})}
|
||||
<Divider />
|
||||
<MenuItem onClick={() => { closeNotif(); navigate('/requests'); }} sx={{ justifyContent: 'center', color: 'primary.main', fontWeight: 600 }}>
|
||||
<MenuItem onClick={() => { closeNotif(); navigate('/dashboard'); }} sx={{ justifyContent: 'center', color: 'primary.main', fontWeight: 600 }}>
|
||||
View all activity
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -19,10 +19,10 @@ import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
|
||||
import navItems from '@/menu/navItems';
|
||||
import Logo from '@/components/Logo';
|
||||
|
||||
export const DRAWER_WIDTH = 264;
|
||||
export const MINI_WIDTH = 78;
|
||||
export const DRAWER_WIDTH = 232;
|
||||
export const MINI_WIDTH = 76;
|
||||
|
||||
const RED = '#C01227';
|
||||
const NAV_BG = '#8E1F2A'; // muted deep-brick brand red (toned down from vivid #C01227)
|
||||
|
||||
function NavLeaf({ item, open, active, depth = 0, onClick }) {
|
||||
const Icon = item.icon;
|
||||
@@ -80,11 +80,31 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Box sx={{ bgcolor: RED, height: '100%', color: '#fff', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ bgcolor: NAV_BG, height: '100%', color: '#fff', display: 'flex', flexDirection: 'column' }}>
|
||||
<Toolbar sx={{ px: expanded ? 2.5 : 0, justifyContent: expanded ? 'flex-start' : 'center', minHeight: 64 }}>
|
||||
<Logo onDark compact={!expanded} />
|
||||
</Toolbar>
|
||||
<Box sx={{ overflowY: 'auto', overflowX: 'hidden', flexGrow: 1, pb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
flexGrow: 1,
|
||||
pb: 2,
|
||||
// slim, subtle scrollbar tuned for the dark-red sidebar — only shows on hover
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'transparent transparent',
|
||||
'&:hover': { scrollbarColor: 'rgba(255,255,255,0.3) transparent' },
|
||||
'&::-webkit-scrollbar': { width: 6 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 8,
|
||||
transition: 'background-color 0.2s ease'
|
||||
},
|
||||
'&:hover::-webkit-scrollbar-thumb': { backgroundColor: 'rgba(255,255,255,0.28)' },
|
||||
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: 'rgba(255,255,255,0.45)' }
|
||||
}}
|
||||
>
|
||||
{navItems.map((grp) => (
|
||||
<Box key={grp.group} sx={{ mt: 1 }}>
|
||||
{expanded && (
|
||||
@@ -156,8 +176,11 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
|
||||
</Box>
|
||||
{expanded && (
|
||||
<Box sx={{ p: 2, borderTop: '1px solid rgba(255,255,255,0.12)' }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.55)' }}>
|
||||
Doormile Console v1.0
|
||||
<Typography variant="caption" sx={{ color: '#fff', fontWeight: 600, display: 'block', lineHeight: 1.3 }}>
|
||||
Delivering Trust.
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
Beyond Boundaries · v1.0
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -7,34 +7,70 @@ import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
|
||||
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
|
||||
import BarChartOutlinedIcon from '@mui/icons-material/BarChartOutlined';
|
||||
import ReceiptLongOutlinedIcon from '@mui/icons-material/ReceiptLongOutlined';
|
||||
import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
|
||||
import SummarizeOutlinedIcon from '@mui/icons-material/SummarizeOutlined';
|
||||
import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined';
|
||||
import MapOutlinedIcon from '@mui/icons-material/MapOutlined';
|
||||
import SecurityOutlinedIcon from '@mui/icons-material/SecurityOutlined';
|
||||
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
|
||||
import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined';
|
||||
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
|
||||
import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined';
|
||||
import TrendingUpOutlinedIcon from '@mui/icons-material/TrendingUpOutlined';
|
||||
import ApiOutlinedIcon from '@mui/icons-material/ApiOutlined';
|
||||
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
|
||||
import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined';
|
||||
import AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
|
||||
|
||||
// ==============================|| DOORMILE - SIDEBAR NAV CONFIG ||============================== //
|
||||
// ==============================|| DOORMILE - LOGISTICS OPERATING SYSTEM NAV ||============================== //
|
||||
// Groups mirror the 8-layer end-to-end flow of the Doormile operating system.
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
group: 'Operations',
|
||||
group: 'Overview',
|
||||
items: [
|
||||
{ id: 'dashboard', title: 'Dashboard', url: '/dashboard', icon: DashboardOutlinedIcon },
|
||||
{ id: 'orders', title: 'Orders', url: '/orders', icon: Inventory2OutlinedIcon },
|
||||
{ id: 'deliveries', title: 'Deliveries', url: '/deliveries', icon: MopedOutlinedIcon }
|
||||
{ id: 'dashboard', title: 'System Overview', url: '/dashboard', icon: DashboardOutlinedIcon },
|
||||
{ id: 'three-mile', title: 'Three-Mile Network', url: '/three-mile', icon: RouteOutlinedIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'Network',
|
||||
group: '1 · Book & Create',
|
||||
items: [
|
||||
{ id: 'tenants', title: 'Tenants', url: '/tenants', icon: ApartmentOutlinedIcon },
|
||||
{ id: 'pricing', title: 'Pricing', url: '/pricing', icon: PaymentsOutlinedIcon },
|
||||
{ id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon },
|
||||
{ id: 'orders', title: 'Shipments', url: '/orders', icon: Inventory2OutlinedIcon },
|
||||
{ id: 'customers', title: 'Customers', url: '/customers', icon: GroupsOutlinedIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: '2 · Trust & Identity',
|
||||
items: [{ id: 'trust', title: 'Trust & Compliance', url: '/trust', icon: SecurityOutlinedIcon }]
|
||||
},
|
||||
{
|
||||
group: '3 · Hub Network',
|
||||
items: [{ id: 'hubs', title: 'Hub Network', url: '/hubs', icon: HubOutlinedIcon }]
|
||||
},
|
||||
{
|
||||
group: '4 · Fleet & Riders',
|
||||
items: [
|
||||
{ id: 'fleet', title: 'Fleet', url: '/fleet', icon: ElectricRickshawOutlinedIcon },
|
||||
{ id: 'riders', title: 'Riders', url: '/riders', icon: TwoWheelerOutlinedIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'Finance & Insights',
|
||||
group: '5 · MileTruth AI',
|
||||
items: [{ id: 'dispatch', title: 'AI Dispatch', url: '/dispatch', icon: AutoAwesomeOutlinedIcon }]
|
||||
},
|
||||
{
|
||||
group: '6 · Execution',
|
||||
items: [
|
||||
{ id: 'tracking', title: 'Live Tracking', url: '/tracking', icon: MyLocationOutlinedIcon },
|
||||
{ id: 'journey', title: 'Shipment Journey', url: '/tracking/journey', icon: AltRouteOutlinedIcon },
|
||||
{ id: 'cold-chain', title: 'Cold Chain', url: '/cold-chain', icon: AcUnitOutlinedIcon },
|
||||
{ id: 'deliveries', title: 'Deliveries', url: '/deliveries', icon: MopedOutlinedIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: '7 · Analytics',
|
||||
items: [
|
||||
{ id: 'analytics', title: 'Analytics', url: '/analytics', icon: TrendingUpOutlinedIcon },
|
||||
{
|
||||
id: 'reports',
|
||||
title: 'Reports',
|
||||
@@ -45,9 +81,21 @@ const navItems = [
|
||||
{ id: 'riders-summary', title: 'Riders Summary', url: '/reports/riders-summary', icon: TwoWheelerOutlinedIcon },
|
||||
{ id: 'riders-logs', title: 'Riders Logs', url: '/reports/riders-logs', icon: MapOutlinedIcon }
|
||||
]
|
||||
},
|
||||
{ id: 'invoice', title: 'Invoice', url: '/invoice', icon: ReceiptLongOutlinedIcon },
|
||||
{ id: 'requests', title: 'Requests', url: '/requests', icon: AssignmentOutlinedIcon }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: '8 · Integrations',
|
||||
items: [
|
||||
{ id: 'integrations', title: 'Integrations', url: '/integrations', icon: ApiOutlinedIcon },
|
||||
{ id: 'tenants', title: 'Clients', url: '/tenants', icon: ApartmentOutlinedIcon }
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'Finance',
|
||||
items: [
|
||||
{ id: 'pricing', title: 'Pricing', url: '/pricing', icon: PaymentsOutlinedIcon },
|
||||
{ id: 'invoice', title: 'Invoice', url: '/invoice', icon: ReceiptLongOutlinedIcon }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Grid, Card, CardContent, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, Avatar, MenuItem, TextField } from '@mui/material';
|
||||
import { Grid, Stack, Typography, Box, Button, Divider, Table, TableBody, TableCell, TableHead, TableRow, MenuItem, TextField, Avatar, LinearProgress, Chip } 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 CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
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 PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
@@ -12,14 +16,20 @@ import StatusChip from '@/components/StatusChip';
|
||||
import AreaChart from '@/components/charts/AreaChart';
|
||||
import DonutChart from '@/components/charts/DonutChart';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { ordersTrend, statusBreakdown, orders, riders } from '@/data/mock';
|
||||
import SystemPipeline from '@/components/SystemPipeline';
|
||||
import ThreeMileStrip from '@/components/ThreeMileStrip';
|
||||
import Toast, { useToast } from '@/components/Toast';
|
||||
import { ordersTrend, statusBreakdown, orders, riders, aiMetrics, fleetSummary, verticals, verticalOf } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
const VERTICAL_COLOR = Object.fromEntries(verticals.map((v) => [v.label, v.color]));
|
||||
|
||||
export default function Dashboard() {
|
||||
const [toast, showToast] = useToast();
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
title="System Overview"
|
||||
breadcrumbs={[{ label: 'Dashboard' }]}
|
||||
action={
|
||||
<Stack direction="row" spacing={1.5}>
|
||||
@@ -28,15 +38,35 @@ export default function Dashboard() {
|
||||
<MenuItem value="blr">Bengaluru</MenuItem>
|
||||
<MenuItem value="mum">Mumbai</MenuItem>
|
||||
</TextField>
|
||||
<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />}>Export</Button>
|
||||
<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('System overview exported as CSV')}>Export</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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 Orders" 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="vs last month" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active Riders" value="48" icon={TwoWheelerOutlinedIcon} color="info" trend={-2.3} caption="vs last month" /></Grid>
|
||||
<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}>
|
||||
@@ -61,39 +91,42 @@ export default function Dashboard() {
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={7}>
|
||||
<MainCard title="Recent Orders" noPadding>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Order ID</TableCell>
|
||||
<TableCell>Customer</TableCell>
|
||||
<TableCell>Route</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{orders.slice(0, 6).map((o) => (
|
||||
<TableRow key={o.id} hover>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{o.id}</TableCell>
|
||||
<TableCell>{o.customer}</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>
|
||||
{/* 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={5}>
|
||||
<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, 5).map((r, i) => (
|
||||
<Stack key={r.id} direction="row" spacing={2} alignItems="center" sx={{ py: 1.25 }}>
|
||||
{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 }}>
|
||||
@@ -109,11 +142,84 @@ export default function Dashboard() {
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={4}>
|
||||
<MainCard title="By Industry Vertical">
|
||||
<Stack spacing={1.5}>
|
||||
{verticals.map((v) => (
|
||||
<Box key={v.key} sx={{ border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 1.5 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: v.color }} />
|
||||
<Box>
|
||||
<Typography variant="subtitle2">{v.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{v.desc}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<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>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<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>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<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">
|
||||
@@ -122,3 +228,8 @@ 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})`;
|
||||
};
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Card, Stack, Button, Box, Collapse, Tabs, Tab, Typography, Grid,
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton,
|
||||
TablePagination, Dialog, DialogTitle, DialogContent, DialogActions, TextField
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { requests } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
function RequestRow({ row, index }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow hover sx={{ '& > *': { borderBottom: open ? 'unset' : undefined } }}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>{row.requestor}</TableCell>
|
||||
<TableCell>{row.bank}</TableCell>
|
||||
<TableCell>{row.ifsc}</TableCell>
|
||||
<TableCell>{row.refNo}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(row.amount)}</TableCell>
|
||||
<TableCell>{row.reason}</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconButton size="small" onClick={() => setOpen((o) => !o)}>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell sx={{ py: 0, borderBottom: open ? 1 : 0, borderColor: 'divider' }} colSpan={8}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ m: 2 }}>
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 2 }}>
|
||||
<Tab label="Client Details" />
|
||||
<Tab label="Client Pricing" />
|
||||
</Tabs>
|
||||
|
||||
{tab === 0 && (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">Contact Name</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.contact}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">Address</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.address}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">City</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.city}</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<Typography variant="caption" color="text.secondary">Zip Code</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{row.zip}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{tab === 1 && (
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ '& th': { bgcolor: 'grey.50', fontWeight: 700 } }}>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Category</TableCell>
|
||||
<TableCell>Skill</TableCell>
|
||||
<TableCell align="right">Cost/Hr</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{row.pricing.map((p, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>{i + 1}</TableCell>
|
||||
<TableCell>{p.category}</TableCell>
|
||||
<TableCell>{p.skill}</TableCell>
|
||||
<TableCell align="right">{inr(p.cost)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Requests() {
|
||||
const [page, setPage] = useState(0);
|
||||
const [rpp, setRpp] = useState(10);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const paged = requests.slice(page * rpp, page * rpp + rpp);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Requests"
|
||||
breadcrumbs={[{ label: 'Requests' }]}
|
||||
action={
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={() => setOpen(true)}>
|
||||
Create Request
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Requestor</TableCell>
|
||||
<TableCell>Bank</TableCell>
|
||||
<TableCell>IFSC</TableCell>
|
||||
<TableCell>Ref No</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
<TableCell>Reason</TableCell>
|
||||
<TableCell align="center" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paged.map((row, idx) => (
|
||||
<RequestRow key={row.id} row={row} index={page * rpp + idx} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
component="div" count={requests.length} page={page} onPageChange={(_, p) => setPage(p)}
|
||||
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25, 100]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Dialog open={open} onClose={() => setOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Create Request</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2.5} sx={{ mt: 0 }}>
|
||||
<Grid item xs={12} sm={6}><TextField label="Reference No" type="number" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Requestor" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Bank Name" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Amount" type="number" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="Account No" type="number" fullWidth /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField label="IFSC Code" fullWidth /></Grid>
|
||||
<Grid item xs={12}><TextField label="Reason" fullWidth multiline minRows={3} /></Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setOpen(false)} color="inherit">Close</Button>
|
||||
<Button variant="contained" onClick={() => setOpen(false)}>Update</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
141
src/pages/analytics/Analytics.jsx
Normal file
141
src/pages/analytics/Analytics.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Grid, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Divider } from '@mui/material';
|
||||
import TrendingUpOutlinedIcon from '@mui/icons-material/TrendingUpOutlined';
|
||||
import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee';
|
||||
import SsidChartOutlinedIcon from '@mui/icons-material/SsidChartOutlined';
|
||||
import AutorenewOutlinedIcon from '@mui/icons-material/AutorenewOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import LayerBanner from '@/components/LayerBanner';
|
||||
import AreaChart from '@/components/charts/AreaChart';
|
||||
import Toast, { useToast } from '@/components/Toast';
|
||||
import { analyticsKpis, lanePerformance, mlLoop, outcomes, ordersTrend } from '@/data/mock';
|
||||
import { inr } from '@/utils/format';
|
||||
|
||||
export default function Analytics() {
|
||||
const [toast, showToast] = useToast();
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Analytics & Intelligence Layer"
|
||||
breadcrumbs={[{ label: 'Analytics' }]}
|
||||
action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('Analytics report exported')}>Export Report</Button>}
|
||||
/>
|
||||
|
||||
<LayerBanner
|
||||
no={7}
|
||||
icon={TrendingUpOutlinedIcon}
|
||||
color="#7C3AED"
|
||||
title="Analytics & Intelligence"
|
||||
subtitle="Operational, cost, business & predictive insight feeding a continuous ML improvement loop."
|
||||
steps={['Operational Analytics', 'Cost Intelligence', 'Business Intelligence', 'Predictive Insights', 'Continuous Improvement']}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="On-Time Delivery" value={`${analyticsKpis.onTime}%`} icon={CheckCircleOutlineIcon} color="success" trend={1.2} caption="SLA achievement 97.4%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Success Rate" value={`${analyticsKpis.successRate}%`} icon={TrendingUpOutlinedIcon} color="info" trend={0.6} caption="first-attempt" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cost / Delivery" value={inr(analyticsKpis.costPerDelivery)} icon={CurrencyRupeeIcon} color="warning" trend={-4.1} caption="energy cost ₹21" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Load Factor" value={`${analyticsKpis.loadFactor}%`} icon={SsidChartOutlinedIcon} color="primary" trend={2.3} caption="capacity utilised" /></Grid>
|
||||
|
||||
<Grid item xs={12} lg={8}>
|
||||
<MainCard
|
||||
title="Cost vs Volume Trend"
|
||||
action={<Stack direction="row" spacing={2}><Legend color="#7C3AED" label="Orders" /><Legend color="#00A854" label="Delivered" /></Stack>}
|
||||
>
|
||||
<AreaChart
|
||||
labels={ordersTrend.map((d) => d.m)}
|
||||
series={[
|
||||
{ name: 'Orders', color: '#7C3AED', 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="Continuous Improvement (ML Loop)">
|
||||
<Stack divider={<Divider />} spacing={0}>
|
||||
{mlLoop.map((m) => (
|
||||
<Stack key={m.step} direction="row" spacing={1.5} alignItems="center" sx={{ py: 1.35 }}>
|
||||
<Avatar variant="rounded" sx={{ bgcolor: '#F3E8FF', color: '#7C3AED', width: 38, height: 38 }}>
|
||||
<AutorenewOutlinedIcon fontSize="small" />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="subtitle2">{m.step}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{m.detail}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={7}>
|
||||
<MainCard title="Lane Performance" noPadding>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Lane</TableCell>
|
||||
<TableCell align="right">Shipments</TableCell>
|
||||
<TableCell>On-Time</TableCell>
|
||||
<TableCell align="right">Cost / Del</TableCell>
|
||||
<TableCell>EV %</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{lanePerformance.map((l) => (
|
||||
<TableRow key={l.lane} hover>
|
||||
<TableCell><Typography variant="subtitle2">{l.lane}</Typography></TableCell>
|
||||
<TableCell align="right">{l.shipments.toLocaleString('en-IN')}</TableCell>
|
||||
<TableCell sx={{ minWidth: 110 }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Box sx={{ width: 54 }}>
|
||||
<LinearProgress variant="determinate" value={l.onTime} color={l.onTime > 97 ? 'success' : 'warning'} sx={{ height: 6, borderRadius: 3 }} />
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>{l.onTime}%</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="right">{inr(l.costPer)}</TableCell>
|
||||
<TableCell><Typography variant="caption" sx={{ fontWeight: 600, color: l.ev > 70 ? 'success.main' : 'text.secondary' }}>{l.ev}%</Typography></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={5}>
|
||||
<MainCard title="System Outcomes">
|
||||
<Grid container spacing={1.5}>
|
||||
{outcomes.map((o) => (
|
||||
<Grid item xs={12} sm={6} key={o.label}>
|
||||
<Box sx={{ border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 1.75, height: '100%' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<CheckCircleOutlineIcon sx={{ color: 'success.main', fontSize: 20 }} />
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, color: 'grey.900' }}>{o.value}</Typography>
|
||||
</Stack>
|
||||
<Typography variant="subtitle2" sx={{ mt: 0.5 }}>{o.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{o.caption}</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Legend({ color, label }) {
|
||||
return (
|
||||
<Stack direction="row" spacing={0.75} alignItems="center">
|
||||
<Box sx={{ width: 10, height: 10, borderRadius: '3px', bgcolor: color }} />
|
||||
<Typography variant="caption" color="text.secondary">{label}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -49,18 +49,21 @@ export default function Login() {
|
||||
<Box sx={{ position: 'absolute', width: 280, height: 280, borderRadius: '50%', bgcolor: 'rgba(255,255,255,0.06)', bottom: -80, left: -60 }} />
|
||||
<Logo onDark />
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Typography variant="h2" sx={{ color: '#fff', fontWeight: 800, lineHeight: 1.2 }}>
|
||||
Move every parcel,
|
||||
<br /> on time, every time.
|
||||
<Typography variant="overline" sx={{ color: 'rgba(255,255,255,0.7)', letterSpacing: '0.14em' }}>
|
||||
One Connected System · One Promise Kept
|
||||
</Typography>
|
||||
<Typography variant="h2" sx={{ color: '#fff', fontWeight: 800, lineHeight: 1.2, mt: 0.5 }}>
|
||||
Delivering Trust.
|
||||
<br /> Beyond Boundaries.
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.85)', mt: 2, maxWidth: 420 }}>
|
||||
The command center for your last-mile operation — orders, riders, pricing and settlements in one corporate console.
|
||||
The MileTruth™ AI command center for Connected Miles — first-mile, mid-mile and last-mile delivery, unified in one intelligent console.
|
||||
</Typography>
|
||||
<Stack spacing={1.5} sx={{ mt: 4 }}>
|
||||
{[
|
||||
{ icon: BoltIcon, t: 'AI-assisted route optimisation' },
|
||||
{ icon: LocalShippingOutlinedIcon, t: 'Real-time rider & delivery tracking' },
|
||||
{ icon: VerifiedOutlinedIcon, t: 'Automated client invoicing & payouts' }
|
||||
{ icon: BoltIcon, t: 'MileTruth™ AI route optimisation' },
|
||||
{ icon: LocalShippingOutlinedIcon, t: 'EV-first real-time tracking & proof of delivery' },
|
||||
{ icon: VerifiedOutlinedIcon, t: 'Cold-chain & chain-of-custody compliance' }
|
||||
].map((f) => (
|
||||
<Stack key={f.t} direction="row" spacing={1.5} alignItems="center">
|
||||
<Box sx={{ width: 34, height: 34, borderRadius: 2, bgcolor: 'rgba(255,255,255,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
|
||||
146
src/pages/coldchain/ColdChain.jsx
Normal file
146
src/pages/coldchain/ColdChain.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Grid, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button } from '@mui/material';
|
||||
import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined';
|
||||
import DeviceThermostatOutlinedIcon from '@mui/icons-material/DeviceThermostatOutlined';
|
||||
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
|
||||
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 Toast, { useToast } from '@/components/Toast';
|
||||
import { coldChainSummary, coldChainShipments } from '@/data/mock';
|
||||
|
||||
const TEMP_COLOR = { 'in-range': '#00A854', 'at-risk': '#FFBF00', breach: '#F04134' };
|
||||
|
||||
export default function ColdChain() {
|
||||
const [toast, showToast] = useToast();
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Pharma Cold-Chain"
|
||||
breadcrumbs={[{ label: 'Cold Chain' }]}
|
||||
action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('Cold-chain compliance report generated')}>Compliance Report</Button>}
|
||||
/>
|
||||
|
||||
<LayerBanner
|
||||
no={0}
|
||||
icon={AcUnitOutlinedIcon}
|
||||
color="#0E7C7B"
|
||||
title="Temperature-Monitored Logistics"
|
||||
subtitle="Battery-aware EV cold-chain with live temperature monitoring, excursion alerts and regulatory compliance."
|
||||
steps={['Sensor Telemetry', 'Live Monitoring', 'Excursion Alerts', 'Compliance Logs']}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Monitored Shipments" value={coldChainSummary.monitored} icon={AcUnitOutlinedIcon} color="info" caption="cold-chain active" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="In Range" value={coldChainSummary.inRange} icon={CheckCircleOutlineIcon} color="success" caption={`avg ${coldChainSummary.avgTemp}°C`} /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="At Risk / Breach" value={`${coldChainSummary.atRisk} / ${coldChainSummary.breaches}`} icon={WarningAmberOutlinedIcon} color="warning" caption="needs attention" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Compliance" value={`${coldChainSummary.compliance}%`} icon={VerifiedOutlinedIcon} color="success" trend={0.3} caption="regulatory pass" /></Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<MainCard title="Live Temperature Monitoring" noPadding>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Shipment</TableCell>
|
||||
<TableCell>Product</TableCell>
|
||||
<TableCell>Client</TableCell>
|
||||
<TableCell>Required Range</TableCell>
|
||||
<TableCell>Current Temp</TableCell>
|
||||
<TableCell>Excursion</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{coldChainShipments.map((s) => {
|
||||
const color = TEMP_COLOR[s.status];
|
||||
return (
|
||||
<TableRow key={s.id} hover>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{s.id}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{s.product}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{s.route}</Typography>
|
||||
</TableCell>
|
||||
<TableCell><Typography variant="caption">{s.tenant}</Typography></TableCell>
|
||||
<TableCell>{s.range}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Avatar variant="rounded" sx={{ bgcolor: hexA(color, 0.12), color, width: 30, height: 30 }}>
|
||||
<DeviceThermostatOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color }}>{s.temp}°C</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{s.excursionMin > 0 ? (
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'error.main' }}>{s.excursionMin} min</Typography>
|
||||
) : (
|
||||
<Typography variant="caption" color="text.secondary">—</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell><StatusChip status={s.status} /></TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<MainCard title="Compliance Health">
|
||||
<Stack spacing={2} sx={{ py: 0.5 }}>
|
||||
<Bar label="In-range shipments" value={Math.round((coldChainSummary.inRange / coldChainSummary.monitored) * 100)} color="#00A854" />
|
||||
<Bar label="Regulatory compliance" value={coldChainSummary.compliance} color="#0E7C7B" />
|
||||
<Bar label="Excursion-free deliveries" value={97} color="#1D4ED8" />
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<MainCard title="Cold-Chain Capabilities">
|
||||
<Grid container spacing={1.5}>
|
||||
{[
|
||||
{ t: 'Live sensor telemetry', d: 'IoT probes per shipment' },
|
||||
{ t: 'Excursion alerts', d: 'Instant breach notification' },
|
||||
{ t: 'Battery-aware EV routing', d: 'Reefer charge planning' },
|
||||
{ t: 'Chain-of-custody logs', d: 'Immutable audit trail' }
|
||||
].map((c) => (
|
||||
<Grid item xs={12} sm={6} key={c.t}>
|
||||
<Box sx={{ border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 1.75, height: '100%' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<AcUnitOutlinedIcon sx={{ color: '#0E7C7B', fontSize: 20 }} />
|
||||
<Typography variant="subtitle2">{c.t}</Typography>
|
||||
</Stack>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>{c.d}</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Bar({ label, value, color }) {
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary">{label}</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color }}>{value}%</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" value={value} sx={{ height: 6, borderRadius: 3, bgcolor: hexA(color, 0.12), '& .MuiLinearProgress-bar': { bgcolor: color } }} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const hexA = (hex, a) => {
|
||||
const n = parseInt(hex.replace('#', ''), 16);
|
||||
return `rgba(${n >> 16}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
|
||||
};
|
||||
161
src/pages/dispatch/AiDispatch.jsx
Normal file
161
src/pages/dispatch/AiDispatch.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState } from 'react';
|
||||
import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Chip, Divider } from '@mui/material';
|
||||
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
|
||||
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
|
||||
import SpeedOutlinedIcon from '@mui/icons-material/SpeedOutlined';
|
||||
import InsightsOutlinedIcon from '@mui/icons-material/InsightsOutlined';
|
||||
import PsychologyOutlinedIcon from '@mui/icons-material/PsychologyOutlined';
|
||||
import FiberNewOutlinedIcon from '@mui/icons-material/FiberNewOutlined';
|
||||
import TimerOutlinedIcon from '@mui/icons-material/TimerOutlined';
|
||||
import TrafficOutlinedIcon from '@mui/icons-material/TrafficOutlined';
|
||||
import ThunderstormOutlinedIcon from '@mui/icons-material/ThunderstormOutlined';
|
||||
import ReplayOutlinedIcon from '@mui/icons-material/ReplayOutlined';
|
||||
import PriorityHighOutlinedIcon from '@mui/icons-material/PriorityHighOutlined';
|
||||
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined';
|
||||
|
||||
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 Toast, { useToast } from '@/components/Toast';
|
||||
import { dispatchQueue, aiTriggers, aiPipeline, aiMetrics } from '@/data/mock';
|
||||
|
||||
const TRIGGER_ICONS = {
|
||||
order: FiberNewOutlinedIcon,
|
||||
delay: TimerOutlinedIcon,
|
||||
traffic: TrafficOutlinedIcon,
|
||||
weather: ThunderstormOutlinedIcon,
|
||||
cancel: ReplayOutlinedIcon,
|
||||
priority: PriorityHighOutlinedIcon
|
||||
};
|
||||
|
||||
const NEXT_STATUS = { optimizing: 'matched', matched: 'dispatched', dispatched: 'dispatched' };
|
||||
|
||||
export default function AiDispatch() {
|
||||
const [rows, setRows] = useState(dispatchQueue);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [toast, showToast] = useToast();
|
||||
|
||||
const runOptimization = () => {
|
||||
setRunning(true);
|
||||
setRows((q) =>
|
||||
q.map((d) => ({
|
||||
...d,
|
||||
confidence: Math.min(99, d.confidence + 2 + ((d.id.charCodeAt(d.id.length - 1) % 4))),
|
||||
etaMin: Math.max(12, d.etaMin - 3),
|
||||
status: NEXT_STATUS[d.status] || d.status
|
||||
}))
|
||||
);
|
||||
showToast('MileTruth AI re-optimized the queue · ETAs improved');
|
||||
setRunning(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="MileTruth AI Engine"
|
||||
breadcrumbs={[{ label: 'AI Dispatch' }]}
|
||||
action={<Button variant="contained" startIcon={<PlayArrowOutlinedIcon />} onClick={runOptimization} disabled={running}>Run Optimization</Button>}
|
||||
/>
|
||||
|
||||
<LayerBanner
|
||||
no={5}
|
||||
icon={AutoAwesomeOutlinedIcon}
|
||||
color="#EA580C"
|
||||
title="AI Dispatch & Optimization"
|
||||
subtitle="Smart pricing, route optimization, delay prediction and dynamic re-routing in real time."
|
||||
steps={['Data Ingestion', 'Processing', 'Intelligence', 'Optimization', 'Assignment', 'Output']}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Route Savings" value={`${aiMetrics.routeSavings}%`} icon={RouteOutlinedIcon} color="success" trend={2.8} caption="vs naïve routing" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="ETA Accuracy" value={`${aiMetrics.avgEtaAccuracy}%`} icon={SpeedOutlinedIcon} color="info" trend={1.4} caption="predicted vs actual" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Re-optimizations" value={aiMetrics.reoptToday} icon={PsychologyOutlinedIcon} color="warning" caption="triggered today" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Delays Avoided" value={`${aiMetrics.delaysAvoided}/${aiMetrics.delaysPredicted}`} icon={InsightsOutlinedIcon} color="primary" caption="predicted & prevented" /></Grid>
|
||||
|
||||
<Grid item xs={12} lg={8}>
|
||||
<MainCard title="Live Dispatch Queue" noPadding>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Shipment</TableCell>
|
||||
<TableCell>Route</TableCell>
|
||||
<TableCell>Priority</TableCell>
|
||||
<TableCell>SLA</TableCell>
|
||||
<TableCell>AI-matched Rider</TableCell>
|
||||
<TableCell>Confidence</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((d) => (
|
||||
<TableRow key={d.id} hover>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{d.id}</TableCell>
|
||||
<TableCell><Typography variant="caption" color="text.secondary">{d.pickup} → {d.drop}</Typography></TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={d.priority} sx={{ textTransform: 'capitalize', bgcolor: d.priority === 'high' ? '#FEEAE9' : d.priority === 'express' ? '#FFF7E0' : '#F0F0F0', color: d.priority === 'high' ? '#A82216' : d.priority === 'express' ? '#8A6500' : '#595959' }} />
|
||||
</TableCell>
|
||||
<TableCell><Typography variant="caption">{d.sla}</Typography></TableCell>
|
||||
<TableCell>{d.suggestedRider}</TableCell>
|
||||
<TableCell sx={{ minWidth: 110 }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Box sx={{ width: 56 }}>
|
||||
<LinearProgress variant="determinate" value={d.confidence} color={d.confidence > 90 ? 'success' : 'warning'} sx={{ height: 6, borderRadius: 3 }} />
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>{d.confidence}%</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell><StatusChip status={d.status} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} lg={4}>
|
||||
<MainCard title="Re-optimization Triggers">
|
||||
<Stack divider={<Divider />} spacing={0}>
|
||||
{aiTriggers.map((t) => {
|
||||
const Icon = TRIGGER_ICONS[t.icon] || FiberNewOutlinedIcon;
|
||||
return (
|
||||
<Stack key={t.key} direction="row" spacing={1.5} alignItems="center" sx={{ py: 1.15 }}>
|
||||
<Avatar variant="rounded" sx={{ bgcolor: 'warning.lighter', color: 'warning.dark', width: 36, height: 36 }}>
|
||||
<Icon fontSize="small" />
|
||||
</Avatar>
|
||||
<Typography variant="body2" sx={{ flexGrow: 1 }}>{t.label}</Typography>
|
||||
<Chip size="small" label={t.count} color={t.count > 5 ? 'warning' : 'default'} />
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<MainCard title="MileTruth AI Pipeline">
|
||||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} alignItems="stretch">
|
||||
{aiPipeline.map((stage, i) => (
|
||||
<Box key={stage.stage} sx={{ flex: 1, position: 'relative' }}>
|
||||
<Box sx={{ border: '1px solid', borderColor: 'grey.200', borderRadius: 2, p: 1.75, height: '100%', bgcolor: 'grey.50' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
|
||||
<Box sx={{ width: 22, height: 22, borderRadius: '50%', bgcolor: 'warning.main', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 12, fontWeight: 700 }}>{i + 1}</Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>{stage.stage}</Typography>
|
||||
</Stack>
|
||||
<Stack spacing={0.5}>
|
||||
{stage.items.map((it) => (
|
||||
<Typography key={it} variant="caption" color="text.secondary">• {it}</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
200
src/pages/fleet/Fleet.jsx
Normal file
200
src/pages/fleet/Fleet.jsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { useState } from 'react';
|
||||
import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Tooltip, TextField, MenuItem } from '@mui/material';
|
||||
import ElectricRickshawOutlinedIcon from '@mui/icons-material/ElectricRickshawOutlined';
|
||||
import BatteryChargingFullOutlinedIcon from '@mui/icons-material/BatteryChargingFullOutlined';
|
||||
import EnergySavingsLeafOutlinedIcon from '@mui/icons-material/EnergySavingsLeafOutlined';
|
||||
import HealthAndSafetyOutlinedIcon from '@mui/icons-material/HealthAndSafetyOutlined';
|
||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||
import AddOutlinedIcon from '@mui/icons-material/AddOutlined';
|
||||
import BoltOutlinedIcon from '@mui/icons-material/BoltOutlined';
|
||||
|
||||
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 DonutChart from '@/components/charts/DonutChart';
|
||||
import FormDialog from '@/components/FormDialog';
|
||||
import Toast, { useToast } from '@/components/Toast';
|
||||
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 [rows, setRows] = useState(fleet);
|
||||
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 addVehicle = () => {
|
||||
if (!form.model.trim()) return showToast('Enter a vehicle model', 'warning');
|
||||
const isEv = form.powertrain === 'EV';
|
||||
const id = form.id.trim() || `VH-${isEv ? 'EV' : 'IC'}-${String(100 + rows.length).slice(-3)}`;
|
||||
setRows((r) => [
|
||||
{ id, model: form.model, type: form.type, powertrain: form.powertrain, battery: isEv ? 100 : null, range: isEv ? 110 : 300, health: 100, capacityKg: Number(form.capacityKg) || 500, status: 'idle', rider: '—', hub: form.hub, uptime: 100 },
|
||||
...r
|
||||
]);
|
||||
setForm(BLANK);
|
||||
setOpen(false);
|
||||
showToast(`${form.model} added to fleet`);
|
||||
};
|
||||
|
||||
const mix = [
|
||||
{ label: 'EV', value: fleetSummary.ev, color: '#00A854' },
|
||||
{ label: 'ICE', value: fleetSummary.ice, color: '#8C8C8C' }
|
||||
];
|
||||
const states = [
|
||||
{ label: 'On Trip', value: fleetSummary.onTrip, color: '#00A2AE' },
|
||||
{ label: 'Charging', value: fleetSummary.charging, color: '#1D4ED8' },
|
||||
{ label: 'Idle', value: fleetSummary.idle, color: '#FFBF00' },
|
||||
{ label: 'Maintenance', value: fleetSummary.maintenance, color: '#F04134' }
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Fleet & Rider Operating System"
|
||||
breadcrumbs={[{ label: 'Fleet' }]}
|
||||
action={<Button variant="contained" startIcon={<AddOutlinedIcon />} onClick={() => setOpen(true)}>Add Vehicle</Button>}
|
||||
/>
|
||||
|
||||
<LayerBanner
|
||||
no={4}
|
||||
icon={ElectricRickshawOutlinedIcon}
|
||||
color="#15803D"
|
||||
title="Fleet Management · EV-First"
|
||||
subtitle="Vehicle health, battery monitoring, capacity & load planning across an EV-native fleet."
|
||||
steps={['Vehicle Health', 'Battery Monitoring', 'Capacity Check', 'Load Optimization', 'Multi-Trip Planning']}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Fleet Size" value={fleetSummary.total} icon={LocalShippingOutlinedIcon} color="info" caption={`${fleetSummary.onTrip} on trip now`} /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="EV Share" value={`${fleetSummary.evShare}%`} icon={EnergySavingsLeafOutlinedIcon} color="success" trend={4.2} caption="EV-first target 85%" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Avg Battery" value={`${fleetSummary.avgBattery}%`} icon={BatteryChargingFullOutlinedIcon} color="warning" caption={`${fleetSummary.charging} charging`} /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="CO₂ Saved" value={`${(fleetSummary.co2SavedKg / 1000).toFixed(1)}t`} icon={BoltOutlinedIcon} color="success" trend={9.6} caption="this month" /></Grid>
|
||||
|
||||
<Grid item xs={12} lg={4}>
|
||||
<MainCard title="Powertrain Mix">
|
||||
<Box sx={{ py: 1 }}>
|
||||
<DonutChart data={mix} centerValue={`${fleetSummary.evShare}%`} centerLabel="EV" />
|
||||
</Box>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={4}>
|
||||
<MainCard title="Fleet Status">
|
||||
<Box sx={{ py: 1 }}>
|
||||
<DonutChart data={states} centerValue={fleetSummary.total} centerLabel="Vehicles" />
|
||||
</Box>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={4}>
|
||||
<MainCard title="Operating Health">
|
||||
<Stack spacing={2} sx={{ py: 0.5 }}>
|
||||
<Metric icon={HealthAndSafetyOutlinedIcon} color="#00A854" label="Avg Vehicle Health" value={`${fleetSummary.avgHealth}%`} bar={fleetSummary.avgHealth} />
|
||||
<Metric icon={BatteryChargingFullOutlinedIcon} color="#1D4ED8" label="Avg Battery (EV)" value={`${fleetSummary.avgBattery}%`} bar={fleetSummary.avgBattery} />
|
||||
<Metric icon={EnergySavingsLeafOutlinedIcon} color="#15803D" label="EV Fleet Share" value={`${fleetSummary.evShare}%`} bar={fleetSummary.evShare} />
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<MainCard title="Vehicles" noPadding>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Vehicle</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Battery / Range</TableCell>
|
||||
<TableCell>Health</TableCell>
|
||||
<TableCell align="right">Capacity</TableCell>
|
||||
<TableCell>Assigned</TableCell>
|
||||
<TableCell>Hub</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((v) => (
|
||||
<TableRow key={v.id} hover>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<Avatar variant="rounded" sx={{ bgcolor: v.powertrain === 'EV' ? 'success.lighter' : 'grey.100', color: v.powertrain === 'EV' ? 'success.main' : 'grey.600', width: 36, height: 36 }}>
|
||||
{v.powertrain === 'EV' ? <BoltOutlinedIcon fontSize="small" /> : <LocalShippingOutlinedIcon fontSize="small" />}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="subtitle2">{v.model}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{v.id}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell><Typography variant="caption">{v.type}</Typography></TableCell>
|
||||
<TableCell sx={{ minWidth: 130 }}>
|
||||
{v.powertrain === 'EV' ? (
|
||||
<>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="caption" color="text.secondary">{v.battery}%</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{v.range} km</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" value={v.battery} color={v.battery < 30 ? 'error' : v.battery < 50 ? 'warning' : 'success'} sx={{ height: 6, borderRadius: 3, mt: 0.5 }} />
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="caption" color="text.secondary">Fuel · {v.range} km range</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={`Uptime ${v.uptime}%`}>
|
||||
<Typography variant="subtitle2" sx={{ color: v.health < 80 ? 'warning.main' : 'success.main' }}>{v.health}%</Typography>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell align="right">{v.capacityKg} kg</TableCell>
|
||||
<TableCell>{v.rider}</TableCell>
|
||||
<TableCell><Typography variant="caption" color="text.secondary">{v.hub}</Typography></TableCell>
|
||||
<TableCell><StatusChip status={v.status} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<FormDialog open={open} onClose={() => setOpen(false)} title="Add Vehicle" onSubmit={addVehicle} submitLabel="Add Vehicle">
|
||||
<Grid container spacing={2} sx={{ mt: 0 }}>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Vehicle ID (optional)" value={form.id} onChange={set('id')} placeholder="auto-generated" /></Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" label="Model" value={form.model} onChange={set('model')} placeholder="e.g. Tata Ace EV" /></Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField select fullWidth size="small" label="Powertrain" value={form.powertrain} onChange={set('powertrain')}>
|
||||
<MenuItem value="EV">EV</MenuItem>
|
||||
<MenuItem value="ICE">ICE</MenuItem>
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField select fullWidth size="small" label="Type" value={form.type} onChange={set('type')}>
|
||||
{['EV 2W', 'EV 3W', 'EV 4W', 'ICE 2W', 'ICE 4W'].map((t) => <MenuItem key={t} value={t}>{t}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}><TextField fullWidth size="small" type="number" label="Capacity (kg)" value={form.capacityKg} onChange={set('capacityKg')} /></Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<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>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</FormDialog>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Metric({ icon: Icon, color, label, value, bar }) {
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}>
|
||||
<Icon sx={{ fontSize: 18, color }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ flexGrow: 1 }}>{label}</Typography>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>{value}</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" value={bar} sx={{ height: 6, borderRadius: 3, '& .MuiLinearProgress-bar': { bgcolor: color } }} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
199
src/pages/hubs/HubNetwork.jsx
Normal file
199
src/pages/hubs/HubNetwork.jsx
Normal file
@@ -0,0 +1,199 @@
|
||||
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 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 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 Toast, { useToast } from '@/components/Toast';
|
||||
import { hubs, hubNetworkTypes, lineHauls } from '@/data/mock';
|
||||
|
||||
const BLANK = { name: '', type: 'Micro Hub', city: 'Bengaluru', capacity: '', dock: '' };
|
||||
|
||||
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']}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
<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>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
133
src/pages/integrations/Integrations.jsx
Normal file
133
src/pages/integrations/Integrations.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useState } from 'react';
|
||||
import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, Button, Chip, TextField, MenuItem } from '@mui/material';
|
||||
import HubOutlinedIcon from '@mui/icons-material/HubOutlined';
|
||||
import ApiOutlinedIcon from '@mui/icons-material/ApiOutlined';
|
||||
import BusinessOutlinedIcon from '@mui/icons-material/BusinessOutlined';
|
||||
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
|
||||
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
|
||||
import AddLinkOutlinedIcon from '@mui/icons-material/AddLinkOutlined';
|
||||
|
||||
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 FormDialog from '@/components/FormDialog';
|
||||
import Toast, { useToast } from '@/components/Toast';
|
||||
import { integrations } from '@/data/mock';
|
||||
|
||||
const GROUP_META = {
|
||||
'APIs & Integrations': { icon: ApiOutlinedIcon, color: '#0F766E' },
|
||||
'Enterprise Systems': { icon: BusinessOutlinedIcon, color: '#1D4ED8' },
|
||||
'Payment & Billing': { icon: PaymentsOutlinedIcon, color: '#15803D' },
|
||||
'Partners & Ecosystem': { icon: GroupsOutlinedIcon, color: '#EA580C' }
|
||||
};
|
||||
const BLANK = { name: '', group: 'APIs & Integrations', desc: '' };
|
||||
|
||||
export default function Integrations() {
|
||||
const [rows, setRows] = useState(integrations);
|
||||
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 addIntegration = () => {
|
||||
if (!form.name.trim()) return showToast('Enter an integration name', 'warning');
|
||||
const meta = GROUP_META[form.group] || {};
|
||||
setRows((r) => [...r, { name: form.name, group: form.group, desc: form.desc || 'Custom connector', status: 'pending', calls: '—', icon: form.group === 'Payment & Billing' ? 'pay' : form.group === 'Enterprise Systems' ? 'erp' : form.group === 'Partners & Ecosystem' ? 'partner' : 'api' }]);
|
||||
setForm(BLANK);
|
||||
setOpen(false);
|
||||
showToast(`${form.name} added — pending connection`);
|
||||
};
|
||||
|
||||
const groups = [...new Set(rows.map((i) => i.group))];
|
||||
const connected = rows.filter((i) => i.status === 'connected').length;
|
||||
const issues = rows.filter((i) => i.status !== 'connected').length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Integrations & Ecosystem"
|
||||
breadcrumbs={[{ label: 'Integrations' }]}
|
||||
action={<Button variant="contained" startIcon={<AddLinkOutlinedIcon />} onClick={() => setOpen(true)}>Add Integration</Button>}
|
||||
/>
|
||||
|
||||
<LayerBanner
|
||||
no={8}
|
||||
icon={HubOutlinedIcon}
|
||||
color="#0F766E"
|
||||
title="Integrations & Ecosystem"
|
||||
subtitle="One system that plugs into partner APIs, ERP/WMS, payments and the broader logistics ecosystem."
|
||||
steps={['APIs & Integrations', 'Enterprise Systems', 'Payment & Billing', 'Partners & Ecosystem']}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 0.5 }}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Connectors" value={rows.length} icon={HubOutlinedIcon} color="info" caption="across 4 categories" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Connected" value={connected} icon={ApiOutlinedIcon} color="success" caption="healthy" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Attention" value={issues} icon={BusinessOutlinedIcon} color="warning" caption="degraded / pending" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="API Calls" value="7.2M" icon={PaymentsOutlinedIcon} color="primary" trend={5.8} caption="per day" /></Grid>
|
||||
</Grid>
|
||||
|
||||
{groups.map((g) => {
|
||||
const meta = GROUP_META[g] || { icon: HubOutlinedIcon, color: '#C01227' };
|
||||
const GIcon = meta.icon;
|
||||
return (
|
||||
<Box key={g} sx={{ mt: 2.5 }}>
|
||||
<MainCard
|
||||
title={
|
||||
<Stack direction="row" spacing={1.25} alignItems="center">
|
||||
<Avatar variant="rounded" sx={{ bgcolor: hexA(meta.color, 0.12), color: meta.color, width: 34, height: 34 }}>
|
||||
<GIcon fontSize="small" />
|
||||
</Avatar>
|
||||
<Typography variant="h5">{g}</Typography>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Grid container spacing={2}>
|
||||
{rows.filter((i) => i.group === g).map((i) => (
|
||||
<Grid item xs={12} sm={6} lg={4} key={i.name}>
|
||||
<Card variant="outlined" sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start">
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>{i.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{i.desc}</Typography>
|
||||
</Box>
|
||||
<StatusChip status={i.status} />
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mt: 1.5 }}>
|
||||
<Chip size="small" label={i.calls} sx={{ bgcolor: 'grey.100' }} />
|
||||
<Button size="small" onClick={() => showToast(i.status === 'pending' ? `Connecting ${i.name}…` : `${i.name} settings opened`)}>
|
||||
{i.status === 'pending' ? 'Connect' : 'Configure'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</MainCard>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
<FormDialog open={open} onClose={() => setOpen(false)} title="Add Integration" onSubmit={addIntegration} submitLabel="Add">
|
||||
<Grid container spacing={2} sx={{ mt: 0 }}>
|
||||
<Grid item xs={12}><TextField fullWidth size="small" label="Integration Name" value={form.name} onChange={set('name')} placeholder="e.g. Shopify, Freshdesk" /></Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField select fullWidth size="small" label="Category" value={form.group} onChange={set('group')}>
|
||||
{Object.keys(GROUP_META).map((g) => <MenuItem key={g} value={g}>{g}</MenuItem>)}
|
||||
</TextField>
|
||||
</Grid>
|
||||
<Grid item xs={12}><TextField fullWidth size="small" label="Description" value={form.desc} onChange={set('desc')} placeholder="What does this connector do?" /></Grid>
|
||||
</Grid>
|
||||
</FormDialog>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const hexA = (hex, a) => {
|
||||
const n = parseInt(hex.replace('#', ''), 16);
|
||||
return `rgba(${n >> 16}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
|
||||
};
|
||||
60
src/pages/network/ThreeMile.jsx
Normal file
60
src/pages/network/ThreeMile.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Grid, Stack, Typography, Box, Button } from '@mui/material';
|
||||
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
|
||||
import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined';
|
||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import StatCard from '@/components/StatCard';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import LayerBanner from '@/components/LayerBanner';
|
||||
import ThreeMileStrip from '@/components/ThreeMileStrip';
|
||||
import MapPlaceholder from '@/components/MapPlaceholder';
|
||||
import Toast, { useToast } from '@/components/Toast';
|
||||
import { threeMile } from '@/data/mock';
|
||||
|
||||
export default function ThreeMile() {
|
||||
const [toast, showToast] = useToast();
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Three-Mile Network"
|
||||
breadcrumbs={[{ label: 'Three-Mile Network' }]}
|
||||
action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => showToast('Three-mile report exported')}>Export</Button>}
|
||||
/>
|
||||
|
||||
<LayerBanner
|
||||
no={0}
|
||||
icon={RouteOutlinedIcon}
|
||||
color="#C01227"
|
||||
title="One Connected System · First → Mid → Last Mile"
|
||||
subtitle="Doormile unifies first-mile pickup, mid-mile line-haul and last-mile delivery into one connected flow."
|
||||
steps={['Origin to Hub', 'Hub to Hub Transit', 'Hub to Doorstep']}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 2.5 }}>
|
||||
<Grid item xs={12} sm={4}><StatCard title="First Mile · Pickups" value={threeMile[0].metric} icon={WarehouseOutlinedIcon} color="warning" caption={`${threeMile[0].onTime}% on-time`} /></Grid>
|
||||
<Grid item xs={12} sm={4}><StatCard title="Mid Mile · Line-Hauls" value={threeMile[1].metric} icon={LocalShippingOutlinedIcon} color="info" caption={`${threeMile[1].onTime}% on-time`} /></Grid>
|
||||
<Grid item xs={12} sm={4}><StatCard title="Last Mile · Out for Delivery" value={threeMile[2].metric} icon={HomeOutlinedIcon} color="primary" caption={`${threeMile[2].onTime}% on-time`} /></Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ mb: 2.5 }}>
|
||||
<ThreeMileStrip />
|
||||
</Box>
|
||||
|
||||
<MainCard title="Connected Miles — Live Flow">
|
||||
<MapPlaceholder
|
||||
height={340}
|
||||
label="First → Mid → Last Mile"
|
||||
pins={[
|
||||
{ x: '14%', y: '74%', label: 'Origin', color: '#EA580C' },
|
||||
{ x: '44%', y: '46%', label: 'Hub', color: '#0E7C7B' },
|
||||
{ x: '82%', y: '20%', label: 'Doorstep', color: '#1D4ED8' }
|
||||
]}
|
||||
/>
|
||||
</MainCard>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
234
src/pages/tracking/LiveTracking.jsx
Normal file
234
src/pages/tracking/LiveTracking.jsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { 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 AltRouteOutlinedIcon from '@mui/icons-material/AltRouteOutlined';
|
||||
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
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 FormDialog from '@/components/FormDialog';
|
||||
import Toast, { useToast } from '@/components/Toast';
|
||||
import { executionStages, executionFeed, ridersLive, orderTimeline } from '@/data/mock';
|
||||
|
||||
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 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>}
|
||||
/>
|
||||
|
||||
<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']}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{/* Share tracking */}
|
||||
<FormDialog open={share} onClose={() => setShare(false)} title="Share Live Tracking" hideActions>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
|
||||
Anyone with this link can follow the shipment in real time.
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
value={trackLink}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Button size="small" startIcon={<ContentCopyOutlinedIcon fontSize="small" />} onClick={copyLink}>Copy</Button>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Stack direction="row" spacing={1.5} sx={{ mt: 2 }}>
|
||||
<Button variant="contained" onClick={() => { showToast('Tracking sent via SMS'); setShare(false); }}>Send SMS</Button>
|
||||
<Button variant="outlined" onClick={() => { showToast('Tracking sent on WhatsApp'); setShare(false); }}>WhatsApp</Button>
|
||||
</Stack>
|
||||
</FormDialog>
|
||||
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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" />;
|
||||
}
|
||||
174
src/pages/tracking/ShipmentJourney.jsx
Normal file
174
src/pages/tracking/ShipmentJourney.jsx
Normal file
@@ -0,0 +1,174 @@
|
||||
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 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 PageHeader from '@/components/PageHeader';
|
||||
import MainCard from '@/components/MainCard';
|
||||
import MapPlaceholder from '@/components/MapPlaceholder';
|
||||
import Toast, { useToast } from '@/components/Toast';
|
||||
import { shipmentJourney as j } from '@/data/mock';
|
||||
|
||||
const HOP_ICONS = { order: ReceiptLongOutlinedIcon, agent: TwoWheelerOutlinedIcon, hub: WarehouseOutlinedIcon, truck: LocalShippingOutlinedIcon, done: HomeOutlinedIcon };
|
||||
|
||||
export default function ShipmentJourney() {
|
||||
const [toast, showToast] = useToast();
|
||||
const doneCount = j.hops.filter((h) => h.status === 'done').length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Shipment Journey"
|
||||
breadcrumbs={[{ label: 'Live Tracking', to: '/tracking' }, { label: j.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>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Summary */}
|
||||
<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 }}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: 'grey.900' }}>{j.from.city}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{j.from.area}</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%)' }} />
|
||||
</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>
|
||||
</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}>
|
||||
<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 }} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2.5}>
|
||||
{/* Hop-by-hop timeline */}
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
{/* Map + live monitoring */}
|
||||
<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>
|
||||
|
||||
<MainCard
|
||||
title={
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Avatar variant="rounded" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', width: 30, height: 30 }}><AutoAwesomeOutlinedIcon sx={{ fontSize: 18 }} /></Avatar>
|
||||
<Typography variant="h5">Live Monitoring & Reroute</Typography>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
<Stack divider={<Divider />} spacing={0}>
|
||||
{j.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 }} />}
|
||||
</Avatar>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>{e.title}</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{e.detail}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{e.time}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Toast {...toast} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
169
src/pages/trust/TrustCompliance.jsx
Normal file
169
src/pages/trust/TrustCompliance.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { useState } from 'react';
|
||||
import { Grid, Card, CardContent, Stack, Typography, Box, Avatar, LinearProgress, Table, TableBody, TableCell, TableHead, TableRow, Button, Divider } from '@mui/material';
|
||||
import VerifiedUserOutlinedIcon from '@mui/icons-material/VerifiedUserOutlined';
|
||||
import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined';
|
||||
import GppMaybeOutlinedIcon from '@mui/icons-material/GppMaybeOutlined';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import LinkOutlinedIcon from '@mui/icons-material/LinkOutlined';
|
||||
import GavelOutlinedIcon from '@mui/icons-material/GavelOutlined';
|
||||
import SecurityOutlinedIcon from '@mui/icons-material/SecurityOutlined';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
|
||||
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 FormDialog from '@/components/FormDialog';
|
||||
import { trustChecks, trustQueue } from '@/data/mock';
|
||||
|
||||
const ICONS = {
|
||||
kyc: VerifiedUserOutlinedIcon,
|
||||
id: BadgeOutlinedIcon,
|
||||
fraud: GppMaybeOutlinedIcon,
|
||||
tamper: LockOutlinedIcon,
|
||||
custody: LinkOutlinedIcon,
|
||||
compliance: GavelOutlinedIcon
|
||||
};
|
||||
|
||||
export default function TrustCompliance() {
|
||||
const [audit, setAudit] = useState(false);
|
||||
const cleared = trustQueue.filter((t) => t.status === 'cleared').length;
|
||||
const review = trustQueue.filter((t) => t.status === 'review').length;
|
||||
const hold = trustQueue.filter((t) => t.status === 'hold').length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Trust & Identity Layer"
|
||||
breadcrumbs={[{ label: 'Trust & Identity' }]}
|
||||
action={<Button variant="outlined" startIcon={<FileDownloadOutlinedIcon />} onClick={() => setAudit(true)}>Audit Log</Button>}
|
||||
/>
|
||||
|
||||
<LayerBanner
|
||||
no={2}
|
||||
icon={SecurityOutlinedIcon}
|
||||
color="#5B5BD6"
|
||||
title="Trust, Security & Compliance"
|
||||
subtitle="Every shipment is verified, screened and sealed before it moves."
|
||||
steps={['KYC Verification', 'ID Verification', 'Fraud Detection', 'Tamper Protection', 'Chain of Custody', 'Compliance Engine']}
|
||||
/>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mb: 0.5 }}>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cleared Rate" value="99.2%" icon={VerifiedUserOutlinedIcon} color="success" trend={0.4} caption="this week" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="In Review" value={review} icon={BadgeOutlinedIcon} color="warning" caption="awaiting verification" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="On Hold" value={hold} icon={GppMaybeOutlinedIcon} color="error" caption="fraud flags" /></Grid>
|
||||
<Grid item xs={12} sm={6} lg={3}><StatCard title="Cleared Today" value={cleared} icon={LockOutlinedIcon} color="info" caption="seals logged" /></Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2.5} sx={{ mt: 0 }}>
|
||||
{trustChecks.map((c) => {
|
||||
const Icon = ICONS[c.icon] || VerifiedUserOutlinedIcon;
|
||||
return (
|
||||
<Grid item xs={12} sm={6} lg={4} key={c.key}>
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mb: 1.5 }}>
|
||||
<Avatar variant="rounded" sx={{ bgcolor: 'primary.lighter', color: 'primary.main', width: 44, height: 44 }}>
|
||||
<Icon fontSize="small" />
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>{c.title}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{c.desc}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between" sx={{ mb: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary">Pass rate</Typography>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'grey.900' }}>{c.passRate}%</Typography>
|
||||
</Stack>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={c.passRate}
|
||||
sx={{ height: 6, borderRadius: 3, bgcolor: 'grey.100', '& .MuiLinearProgress-bar': { bgcolor: 'grey.800' } }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
{c.pending} pending review
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ mt: 2.5 }}>
|
||||
<MainCard title="Verification & Fraud Queue" noPadding>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Shipment</TableCell>
|
||||
<TableCell>Entity</TableCell>
|
||||
<TableCell>Check</TableCell>
|
||||
<TableCell>Risk</TableCell>
|
||||
<TableCell>Risk Score</TableCell>
|
||||
<TableCell>Signal</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{trustQueue.map((r) => (
|
||||
<TableRow key={r.id} hover>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{r.id}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{r.entity}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{r.type}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{r.check}</TableCell>
|
||||
<TableCell><StatusChip status={r.risk} /></TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Box sx={{ width: 60 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={r.score}
|
||||
color={r.score > 60 ? 'error' : r.score > 35 ? 'warning' : 'success'}
|
||||
sx={{ height: 6, borderRadius: 3 }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>{r.score}</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell><Typography variant="caption" color="text.secondary">{r.flagged}</Typography></TableCell>
|
||||
<TableCell><StatusChip status={r.status} /></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</MainCard>
|
||||
</Box>
|
||||
|
||||
<FormDialog open={audit} onClose={() => setAudit(false)} title="Chain-of-Custody Audit Log" maxWidth="md" hideActions>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
|
||||
Immutable, tamper-evident log of every verification & custody event.
|
||||
</Typography>
|
||||
<Stack divider={<Divider />} spacing={0}>
|
||||
{trustQueue.map((r, i) => (
|
||||
<Stack key={r.id} direction="row" spacing={1.5} alignItems="center" sx={{ py: 1.1 }}>
|
||||
<Box sx={{ width: 70 }}>
|
||||
<Typography variant="caption" color="text.secondary">10:{42 - i}:0{i}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="subtitle2">{r.id} · {r.check}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{r.entity} — {r.flagged}</Typography>
|
||||
</Box>
|
||||
<StatusChip status={r.status} />
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: 'grey.500', display: { xs: 'none', sm: 'block' } }}>
|
||||
0x{(r.id.length * 7 + r.score).toString(16)}a{i}f3
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</FormDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const hexA = (hex, a) => {
|
||||
const n = parseInt(hex.replace('#', ''), 16);
|
||||
return `rgba(${n >> 16}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
|
||||
};
|
||||
Reference in New Issue
Block a user