added the api for the real data
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box, Typography, Card, CardContent, Avatar, Chip, Table, TableBody,
|
||||
TableCell, TableContainer, TableHead, TableRow, IconButton, Button,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
TextField, MenuItem, Divider, InputAdornment, Menu, ListItemIcon,
|
||||
Snackbar, Alert, useMediaQuery, Grid, Select, FormControl, InputLabel,
|
||||
Autocomplete, Switch, FormControlLabel, Paper, ToggleButtonGroup,
|
||||
ToggleButton, Stepper, Step, StepLabel
|
||||
ToggleButton, Stepper, Step, StepLabel, CircularProgress
|
||||
} from '@mui/material';
|
||||
import { useTheme, alpha } from '@mui/material/styles';
|
||||
|
||||
@@ -48,6 +48,20 @@ import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||
import HomeWorkOutlinedIcon from '@mui/icons-material/HomeWorkOutlined';
|
||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||
|
||||
import { getMilers, createMiler, updateMiler, deleteMiler } from '@/api/hub';
|
||||
import { getHubContext } from '@/auth/session';
|
||||
|
||||
// The backend uses snake-case availability values; this page uses friendlier labels.
|
||||
const API_TO_UI_STATUS = { Available: 'Idle', Assigned: 'On Pickup', On_Break: 'On Break', Offline: 'Offline' };
|
||||
const UI_TO_API_STATUS = {
|
||||
Idle: 'Available',
|
||||
'On Pickup': 'Assigned',
|
||||
'On Break': 'On_Break',
|
||||
'Returning to Hub': 'Assigned',
|
||||
Offline: 'Offline',
|
||||
Suspended: 'Offline'
|
||||
};
|
||||
|
||||
// ── Reference data ──────────────────────────────────────────────────────────────
|
||||
const VEHICLES = {
|
||||
'Electric Bike': { capacity: 30, icon: ElectricBikeOutlinedIcon },
|
||||
@@ -74,74 +88,49 @@ const STATUS_META = {
|
||||
let idSeq = 8060;
|
||||
const genId = () => `RDR-${idSeq++}`;
|
||||
|
||||
// ── Seed roster ────────────────────────────────────────────────────────────────
|
||||
const SEED = [
|
||||
{
|
||||
id: 'RDR-8012', name: 'Muthu Kumar', phone: '+91 98765 43210', hub: 'Delhi Operations Hub',
|
||||
zones: ['Dwarka', 'Janakpuri'], vehicle: 'Electric Bike', vehicleNo: 'DL-04-EB-1234',
|
||||
status: 'On Pickup', checkInTime: '08:12', hoursToday: 6.4,
|
||||
assigned: 24, capacity: 30, pickupsPending: 3, deliveriesPending: 21,
|
||||
deliveriesDone: 45, deliveriesFailed: 2, codCollected: 4500, codPending: 1200,
|
||||
rating: 4.8, verified: true,
|
||||
},
|
||||
{
|
||||
id: 'RDR-8015', name: 'Rajesh Sharma', phone: '+91 98765 43211', hub: 'Delhi Operations Hub',
|
||||
zones: ['Saket', 'Malviya Nagar'], vehicle: 'Cargo Van', vehicleNo: 'DL-01-CV-9876',
|
||||
status: 'On Pickup', checkInTime: '07:50', hoursToday: 6.8,
|
||||
assigned: 88, capacity: 120, pickupsPending: 12, deliveriesPending: 76,
|
||||
deliveriesDone: 110, deliveriesFailed: 4, codCollected: 12000, codPending: 3400,
|
||||
rating: 4.9, verified: true,
|
||||
},
|
||||
{
|
||||
id: 'RDR-8022', name: 'Vikram Singh', phone: '+91 98765 43212', hub: 'Delhi Operations Hub',
|
||||
zones: ['Rohini'], vehicle: 'Motorcycle', vehicleNo: 'DL-08-MC-4567',
|
||||
status: 'Idle', checkInTime: '09:05', hoursToday: 5.2,
|
||||
assigned: 0, capacity: 25, pickupsPending: 0, deliveriesPending: 0,
|
||||
deliveriesDone: 68, deliveriesFailed: 1, codCollected: 0, codPending: 0,
|
||||
rating: 4.5, verified: false,
|
||||
},
|
||||
// ... (rest of seed data remains the same)
|
||||
{
|
||||
id: 'RDR-8030', name: 'Amit Patel', phone: '+91 98765 43213', hub: 'Delhi Operations Hub',
|
||||
zones: ['Vasant Kunj'], vehicle: 'Electric Bike', vehicleNo: 'DL-03-EB-7654',
|
||||
status: 'Returning to Hub', checkInTime: '08:30', hoursToday: 7.1,
|
||||
assigned: 2, capacity: 30, pickupsPending: 0, deliveriesPending: 2,
|
||||
deliveriesDone: 55, deliveriesFailed: 6, codCollected: 8500, codPending: 0,
|
||||
rating: 4.2, verified: true,
|
||||
},
|
||||
{
|
||||
id: 'RDR-8041', name: 'Sana Sheikh', phone: '+91 98765 43214', hub: 'Delhi Operations Hub',
|
||||
zones: ['Central Delhi', 'Karol Bagh'], vehicle: 'Motorcycle', vehicleNo: 'DL-02-MC-1199',
|
||||
status: 'On Pickup', checkInTime: '08:00', hoursToday: 6.6,
|
||||
assigned: 19, capacity: 25, pickupsPending: 2, deliveriesPending: 17,
|
||||
deliveriesDone: 73, deliveriesFailed: 0, codCollected: 6200, codPending: 900,
|
||||
rating: 5.0, verified: true,
|
||||
},
|
||||
{
|
||||
id: 'RDR-8044', name: 'Harpreet Gill', phone: '+91 98765 43215', hub: 'Delhi Operations Hub',
|
||||
zones: ['Mayur Vihar'], vehicle: 'Cycle', vehicleNo: '—',
|
||||
status: 'On Break', checkInTime: '09:20', hoursToday: 3.9,
|
||||
assigned: 8, capacity: 15, pickupsPending: 1, deliveriesPending: 7,
|
||||
deliveriesDone: 22, deliveriesFailed: 1, codCollected: 1500, codPending: 300,
|
||||
rating: 4.6, verified: true,
|
||||
},
|
||||
{
|
||||
id: 'RDR-8050', name: 'Deepak Yadav', phone: '+91 98765 43216', hub: 'Delhi Operations Hub',
|
||||
zones: ['Lajpat Nagar'], vehicle: 'Mini Truck', vehicleNo: 'DL-09-MT-3321',
|
||||
status: 'Offline', checkInTime: '—', hoursToday: 0,
|
||||
assigned: 0, capacity: 200, pickupsPending: 0, deliveriesPending: 0,
|
||||
deliveriesDone: 0, deliveriesFailed: 0, codCollected: 0, codPending: 0,
|
||||
rating: 4.4, verified: false,
|
||||
},
|
||||
{
|
||||
id: 'RDR-8055', name: 'Iqbal Ahmed', phone: '+91 98765 43217', hub: 'Delhi Operations Hub',
|
||||
zones: ['Connaught Place'], vehicle: 'Electric Bike', vehicleNo: 'DL-05-EB-8080',
|
||||
status: 'Suspended', checkInTime: '—', hoursToday: 0,
|
||||
assigned: 0, capacity: 30, pickupsPending: 0, deliveriesPending: 0,
|
||||
deliveriesDone: 0, deliveriesFailed: 9, codCollected: 0, codPending: 5600,
|
||||
rating: 3.1, verified: true,
|
||||
},
|
||||
];
|
||||
// Turn a vehicle type name into the 1-based id the backend uses (best effort).
|
||||
const vehicleIdFor = (vehicle) => Math.max(1, VEHICLE_TYPES.indexOf(vehicle) + 1);
|
||||
|
||||
// Map a raw API miler onto the rich shape this page renders. Fields the API does
|
||||
// not provide (zones, COD, live load) default to empty/zero so the UI still works.
|
||||
// Format an ISO check-in timestamp as HH:MM (or "—").
|
||||
const checkInLabel = (iso) => {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '—';
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const mapMiler = (m, hubName) => {
|
||||
// Real backend exposes defaultvehicletype (e.g. "Bike"); fall back to id lookup.
|
||||
const vehicle = m.defaultvehicletype || VEHICLE_TYPES[(m.vehicleid || 1) - 1] || 'Motorcycle';
|
||||
const capacity = m.capacity || VEHICLES[vehicle]?.capacity || 0;
|
||||
return {
|
||||
id: m.userid,
|
||||
userid: m.userid,
|
||||
vehicleid: m.vehicleid,
|
||||
hubid: m.hubid,
|
||||
name: m.displayname || `Miler ${m.userid}`,
|
||||
phone: m.phone || '—',
|
||||
hub: hubName,
|
||||
zones: Array.isArray(m.zones) ? m.zones : m.currentpincode ? [m.currentpincode] : [],
|
||||
vehicle,
|
||||
vehicleNo: m.vehicleid ? `VEH-${m.vehicleid}` : '—',
|
||||
status: API_TO_UI_STATUS[m.availabilitystatus] || 'Idle',
|
||||
checkInTime: checkInLabel(m.checkinat),
|
||||
hoursToday: m.hoursactive ?? 0,
|
||||
assigned: m.assignedload ?? 0,
|
||||
capacity,
|
||||
pickupsPending: m.pickupspending ?? 0,
|
||||
deliveriesPending: m.assignedload ?? 0,
|
||||
deliveriesDone: m.totalcompletedpickups ?? m.completedorders ?? 0,
|
||||
deliveriesFailed: m.totalcancelledpickups ?? m.cancelledorders ?? 0,
|
||||
codCollected: m.codcollected ?? 0,
|
||||
codPending: m.codpending ?? 0,
|
||||
rating: m.rating ?? 0,
|
||||
verified: Boolean(m.isverified ?? m.device_token)
|
||||
};
|
||||
};
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
const successRate = (r) => {
|
||||
@@ -938,12 +927,35 @@ export default function Riders() {
|
||||
const isMdDown = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isLgDown = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
|
||||
const [riders, setRiders] = useState(SEED);
|
||||
const hub = getHubContext();
|
||||
const hubName = hub.hubname || 'This Hub';
|
||||
|
||||
const [riders, setRiders] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState('All');
|
||||
const [vehicleFilter, setVehicleFilter] = useState('All');
|
||||
const [view, setView] = useState('table');
|
||||
|
||||
const loadMilers = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setLoadError('');
|
||||
try {
|
||||
const res = await getMilers();
|
||||
setRiders((res?.data || []).map((m) => mapMiler(m, hubName)));
|
||||
} catch (err) {
|
||||
setLoadError(err?.message || 'Could not load milers.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadMilers();
|
||||
}, [loadMilers]);
|
||||
|
||||
const [formDialog, setFormDialog] = useState({ open: false, mode: 'add', initial: null });
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [profileEdit, setProfileEdit] = useState(false);
|
||||
@@ -979,22 +991,43 @@ export default function Riders() {
|
||||
});
|
||||
}, [riders, search, statusFilter, vehicleFilter]);
|
||||
|
||||
const handleSave = (rider) => {
|
||||
if (formDialog.mode === 'add') {
|
||||
setRiders(p => [rider, ...p]);
|
||||
toast(`${rider.name} onboarded successfully`);
|
||||
} else {
|
||||
setRiders(p => p.map(r => r.id === rider.id ? rider : r));
|
||||
toast(`${rider.name} updated`);
|
||||
// Build the API payload from the rich form object.
|
||||
const toApiPayload = (rider) => ({
|
||||
displayname: rider.name,
|
||||
phone: rider.phone,
|
||||
hubid: rider.hubid || hub.hubid,
|
||||
vehicleid: rider.vehicleid || vehicleIdFor(rider.vehicle),
|
||||
availabilitystatus: UI_TO_API_STATUS[rider.status] || 'Available'
|
||||
});
|
||||
|
||||
const handleSave = async (rider) => {
|
||||
try {
|
||||
if (formDialog.mode === 'add') {
|
||||
await createMiler(toApiPayload(rider));
|
||||
toast(`${rider.name} onboarded successfully`);
|
||||
} else {
|
||||
await updateMiler(rider.userid ?? rider.id, toApiPayload(rider));
|
||||
toast(`${rider.name} updated`);
|
||||
}
|
||||
setFormDialog({ open: false, mode: 'add', initial: null });
|
||||
loadMilers();
|
||||
} catch (err) {
|
||||
toast(err?.message || 'Could not save this miler.', 'error');
|
||||
}
|
||||
setFormDialog({ open: false, mode: 'add', initial: null });
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setRiders(p => p.filter(r => r.id !== deleteTarget.id));
|
||||
toast(`${deleteTarget.name} removed`, 'info');
|
||||
setDeleteTarget(null);
|
||||
setProfile(null);
|
||||
const handleDelete = async () => {
|
||||
const target = deleteTarget;
|
||||
try {
|
||||
await deleteMiler(target.userid ?? target.id);
|
||||
setRiders((p) => p.filter((r) => r.id !== target.id));
|
||||
toast(`${target.name} removed`, 'info');
|
||||
} catch (err) {
|
||||
toast(err?.message || 'Could not remove this miler.', 'error');
|
||||
} finally {
|
||||
setDeleteTarget(null);
|
||||
setProfile(null);
|
||||
}
|
||||
};
|
||||
|
||||
const openAdd = () => setFormDialog({ open: true, mode: 'add', initial: null });
|
||||
@@ -1009,11 +1042,16 @@ export default function Riders() {
|
||||
setProfile(r);
|
||||
setProfileEdit(false);
|
||||
};
|
||||
const saveProfileEdit = (updated) => {
|
||||
setRiders(p => p.map(r => (r.id === updated.id ? updated : r)));
|
||||
setProfile(updated);
|
||||
setProfileEdit(false);
|
||||
toast(`${updated.name} updated`);
|
||||
const saveProfileEdit = async (updated) => {
|
||||
try {
|
||||
await updateMiler(updated.userid ?? updated.id, toApiPayload(updated));
|
||||
setRiders((p) => p.map((r) => (r.id === updated.id ? updated : r)));
|
||||
setProfile(updated);
|
||||
setProfileEdit(false);
|
||||
toast(`${updated.name} updated`);
|
||||
} catch (err) {
|
||||
toast(err?.message || 'Could not update this miler.', 'error');
|
||||
}
|
||||
};
|
||||
const openMenu = (e, r) => {
|
||||
setMenuAnchor(e.currentTarget);
|
||||
@@ -1253,7 +1291,16 @@ export default function Riders() {
|
||||
</Box>
|
||||
|
||||
{/* Content Area */}
|
||||
{filtered.length === 0 ? (
|
||||
{loadError && (
|
||||
<Alert severity="error" onClose={() => setLoadError('')} sx={{ m: 2, borderRadius: 2 }}>
|
||||
{loadError}
|
||||
</Alert>
|
||||
)}
|
||||
{loading ? (
|
||||
<Box sx={{ py: 12, display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : filtered.length === 0 ? (
|
||||
<Box sx={{ py: 12, textAlign: 'center' }}>
|
||||
<SearchOutlinedIcon sx={{ fontSize: 80, color: '#E0E0E0', mb: 3 }} />
|
||||
<Typography variant="h5" fontWeight={600}>No milers match your filters</Typography>
|
||||
|
||||
Reference in New Issue
Block a user