wired the api for rest mock data

This commit is contained in:
2026-07-07 17:35:00 +05:30
parent e6ad37b20f
commit 903c7ea74c
14 changed files with 672 additions and 173 deletions

View File

@@ -22,7 +22,10 @@ import {
Divider,
Popover,
Alert,
Skeleton
Skeleton,
useMediaQuery,
Snackbar,
CircularProgress
} from '@mui/material';
import dayjs from 'dayjs';
import { alpha } from '@mui/material/styles';
@@ -38,11 +41,12 @@ import SpeedIcon from '@mui/icons-material/Speed';
import DeliveryDiningIcon from '@mui/icons-material/DeliveryDining';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
import { getDashboard, getInboundVehicles, getActivity, getZones } from '@/api/hub';
import { getDashboard, getInboundVehicles, getActivity, getZones, getHubReport } from '@/api/hub';
import { getHubContext } from '@/auth/session';
// Format an ISO time as a short clock label for the activity feed.
@@ -178,11 +182,27 @@ const KPI_DEFS = [
const DATE_FMT = 'YYYY-MM-DD';
const RANGE_KEY = 'hub_dashboard_range';
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 });
// Persist the chosen range so it survives leaving the page and coming back
// (the component unmounts on navigation, which would otherwise reset it).
const [range, setRange] = useState(() => {
try {
const saved = JSON.parse(localStorage.getItem(RANGE_KEY));
// Ignore a stale future/invalid `to`; otherwise restore the saved range.
if (saved?.from && saved?.to && !dayjs(saved.to).isAfter(dayjs(today))) return saved;
} catch { /* fall through to default */ }
return { from: weekAgo, to: today };
});
const [calAnchor, setCalAnchor] = useState(null);
const isMobile = useMediaQuery('(max-width:600px)');
useEffect(() => {
localStorage.setItem(RANGE_KEY, JSON.stringify(range));
}, [range]);
const hub = getHubContext();
const [kpis, setKpis] = useState(null);
@@ -191,6 +211,8 @@ export default function Dashboard() {
const [incomingVehicles, setIncomingVehicles] = useState([]);
const [recentActivity, setRecentActivity] = useState([]);
const [activeRoutes, setActiveRoutes] = useState([]);
const [exporting, setExporting] = useState(false);
const [toast, setToast] = useState({ open: false, msg: '', severity: 'success' });
const loadDashboard = useCallback(async () => {
setLoading(true);
@@ -235,6 +257,36 @@ export default function Dashboard() {
loadDashboard();
}, [loadDashboard]);
// Pull the full report for the selected range and download it as an Excel file.
const handleExport = useCallback(async () => {
if (exporting) return;
if (dayjs(range.to).isBefore(dayjs(range.from))) {
setToast({ open: true, msg: 'The "to" date is before the "from" date. Fix the range first.', severity: 'warning' });
return;
}
setExporting(true);
try {
// Lazy-load the Excel builder (pulls in the heavy xlsx lib) only on first export.
const [{ downloadHubReport }, res] = await Promise.all([
import('@/lib/hubReport'),
getHubReport(range.from, range.to)
]);
downloadHubReport(res?.data || {}, { hubName: hub.hubname || 'Hub', from: range.from, to: range.to });
setToast({ open: true, msg: 'Report downloaded.', severity: 'success' });
} catch (err) {
const notReady = err?.status === 404;
setToast({
open: true,
msg: notReady
? 'Report endpoint not available yet. Ask the backend to add GET /hub/report?from=&to=.'
: err?.message || 'Could not generate the report.',
severity: notReady ? 'warning' : 'error'
});
} finally {
setExporting(false);
}
}, [exporting, range.from, range.to, hub.hubname]);
const kpiCards = KPI_DEFS.map((d) => ({
...d,
value: kpis && kpis[d.key] != null ? Number(kpis[d.key]).toLocaleString('en-IN') : '—'
@@ -282,9 +334,10 @@ export default function Dashboard() {
direction="row"
spacing={1}
alignItems="center"
flexWrap="wrap"
useFlexGap
sx={{ flexGrow: 1, minWidth: 0, justifyContent: { xs: 'flex-start', md: 'flex-end' } }}
// flexWrap must live in sx MUI's Stack ignores it as a top-level prop, which
// is why this row overflowed instead of wrapping on narrow screens.
sx={{ flexWrap: 'wrap', width: { xs: '100%', md: 'auto' }, flexGrow: 1, minWidth: 0, justifyContent: { xs: 'flex-start', md: 'flex-end' } }}
>
{/* Refresh Button */}
<IconButton
@@ -346,8 +399,10 @@ export default function Dashboard() {
color: '#334155',
borderColor: invalidRange ? '#EF4444' : '#E2E8F0',
bgcolor: '#ffffff',
justifyContent: 'flex-start',
minWidth: 210, // Compressed from 250px to remove dead layout space
justifyContent: 'flex-start',
// Full width on mobile so it wraps to its own line; fixed on larger screens.
width: { xs: '100%', sm: 'auto' },
minWidth: { sm: 210 }, // Compressed from 250px to remove dead layout space
'&:hover': { borderColor: BRAND, bgcolor: alpha(BRAND, 0.02) }
}}
>
@@ -357,6 +412,31 @@ export default function Dashboard() {
<Box component="span">{dayjs(range.to).format('DD MMM YYYY')}</Box>
</Stack>
</Button>
{/* Export report (Excel) for the selected date range */}
<Button
onClick={handleExport}
disabled={exporting || invalidRange}
variant="contained"
startIcon={exporting
? <CircularProgress size={16} sx={{ color: '#fff' }} />
: <FileDownloadOutlinedIcon sx={{ fontSize: 18 }} />}
sx={{
height: 36,
px: 2,
borderRadius: 2,
textTransform: 'none',
fontWeight: 700,
fontSize: '0.825rem',
bgcolor: BRAND,
boxShadow: 'none',
whiteSpace: 'nowrap',
width: { xs: '100%', sm: 'auto' },
'&:hover': { bgcolor: '#9E0E20', boxShadow: 'none' }
}}
>
{exporting ? 'Exporting…' : 'Export'}
</Button>
</Stack>
</Stack>
@@ -365,9 +445,12 @@ export default function Dashboard() {
open={Boolean(calAnchor)}
anchorEl={calAnchor}
onClose={() => setCalAnchor(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
PaperProps={{ sx: { mt: 1, borderRadius: 2.5, border: '1px solid #EEF0F2', boxShadow: '0 8px 28px rgba(0,0,0,0.10)', overflow: 'hidden' } }}
// On mobile the trigger sits on the left, so a right-aligned popover ran off
// the screen edge. Left-align on mobile and keep MUI clamping it into view.
anchorOrigin={{ vertical: 'bottom', horizontal: isMobile ? 'left' : 'right' }}
transformOrigin={{ vertical: 'top', horizontal: isMobile ? 'left' : 'right' }}
marginThreshold={12}
PaperProps={{ sx: { mt: 1, borderRadius: 2.5, border: '1px solid #EEF0F2', boxShadow: '0 8px 28px rgba(0,0,0,0.10)', overflow: 'hidden', maxWidth: 'calc(100vw - 24px)' } }}
>
<Box sx={{ px: 2, pt: 1.75, pb: 0.5 }}>
<Typography sx={{ fontWeight: 700, fontSize: '0.9rem', color: '#212529' }}>Select date range</Typography>
@@ -454,13 +537,30 @@ export default function Dashboard() {
<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>
{(() => {
// Real sorting progress from the dashboard KPIs: parcels sorted today
// against the hub's configured capacity (sorting_target).
const sorted = Number(kpis?.parcels_sorted ?? 0);
const target = Number(kpis?.sorting_target ?? 0);
const pct = target > 0 ? Math.min(100, Math.round((sorted / target) * 100)) : 0;
return (
<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 }}>
{target > 0
? `Today's target — ${target.toLocaleString('en-IN')} parcels`
: `Sorted today — ${sorted.toLocaleString('en-IN')} parcels`}
</Typography>
{target > 0 && (
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 700, whiteSpace: 'nowrap' }}>
{sorted.toLocaleString('en-IN')} done · {pct}%
</Typography>
)}
</Stack>
<LinearProgress variant="determinate" value={pct} 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' }}>
@@ -640,6 +740,22 @@ export default function Dashboard() {
</Card>
</Grid>
</Grid>
<Snackbar
open={toast.open}
autoHideDuration={4000}
onClose={() => setToast((t) => ({ ...t, open: false }))}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
severity={toast.severity}
variant="filled"
onClose={() => setToast((t) => ({ ...t, open: false }))}
sx={{ borderRadius: 2 }}
>
{toast.msg}
</Alert>
</Snackbar>
</Box>
);
}