added the api for the real data
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Card,
|
||||
@@ -20,7 +20,9 @@ import {
|
||||
Avatar,
|
||||
Button,
|
||||
Divider,
|
||||
Popover
|
||||
Popover,
|
||||
Alert,
|
||||
Skeleton
|
||||
} from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
@@ -33,7 +35,6 @@ 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';
|
||||
@@ -41,6 +42,19 @@ 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 { getHubContext } from '@/auth/session';
|
||||
|
||||
// Format an ISO time as a short clock label for the activity feed.
|
||||
const clockTime = (iso) => {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
// Map a vehicle status to the MUI chip colour the table expects.
|
||||
const vehicleColor = (status) => (status === 'Unloading' ? 'success' : status === 'On the way' ? 'info' : 'default');
|
||||
|
||||
const BRAND = '#C01227';
|
||||
const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
|
||||
@@ -152,19 +166,14 @@ function RangeCalendar({ from, to, maxDate, onSelect }) {
|
||||
);
|
||||
}
|
||||
|
||||
// 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' }
|
||||
// Live KPI cards — each maps to one field of GET /api/v1/hub/dashboard.
|
||||
const KPI_DEFS = [
|
||||
{ key: 'parcels_received_today', label: 'Total Parcels Received', icon: DynamicFeedIcon, color: '#1A73E8', sub: 'Received today' },
|
||||
{ key: 'milers_available', label: 'Available Milers', icon: DeliveryDiningIcon, color: '#1E8E3E', sub: 'Free right now' },
|
||||
{ key: 'milers_on_duty', label: 'Milers on Duty', icon: InfoOutlinedIcon, color: '#1A73E8', sub: 'Currently working' },
|
||||
{ key: 'pending_pickups', label: 'Pending Pickups', icon: AssignmentIcon, color: '#F29900', sub: 'Awaiting a miler' },
|
||||
{ key: 'batches_sent_today', label: 'Batches Sent Today', icon: LocalShippingIcon, color: '#8E24AA', sub: 'Dispatched today' },
|
||||
{ key: 'exceptions', label: 'Needs Checking', icon: WarningAmberIcon, color: '#D93025', sub: 'Damaged or unclear' }
|
||||
];
|
||||
|
||||
const DATE_FMT = 'YYYY-MM-DD';
|
||||
@@ -175,6 +184,62 @@ export default function Dashboard() {
|
||||
const [range, setRange] = useState({ from: weekAgo, to: today });
|
||||
const [calAnchor, setCalAnchor] = useState(null);
|
||||
|
||||
const hub = getHubContext();
|
||||
const [kpis, setKpis] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [incomingVehicles, setIncomingVehicles] = useState([]);
|
||||
const [recentActivity, setRecentActivity] = useState([]);
|
||||
const [activeRoutes, setActiveRoutes] = useState([]);
|
||||
|
||||
const loadDashboard = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const [dash, vehicles, activity, zones] = await Promise.all([
|
||||
getDashboard(),
|
||||
getInboundVehicles().catch(() => null),
|
||||
getActivity(8).catch(() => null),
|
||||
getZones().catch(() => null)
|
||||
]);
|
||||
setKpis(dash?.data || {});
|
||||
setIncomingVehicles(
|
||||
(vehicles?.data || []).map((v) => ({
|
||||
id: v.vehicleno || `Trip ${v.tripsheetid}`,
|
||||
origin: v.origin || '—',
|
||||
estTime: v.eta || '',
|
||||
status: v.status || 'On the way',
|
||||
progress: v.unloadedpct || 0,
|
||||
color: vehicleColor(v.status)
|
||||
}))
|
||||
);
|
||||
setRecentActivity(
|
||||
(activity?.data || []).map((a) => ({ time: clockTime(a.time), type: a.type, text: a.text }))
|
||||
);
|
||||
setActiveRoutes(
|
||||
(zones?.data || []).map((z) => ({
|
||||
zone: z.zonename ? `${z.zonename} (${z.zone})` : z.zone,
|
||||
packages: z.parcels ?? 0,
|
||||
riders: z.milers ?? 0,
|
||||
status: z.status || 'Active'
|
||||
}))
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err?.message || 'Could not load dashboard stats.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
}, [loadDashboard]);
|
||||
|
||||
const kpiCards = KPI_DEFS.map((d) => ({
|
||||
...d,
|
||||
value: kpis && kpis[d.key] != null ? Number(kpis[d.key]).toLocaleString('en-IN') : '—'
|
||||
}));
|
||||
|
||||
// Inclusive day count for the chosen window (min 1); drives cumulative metrics.
|
||||
const dayCount = useMemo(() => {
|
||||
const from = dayjs(range.from);
|
||||
@@ -191,39 +256,6 @@ export default function Dashboard() {
|
||||
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>
|
||||
@@ -238,13 +270,10 @@ export default function Dashboard() {
|
||||
{/* Left Title Section */}
|
||||
<Box sx={{ flexShrink: 0 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 700, color: '#1E293B', letterSpacing: '-0.02em', mb: 0.5 }}>
|
||||
Delhi Hub
|
||||
{hub.hubname || 'Doormile 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>
|
||||
Live hub snapshot{hub.city ? ` · ${hub.city}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -258,19 +287,21 @@ export default function Dashboard() {
|
||||
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,
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={loadDashboard}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
border: '1px solid #E2E8F0',
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 2,
|
||||
bgcolor: '#ffffff',
|
||||
color: '#64748B',
|
||||
'&:hover': { bgcolor: '#F8FAFC', color: '#0F172A' }
|
||||
}}
|
||||
>
|
||||
<RefreshIcon sx={{ fontSize: 18 }} />
|
||||
<RefreshIcon sx={{ fontSize: 18, animation: loading ? 'spin 1s linear infinite' : 'none', '@keyframes spin': { to: { transform: 'rotate(360deg)' } } }} />
|
||||
</IconButton>
|
||||
|
||||
{/* Quick Presets */}
|
||||
@@ -362,6 +393,13 @@ export default function Dashboard() {
|
||||
</Stack>
|
||||
</Popover>
|
||||
|
||||
{/* Live stats error banner */}
|
||||
{error && (
|
||||
<Alert severity="error" onClose={() => setError('')} sx={{ borderRadius: 2, mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Metrics Row — CSS grid with minmax(0,1fr) never overflows on mobile */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -369,14 +407,13 @@ export default function Dashboard() {
|
||||
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))'
|
||||
lg: 'repeat(6, minmax(0, 1fr))'
|
||||
},
|
||||
gap: { xs: 1.5, sm: 2, md: 3 },
|
||||
mb: { xs: 3, md: 5 }
|
||||
}}
|
||||
>
|
||||
{stats.map((s) => {
|
||||
{kpiCards.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' }}>
|
||||
@@ -389,9 +426,13 @@ export default function Dashboard() {
|
||||
{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>
|
||||
{loading ? (
|
||||
<Skeleton variant="text" width="60%" sx={{ fontSize: { xs: '1.5rem', sm: '1.9rem' }, mb: 0.75 }} />
|
||||
) : (
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user