updated the ui for the doormile
This commit is contained in:
@@ -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