wired the api for rest mock data
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user