Initial commit
This commit is contained in:
585
src/pages/Dashboard.jsx
Normal file
585
src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,585 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardContent,
|
||||
Typography,
|
||||
Box,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
IconButton,
|
||||
Avatar,
|
||||
Button,
|
||||
Divider,
|
||||
Popover
|
||||
} from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner';
|
||||
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import ElectricBoltIcon from '@mui/icons-material/ElectricBolt';
|
||||
import DynamicFeedIcon from '@mui/icons-material/DynamicFeed';
|
||||
import SpeedIcon from '@mui/icons-material/Speed';
|
||||
import LocalOfferIcon from '@mui/icons-material/LocalOffer';
|
||||
import DeliveryDiningIcon from '@mui/icons-material/DeliveryDining';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
|
||||
import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';
|
||||
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
|
||||
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
|
||||
|
||||
const BRAND = '#C01227';
|
||||
const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
|
||||
// ── Doormile-themed range calendar ───────────────────────────────────────────────
|
||||
// Click a day to set the start, click again to set the end (auto-swaps if reversed).
|
||||
// In-range days get a soft red wash; the two endpoints are solid brand red.
|
||||
function RangeCalendar({ from, to, maxDate, onSelect }) {
|
||||
const [view, setView] = useState(dayjs(to || from || undefined).startOf('month'));
|
||||
const [anchorDate, setAnchorDate] = useState(null); // first click while picking a new range
|
||||
|
||||
const start = from ? dayjs(from) : null;
|
||||
const end = to ? dayjs(to) : null;
|
||||
const max = maxDate ? dayjs(maxDate) : null;
|
||||
|
||||
const gridStart = view.startOf('month').subtract(view.startOf('month').day(), 'day');
|
||||
const cells = Array.from({ length: 42 }, (_, i) => gridStart.add(i, 'day'));
|
||||
|
||||
const handleDay = (d) => {
|
||||
if (!anchorDate) {
|
||||
setAnchorDate(d);
|
||||
onSelect(d.format(DATE_FMT), d.format(DATE_FMT));
|
||||
} else {
|
||||
const a = anchorDate;
|
||||
const lo = d.isBefore(a) ? d : a;
|
||||
const hi = d.isBefore(a) ? a : d;
|
||||
onSelect(lo.format(DATE_FMT), hi.format(DATE_FMT));
|
||||
setAnchorDate(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 2, width: 320 }}>
|
||||
{/* Month header */}
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 1.5 }}>
|
||||
<IconButton size="small" onClick={() => setView((v) => v.subtract(1, 'month'))} sx={{ color: '#5F6368' }}>
|
||||
<ChevronLeftRoundedIcon />
|
||||
</IconButton>
|
||||
<Typography sx={{ fontWeight: 800, color: '#212529' }}>{view.format('MMMM YYYY')}</Typography>
|
||||
<IconButton size="small" onClick={() => setView((v) => v.add(1, 'month'))} sx={{ color: '#5F6368' }}>
|
||||
<ChevronRightRoundedIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
|
||||
{/* Weekday labels */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', mb: 0.5 }}>
|
||||
{DAY_LABELS.map((d, i) => (
|
||||
<Typography key={i} align="center" sx={{ fontSize: '0.72rem', fontWeight: 700, color: '#9AA0A6', py: 0.5 }}>
|
||||
{d}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Days */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', rowGap: 0.25 }}>
|
||||
{cells.map((d) => {
|
||||
const inMonth = d.month() === view.month();
|
||||
const isStart = start && d.isSame(start, 'day');
|
||||
const isEnd = end && d.isSame(end, 'day');
|
||||
const isEndpoint = isStart || isEnd;
|
||||
const inRange = start && end && d.isAfter(start, 'day') && d.isBefore(end, 'day');
|
||||
const isToday = d.isSame(dayjs(), 'day');
|
||||
const disabled = max && d.isAfter(max, 'day');
|
||||
return (
|
||||
<Box key={d.format(DATE_FMT)} sx={{ display: 'flex', justifyContent: 'center', bgcolor: inRange ? alpha(BRAND, 0.08) : 'transparent',
|
||||
borderTopLeftRadius: isStart ? 8 : 0, borderBottomLeftRadius: isStart ? 8 : 0,
|
||||
borderTopRightRadius: isEnd ? 8 : 0, borderBottomRightRadius: isEnd ? 8 : 0 }}>
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => handleDay(d)}
|
||||
sx={{
|
||||
width: 36, height: 36, m: '2px', border: 'none', cursor: disabled ? 'default' : 'pointer',
|
||||
borderRadius: 2, fontSize: '0.85rem', fontFamily: 'inherit',
|
||||
fontWeight: isEndpoint ? 800 : 600,
|
||||
color: disabled ? '#CED4DA' : isEndpoint ? '#fff' : inMonth ? '#212529' : '#B9BEC4',
|
||||
bgcolor: isEndpoint ? BRAND : 'transparent',
|
||||
boxShadow: isToday && !isEndpoint ? `inset 0 0 0 1.5px ${BRAND}` : 'none',
|
||||
transition: 'background-color .15s',
|
||||
'&:hover': { bgcolor: disabled ? 'transparent' : isEndpoint ? '#9E0E20' : alpha(BRAND, 0.12) }
|
||||
}}
|
||||
>
|
||||
{d.date()}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Per-day snapshot for a single hub day. `cumulative` metrics add up across the
|
||||
// selected date range; the rest are "right now" figures that stay as a live count.
|
||||
const STAT_DEFS = [
|
||||
{ label: 'Total Parcels', base: 3142, cumulative: true, icon: DynamicFeedIcon, color: '#1A73E8', sub: 'Handled in range' },
|
||||
{ label: 'Picked Up Locally', base: 1482, cumulative: true, icon: LocalOfferIcon, color: '#1E8E3E', sub: 'Collected by milers' },
|
||||
{ label: 'From Other Cities', base: 1660, cumulative: true, icon: LocalShippingIcon, color: '#1A73E8', sub: 'Arrived by truck' },
|
||||
{ label: 'Ready for Delivery', base: 840, cumulative: false, icon: AssignmentIcon, color: '#00A854', sub: 'Sorted for local areas' },
|
||||
{ label: 'Ready to Transfer', base: 1120, cumulative: false, icon: LocalShippingIcon, color: '#8E24AA', sub: 'Going to other cities' },
|
||||
{ label: 'Out for Delivery', base: 620, cumulative: false, icon: DeliveryDiningIcon, color: '#F29900', sub: 'With milers right now' },
|
||||
{ label: 'Needs Checking', base: 48, cumulative: true, icon: WarningAmberIcon, color: '#D93025', sub: 'Damaged or unclear' },
|
||||
{ label: 'Returns', base: 12, cumulative: true, icon: WarningAmberIcon, color: '#F29900', sub: 'Going back to sender' },
|
||||
{ label: 'Available Milers', base: 24, cumulative: false, icon: InfoOutlinedIcon, color: '#1A73E8', sub: 'Free or on duty' },
|
||||
{ label: 'Batches Going Out', base: 8, cumulative: true, icon: LocalShippingIcon, color: '#1E8E3E', sub: 'Sent in range' }
|
||||
];
|
||||
|
||||
const DATE_FMT = 'YYYY-MM-DD';
|
||||
|
||||
export default function Dashboard() {
|
||||
const today = dayjs().format(DATE_FMT);
|
||||
const weekAgo = dayjs().subtract(6, 'day').format(DATE_FMT);
|
||||
const [range, setRange] = useState({ from: weekAgo, to: today });
|
||||
const [calAnchor, setCalAnchor] = useState(null);
|
||||
|
||||
// Inclusive day count for the chosen window (min 1); drives cumulative metrics.
|
||||
const dayCount = useMemo(() => {
|
||||
const from = dayjs(range.from);
|
||||
const to = dayjs(range.to);
|
||||
if (!from.isValid() || !to.isValid() || to.isBefore(from)) return 1;
|
||||
return to.diff(from, 'day') + 1;
|
||||
}, [range]);
|
||||
|
||||
const invalidRange = dayjs(range.to).isBefore(dayjs(range.from));
|
||||
|
||||
const applyPreset = (days) => {
|
||||
setRange({ from: dayjs().subtract(days - 1, 'day').format(DATE_FMT), to: today });
|
||||
};
|
||||
const isPreset = (days) =>
|
||||
range.to === today && range.from === dayjs().subtract(days - 1, 'day').format(DATE_FMT);
|
||||
|
||||
const stats = useMemo(
|
||||
() =>
|
||||
STAT_DEFS.map((s) => ({
|
||||
...s,
|
||||
value: (s.cumulative ? s.base * dayCount : s.base).toLocaleString('en-IN')
|
||||
})),
|
||||
[dayCount]
|
||||
);
|
||||
|
||||
const rangeLabel =
|
||||
dayCount === 1
|
||||
? dayjs(range.from).format('DD MMM YYYY')
|
||||
: `${dayjs(range.from).format('DD MMM')} – ${dayjs(range.to).format('DD MMM YYYY')} · ${dayCount} days`;
|
||||
|
||||
const incomingVehicles = [
|
||||
{ id: 'Truck MH-04-8822', origin: 'Mumbai Hub', estTime: 'Arrived (Bay 4)', status: 'Unloading', progress: 85, color: 'success' },
|
||||
{ id: 'Truck RJ-14-1049', origin: 'Jaipur Hub', estTime: '15 min away', status: 'Expected', progress: 0, color: 'info' },
|
||||
{ id: 'Truck KA-03-0284', origin: 'Bengaluru Hub', estTime: '1.5 hrs away', status: 'On the way', progress: 0, color: 'default' }
|
||||
];
|
||||
|
||||
const recentActivity = [
|
||||
{ time: '11:24 AM', type: 'inbound', text: 'Received 142 parcels from the Mumbai truck' },
|
||||
{ time: '11:15 AM', type: 'dispatch', text: 'Batch BATCH-9281 sent out with miler Deepak (West Delhi)' },
|
||||
{ time: '10:50 AM', type: 'exception', text: 'Parcel DM-1005 put on hold (damaged label)' },
|
||||
{ time: '10:30 AM', type: 'sorting', text: 'Cold room temperature checked — all good (4.2°C)' }
|
||||
];
|
||||
|
||||
const activeRoutes = [
|
||||
{ zone: 'West Delhi (Dwarka)', packages: 145, riders: 4, status: 'Active' },
|
||||
{ zone: 'South Delhi (Saket)', packages: 210, riders: 6, status: 'Active' },
|
||||
{ zone: 'East Delhi (Mayur Vihar)', packages: 98, riders: 3, status: 'Need Milers' },
|
||||
{ zone: 'North Delhi (Rohini)', packages: 122, riders: 4, status: 'Active' }
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Title + date-range filter, all on one line */}
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'stretch', md: 'center' }}
|
||||
sx={{ mb: 2.5 }} // Reduced from mb: 4 to tighten vertical spacing
|
||||
gap={1.5}
|
||||
>
|
||||
{/* Left Title Section */}
|
||||
<Box sx={{ flexShrink: 0 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: '#1E293B', letterSpacing: '-0.02em', mb: 0.5 }}>
|
||||
Delhi Hub
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#64748B', fontSize: '0.825rem' }}>
|
||||
Showing hub activity for{' '}
|
||||
<Box component="span" sx={{ fontWeight: 600, color: '#0F172A' }}>
|
||||
{rangeLabel}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Right Controls Section */}
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
flexWrap="wrap"
|
||||
useFlexGap
|
||||
sx={{ flexGrow: 1, minWidth: 0, justifyContent: { xs: 'flex-start', md: 'flex-end' } }}
|
||||
>
|
||||
{/* Refresh Button */}
|
||||
<IconButton
|
||||
color="primary"
|
||||
sx={{
|
||||
border: '1px solid #E2E8F0',
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 2,
|
||||
bgcolor: '#ffffff',
|
||||
color: '#64748B',
|
||||
'&:hover': { bgcolor: '#F8FAFC', color: '#0F172A' }
|
||||
}}
|
||||
>
|
||||
<RefreshIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
|
||||
{/* Quick Presets */}
|
||||
{[
|
||||
{ label: 'Today', days: 1 },
|
||||
{ label: 'Last 7 days', days: 7 },
|
||||
{ label: 'Last 30 days', days: 30 }
|
||||
].map((p) => {
|
||||
const active = isPreset(p.days);
|
||||
return (
|
||||
<Chip
|
||||
key={p.label}
|
||||
label={p.label}
|
||||
onClick={() => applyPreset(p.days)}
|
||||
sx={{
|
||||
height: 36,
|
||||
fontSize: '0.825rem',
|
||||
fontWeight: 600,
|
||||
borderRadius: 2,
|
||||
bgcolor: active ? BRAND : '#F1F5F9',
|
||||
color: active ? '#ffffff' : '#475569',
|
||||
boxShadow: active ? '0 4px 10px rgba(192,18,39,0.15)' : 'none',
|
||||
'& .MuiChip-label': { px: 1.5 },
|
||||
'&:hover': { bgcolor: active ? '#9E0E20' : '#E2E8F0' }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Range Trigger */}
|
||||
<Button
|
||||
onClick={(e) => setCalAnchor(e.currentTarget)}
|
||||
variant="outlined"
|
||||
startIcon={<CalendarTodayOutlinedIcon sx={{ fontSize: 16 }} />}
|
||||
sx={{
|
||||
height: 36,
|
||||
px: 1.5,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.825rem',
|
||||
color: '#334155',
|
||||
borderColor: invalidRange ? '#EF4444' : '#E2E8F0',
|
||||
bgcolor: '#ffffff',
|
||||
justifyContent: 'flex-start',
|
||||
minWidth: 210, // Compressed from 250px to remove dead layout space
|
||||
'&:hover': { borderColor: BRAND, bgcolor: alpha(BRAND, 0.02) }
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ color: '#334155' }}>
|
||||
<Box component="span">{dayjs(range.from).format('DD MMM YYYY')}</Box>
|
||||
<ArrowRightAltRoundedIcon sx={{ fontSize: 16, color: '#94A3B8' }} />
|
||||
<Box component="span">{dayjs(range.to).format('DD MMM YYYY')}</Box>
|
||||
</Stack>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Themed calendar popover */}
|
||||
<Popover
|
||||
open={Boolean(calAnchor)}
|
||||
anchorEl={calAnchor}
|
||||
onClose={() => setCalAnchor(null)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
PaperProps={{ sx: { mt: 1, borderRadius: 3, border: '1px solid #ECEEF1', boxShadow: '0 12px 40px rgba(0,0,0,0.14)', overflow: 'hidden' } }}
|
||||
>
|
||||
<Box sx={{ px: 2, pt: 2 }}>
|
||||
<Typography sx={{ fontWeight: 800, color: '#212529' }}>Select date range</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{dayjs(range.from).format('DD MMM')} – {dayjs(range.to).format('DD MMM YYYY')} · {dayCount} {dayCount === 1 ? 'day' : 'days'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<RangeCalendar
|
||||
from={range.from}
|
||||
to={range.to}
|
||||
maxDate={today}
|
||||
onSelect={(from, to) => setRange({ from, to })}
|
||||
/>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ px: 2, py: 1.25 }}>
|
||||
<Button size="small" onClick={() => applyPreset(1)} sx={{ textTransform: 'none', color: '#5F6368', fontWeight: 700 }}>
|
||||
Reset to today
|
||||
</Button>
|
||||
<Button size="small" variant="contained" onClick={() => setCalAnchor(null)}
|
||||
sx={{ textTransform: 'none', fontWeight: 700, bgcolor: BRAND, borderRadius: 2, '&:hover': { bgcolor: '#9E0E20' } }}>
|
||||
Done
|
||||
</Button>
|
||||
</Stack>
|
||||
</Popover>
|
||||
|
||||
{/* Metrics Row — CSS grid with minmax(0,1fr) never overflows on mobile */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: {
|
||||
xs: 'repeat(2, minmax(0, 1fr))',
|
||||
sm: 'repeat(3, minmax(0, 1fr))',
|
||||
md: 'repeat(4, minmax(0, 1fr))',
|
||||
lg: 'repeat(5, minmax(0, 1fr))'
|
||||
},
|
||||
gap: { xs: 1.5, sm: 2, md: 3 },
|
||||
mb: { xs: 3, md: 5 }
|
||||
}}
|
||||
>
|
||||
{stats.map((s) => {
|
||||
const Icon = s.icon;
|
||||
return (
|
||||
<Card key={s.label} sx={{ height: '100%', position: 'relative', overflow: 'hidden', borderRadius: 2, boxShadow: '0px 2px 14px rgba(38,38,38,0.03)', border: '1px solid #ECEEF1' }}>
|
||||
<CardContent sx={{ p: { xs: 2, sm: 2.5 }, '&:last-child': { pb: { xs: 2, sm: 2.5 } } }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1.75} sx={{ mb: 1.75 }}>
|
||||
<Avatar sx={{ bgcolor: s.color + '15', color: s.color, width: { xs: 40, sm: 46 }, height: { xs: 40, sm: 46 }, borderRadius: 2, flexShrink: 0 }}>
|
||||
<Icon fontSize="small" />
|
||||
</Avatar>
|
||||
<Typography sx={{ color: '#868E96', fontWeight: 700, lineHeight: 1.3, fontSize: { xs: '0.72rem', sm: '0.78rem' }, textTransform: 'uppercase', letterSpacing: 0.6 }}>
|
||||
{s.label}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography sx={{ fontWeight: 800, color: '#212529', fontSize: { xs: '1.5rem', sm: '1.9rem' }, lineHeight: 1.15, mb: 0.75 }}>
|
||||
{s.value}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ display: 'block', color: '#6c757d', fontWeight: 500 }}>
|
||||
{s.sub}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* Operations Grid */}
|
||||
<Grid container spacing={{ xs: 2, md: 3.5 }}>
|
||||
|
||||
{/* Sorting Progress & Incoming Vehicles */}
|
||||
<Grid size={{ xs: 12, md: 7, lg: 8 }} >
|
||||
<Stack spacing={3.5}>
|
||||
|
||||
{/* Sorting Station Overview */}
|
||||
<Card>
|
||||
<CardHeader title="Sorting Progress" subheader="How many parcels we've sorted today" />
|
||||
<Divider />
|
||||
<CardContent sx={{ pt: 3 }}>
|
||||
<Box sx={{ mb: 3.5 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }} gap={1} flexWrap="wrap">
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>Today's target — 2,000 parcels</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 700, whiteSpace: 'nowrap' }}>74% done</Typography>
|
||||
</Stack>
|
||||
<LinearProgress variant="determinate" value={74.1} sx={{ height: 10, borderRadius: 2, bgcolor: 'grey.200' }} />
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 700 }}>Trucks Arriving</Typography>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ overflowX: 'auto' }}>
|
||||
<Table size="small" sx={{ minWidth: 480 }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Truck</TableCell>
|
||||
<TableCell>Coming From</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Unloaded</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{incomingVehicles.map((v) => (
|
||||
<TableRow key={v.id}>
|
||||
<TableCell sx={{ fontWeight: 700 }}>{v.id}</TableCell>
|
||||
<TableCell>{v.origin}</TableCell>
|
||||
<TableCell>
|
||||
<Chip size="small" label={v.status} color={v.color} variant={v.status === 'Expected' ? 'outlined' : 'filled'} />
|
||||
</TableCell>
|
||||
<TableCell sx={{ minWidth: 150 }}>
|
||||
{v.progress > 0 ? (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<LinearProgress variant="determinate" value={v.progress} sx={{ flex: 1, height: 6, borderRadius: 2 }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600 }}>{v.progress}%</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Typography variant="caption" color="text.secondary">—</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Online Research: Heavy Traffic / Peak Season Control Center */}
|
||||
<Card sx={{ borderLeft: '1px solid', borderColor: 'primary.main' }}>
|
||||
<CardHeader
|
||||
title="Handling Busy Days"
|
||||
subheader="Simple ways the hub keeps up when parcels pile up"
|
||||
avatar={
|
||||
<Avatar sx={{ bgcolor: 'primary.lighter', color: 'primary.main', borderRadius: 2 }}>
|
||||
<SpeedIcon />
|
||||
</Avatar>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent sx={{ pt: 3 }}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, sm: 4 }} >
|
||||
<Box sx={{ p: 2, bgcolor: 'grey.50', borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
|
||||
<ElectricBoltIcon color="success" sx={{ fontSize: 18 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>Fast Lane</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Urgent parcels skip the shelves and go straight from arrival to the outgoing trucks.
|
||||
</Typography>
|
||||
<Chip label="On" color="success" size="small" variant="outlined" />
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 4 }} >
|
||||
<Box sx={{ p: 2, bgcolor: 'grey.50', borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
|
||||
<DynamicFeedIcon color="info" sx={{ fontSize: 18 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>Separate Lines</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Big, heavy boxes are sorted on a different line from small letters and envelopes.
|
||||
</Typography>
|
||||
<Chip label="Using Line 4" color="info" size="small" variant="outlined" />
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, sm: 4 }} >
|
||||
<Box sx={{ p: 2, bgcolor: 'grey.50', borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
|
||||
<QrCodeScannerIcon color="primary" sx={{ fontSize: 18 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>Quick Scan</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
A scanner reads each parcel automatically as it passes, so nothing is missed.
|
||||
</Typography>
|
||||
<Chip label="Working" color="primary" size="small" variant="outlined" />
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Active Delivery Routes */}
|
||||
<Card>
|
||||
<CardHeader title="Delivery Areas Today" subheader="Parcels and milers for each part of the city" />
|
||||
<Divider />
|
||||
<CardContent sx={{ pt: 3 }}>
|
||||
<TableContainer component={Paper} variant="outlined" sx={{ overflowX: 'auto' }}>
|
||||
<Table size="small" sx={{ minWidth: 420 }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Area</TableCell>
|
||||
<TableCell align="center">Parcels</TableCell>
|
||||
<TableCell align="center">Milers</TableCell>
|
||||
<TableCell align="center">Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{activeRoutes.map((r) => (
|
||||
<TableRow key={r.zone}>
|
||||
<TableCell sx={{ fontWeight: 600 }}>{r.zone}</TableCell>
|
||||
<TableCell align="center">{r.packages}</TableCell>
|
||||
<TableCell align="center">{r.riders}</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
size="small"
|
||||
label={r.status}
|
||||
color={r.status === 'Active' ? 'success' : 'warning'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* Sidebar logs / Active updates */}
|
||||
<Grid size={{ xs: 12, md: 5, lg: 4 }} >
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardHeader title="Recent Activity" subheader="What's been happening at the hub" />
|
||||
<Divider />
|
||||
<CardContent sx={{ pt: 3 }}>
|
||||
<Stack spacing={3}>
|
||||
{recentActivity.map((act, i) => (
|
||||
<Stack direction="row" spacing={2} key={i} alignItems="flex-start">
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
bgcolor: act.type === 'inbound' ? '#C01227' : act.type === 'exception' ? '#F04134' : '#00A854',
|
||||
mt: 0.75,
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
<Stack spacing={0.25} sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||
{act.text}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{act.time}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ mt: 4, p: 2.5, bgcolor: '#8E1F2A10', borderRadius: 2, border: '1px dashed #8E1F2A30' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: 'primary.dark', mb: 1 }}>
|
||||
Not sure where a parcel goes?
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
Scan it and we'll tell you exactly what to do next.
|
||||
</Typography>
|
||||
<Button size="small" variant="contained" endIcon={<ArrowForwardIcon fontSize="small" />} href="/routing">
|
||||
Where Does It Go?
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user