added the api for the real data

This commit is contained in:
2026-07-04 17:08:16 +05:30
parent 200831d19c
commit e6ad37b20f
22 changed files with 2854 additions and 1061 deletions

View File

@@ -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>