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