Files
doormile_hub_console/src/pages/operations/Riders.jsx
Thiru-tenext ef0b14d254 feat: Implement date range picker and enhance order assignment functionality
- Added DateRangePicker component for selecting date ranges in OrderAssignment.
- Updated OrderAssignment to fetch bookings based on selected date range.
- Enhanced status handling in OrderAssignment with new status chip display logic.
- Refactored KPI card implementation in RiderRoutes and Riders to use shared StatCard component.
- Improved ProfileDrawer to retain rider data during close transition.
- Fixed minor text formatting in Routing component.
2026-07-09 16:28:45 +05:30

1405 lines
55 KiB
JavaScript

import React, { useState, useMemo, useEffect, useCallback } from 'react';
import {
Box, Typography, Card, CardContent, Avatar, Chip, Table, TableBody,
TableCell, TableContainer, TableHead, TableRow, IconButton, Button,
Stack, Tooltip, LinearProgress, Dialog, DialogContent, DialogActions, Drawer,
TextField, MenuItem, Divider, InputAdornment, Menu, ListItemIcon,
Snackbar, Alert, useMediaQuery, Grid, Select, FormControl, InputLabel,
Autocomplete, Switch, FormControlLabel, Paper, ToggleButtonGroup,
ToggleButton, Stepper, Step, StepLabel, CircularProgress
} from '@mui/material';
import { useTheme, alpha } from '@mui/material/styles';
// ── Professional outlined icon set ──────────────────────────────────────────────
import AddOutlinedIcon from '@mui/icons-material/AddOutlined';
import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlineOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import MoreVertOutlinedIcon from '@mui/icons-material/MoreVertOutlined';
import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined';
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
import CircleRoundedIcon from '@mui/icons-material/CircleRounded';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined';
import CallOutlinedIcon from '@mui/icons-material/CallOutlined';
import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined';
import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined';
import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined';
import PersonAddAlt1OutlinedIcon from '@mui/icons-material/PersonAddAlt1Outlined';
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import ElectricBikeOutlinedIcon from '@mui/icons-material/ElectricBikeOutlined';
import PedalBikeOutlinedIcon from '@mui/icons-material/PedalBikeOutlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import AirportShuttleOutlinedIcon from '@mui/icons-material/AirportShuttleOutlined';
import InventoryOutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined';
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined';
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
import GppGoodOutlinedIcon from '@mui/icons-material/GppGoodOutlined';
import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined';
import TableRowsOutlinedIcon from '@mui/icons-material/TableRowsOutlined';
import GridViewOutlinedIcon from '@mui/icons-material/GridViewOutlined';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
import HomeWorkOutlinedIcon from '@mui/icons-material/HomeWorkOutlined';
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
import StatCard from '@/components/StatCard';
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 },
'Motorcycle': { capacity: 25, icon: TwoWheelerOutlinedIcon },
'Cycle': { capacity: 15, icon: PedalBikeOutlinedIcon },
'Cargo Van': { capacity: 120, icon: AirportShuttleOutlinedIcon },
'Mini Truck': { capacity: 200, icon: LocalShippingOutlinedIcon },
};
const VEHICLE_TYPES = Object.keys(VEHICLES);
const ALL_ZONES = ['Dwarka', 'Janakpuri', 'Saket', 'Malviya Nagar', 'Rohini', 'Vasant Kunj', 'Central Delhi', 'Lajpat Nagar', 'Karol Bagh', 'Connaught Place', 'Mayur Vihar'];
const HUB_OPTIONS = ['Delhi Operations Hub', 'Mumbai Hub (BOM-02)', 'Bengaluru Hub (BLR-03)', 'Jaipur Hub (JAI-08)'];
const STATUS_OPTIONS = ['On Pickup', 'Idle', 'On Break', 'Returning to Hub', 'Offline', 'Suspended'];
const STATUS_META = {
'On Pickup': { color: '#1A73E8', bg: '#E8F0FE', label: 'On Pickup' },
'Idle': { color: '#B06000', bg: '#FEF7E0', label: 'Idle / Available' },
'On Break': { color: '#8E24AA', bg: '#F3E5F5', label: 'On Break' },
'Returning to Hub': { color: '#0B8043', bg: '#E6F4EA', label: 'Returning' },
'Offline': { color: '#80868B', bg: '#F1F3F4', label: 'Offline' },
'Suspended': { color: '#D93025', bg: '#FCE8E6', label: 'Suspended' },
};
let idSeq = 8060;
const genId = () => `RDR-${idSeq++}`;
// 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,
// Real registration plate from the joined vehicles table; fall back to a
// placeholder tag if the miler has no vehicle assigned.
vehicleNo: m.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) => {
const total = r.deliveriesDone + r.deliveriesFailed;
return total === 0 ? 100 : Math.round((r.deliveriesDone / total) * 100);
};
const loadPct = (r) => (r.capacity === 0 ? 0 : Math.round((r.assigned / r.capacity) * 100));
const initials = (name) => name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase();
const inr = (n) => `${n.toLocaleString('en-IN')}`;
const EMPTY_FORM = {
name: '', phone: '', hub: 'Delhi Operations Hub', zones: [],
vehicle: '', vehicleNo: '', status: 'Idle', verified: false,
};
// ════════════════════════════════════════════════════════════════════════════════
// Presentational Components (Improved spacing & visuals)
// ════════════════════════════════════════════════════════════════════════════════
function StatusPill({ status, size = 'md' }) {
const m = STATUS_META[status] || STATUS_META['Offline'];
const small = size === 'sm';
return (
<Box sx={{
display: 'inline-flex', alignItems: 'center', gap: 0.75,
px: small ? 1.5 : 2, py: small ? 0.5 : 0.75, borderRadius: 2,
bgcolor: m.bg, border: `1px solid ${alpha(m.color, 0.2)}`, whiteSpace: 'nowrap',
}}>
<CircleRoundedIcon sx={{ fontSize: small ? 8 : 9, color: m.color }} />
<Typography sx={{ fontSize: small ? '0.7rem' : '0.75rem', fontWeight: 700, color: m.color, lineHeight: 1 }}>
{m.label}
</Typography>
</Box>
);
}
function CapacityBar({ rider, showLabel = true }) {
const pct = loadPct(rider);
const color = pct > 85 ? '#D93025' : pct > 60 ? '#F29900' : '#1A73E8';
return (
<Box sx={{ minWidth: 140 }}>
{showLabel && (
<Typography variant="caption" sx={{ display: 'block', mb: 0.8 }}>
<Box component="span" sx={{ fontWeight: 700, color: '#343A40' }}>{rider.assigned}/{rider.capacity}</Box>
<Box component="span" sx={{ fontWeight: 700, color, ml: 0.75 }}>· {pct}%</Box>
</Typography>
)}
<LinearProgress
variant="determinate"
value={pct}
sx={{
height: 6,
borderRadius: 2,
bgcolor: '#EDEFF2',
'& .MuiLinearProgress-bar': { bgcolor: color, borderRadius: 2 }
}}
/>
</Box>
);
}
function RiderAvatar({ rider, size = 48 }) {
const m = STATUS_META[rider.status] || STATUS_META['Offline'];
return (
<Box sx={{ position: 'relative', flexShrink: 0 }}>
<Avatar sx={{
width: size, height: size, fontSize: size * 0.36, fontWeight: 800,
bgcolor: alpha(m.color, 0.1), color: m.color, border: `2px solid ${alpha(m.color, 0.2)}`,
}}>
{initials(rider.name)}
</Avatar>
<Box sx={{
position: 'absolute', bottom: -2, right: -2, width: size * 0.32, height: size * 0.32,
borderRadius: '50%', bgcolor: m.color, border: '2.5px solid #fff',
}} />
</Box>
);
}
function VehicleCell({ vehicle, vehicleNo }) {
const Icon = VEHICLES[vehicle]?.icon || LocalShippingOutlinedIcon;
return (
<Stack direction="row" alignItems="center" gap={1.5}>
<Avatar variant="rounded" sx={{ width: 36, height: 36, bgcolor: '#F8F9FA', color: '#5F6368' }}>
<Icon sx={{ fontSize: 20 }} />
</Avatar>
<Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{vehicle}</Typography>
<Typography variant="caption" sx={{ color: '#9AA0A6', fontFamily: 'monospace' }}>{vehicleNo}</Typography>
</Box>
</Stack>
);
}
// KPI card — the shared StatCard is the single reference design (see components/StatCard).
// Kept as a thin alias so the call sites below stay unchanged; `hover` on for the lift effect.
const KpiCard = (props) => <StatCard {...props} hover />;
// ════════════════════════════════════════════════════════════════════════════════
// Dialogs (Cleaner & Better Spaced)
// ════════════════════════════════════════════════════════════════════════════════
const STEPS = ['Personal', 'Assignment', 'Vehicle & KYC'];
function RiderFormDialog({ open, onClose, onSave, initial, mode }) {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const [activeStep, setActiveStep] = useState(0);
const [form, setForm] = useState(initial || EMPTY_FORM);
const [errors, setErrors] = useState({});
React.useEffect(() => {
if (open) {
setForm(initial || EMPTY_FORM);
setErrors({});
setActiveStep(0);
}
}, [open, initial]);
const set = (k, v) => {
setForm(p => ({ ...p, [k]: v }));
setErrors(e => ({ ...e, [k]: undefined }));
};
const validateStep = (step) => {
const e = {};
if (step === 0) {
if (!form.name.trim()) e.name = 'Full name is required';
if (!form.phone.trim()) e.phone = 'Phone number is required';
else if (!/[0-9]{10}/.test(form.phone.replace(/\D/g, ''))) e.phone = 'Enter a valid 10-digit phone number';
}
if (step === 1) {
if (form.zones.length === 0) e.zones = 'Assign at least one service zone';
}
if (step === 2) {
if (!form.vehicle) e.vehicle = 'Select a vehicle type';
if (!form.vehicleNo.trim() && form.vehicle !== 'Cycle') e.vehicleNo = 'Vehicle number is required';
}
setErrors(e);
return Object.keys(e).length === 0;
};
const next = () => { if (validateStep(activeStep)) setActiveStep(s => s + 1); };
const back = () => setActiveStep(s => s - 1);
const submit = () => {
if (!validateStep(2)) return;
const cap = VEHICLES[form.vehicle]?.capacity || 30;
onSave({
...form,
vehicleNo: form.vehicleNo.trim() || '—',
id: mode === 'add' ? genId() : form.id,
capacity: cap,
checkInTime: mode === 'add' ? '—' : form.checkInTime,
hoursToday: mode === 'add' ? 0 : form.hoursToday,
assigned: mode === 'add' ? 0 : form.assigned,
pickupsPending: mode === 'add' ? 0 : form.pickupsPending,
deliveriesPending: mode === 'add' ? 0 : form.deliveriesPending,
deliveriesDone: mode === 'add' ? 0 : form.deliveriesDone,
deliveriesFailed: mode === 'add' ? 0 : form.deliveriesFailed,
codCollected: mode === 'add' ? 0 : form.codCollected,
codPending: mode === 'add' ? 0 : form.codPending,
rating: mode === 'add' ? 5.0 : form.rating,
});
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
fullScreen={fullScreen}
PaperProps={{
sx: {
borderRadius: fullScreen ? 0 : 2,
boxShadow: '0 25px 70px rgba(0,0,0,0.18)'
}
}}
>
{/* Header */}
<Box sx={{ px: 4, pt: 4, pb: 3 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between">
<Stack direction="row" alignItems="center" gap={2}>
<Avatar variant="rounded" sx={{ bgcolor: alpha('#C01227', 0.1), color: '#C01227', width: 48, height: 48 }}>
<PersonAddAlt1OutlinedIcon />
</Avatar>
<Box>
<Typography variant="h5" sx={{ fontWeight: 800, color: '#1A1A2E' }}>
{mode === 'add' ? 'Onboard New Miler' : 'Edit Miler'}
</Typography>
<Typography variant="body2" color="text.secondary">
{mode === 'add' ? 'Add delivery agent to the fleet' : `Updating ${form.name}`}
</Typography>
</Box>
</Stack>
<IconButton onClick={onClose}>
<CloseOutlinedIcon />
</IconButton>
</Stack>
<Stepper activeStep={activeStep} sx={{ mt: 4 }} alternativeLabel>
{STEPS.map(label => (
<Step key={label}>
<StepLabel sx={{ '& .MuiStepLabel-label': { fontWeight: 600 } }}>{label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
<Divider />
<DialogContent sx={{ px: 4, py: 4 }}>
{/* STEP 0 */}
{activeStep === 0 && (
<Stack spacing={3}>
<TextField fullWidth label="Full Name" value={form.name} autoFocus
onChange={e => set('name', e.target.value)} error={!!errors.name} helperText={errors.name}
InputProps={{ startAdornment: <InputAdornment position="start"><PersonOutlineOutlinedIcon /></InputAdornment> }} />
<TextField fullWidth label="Phone Number" value={form.phone}
onChange={e => set('phone', e.target.value)} error={!!errors.phone} helperText={errors.phone}
placeholder="+91 XXXXXXXXXX" InputProps={{ startAdornment: <InputAdornment position="start"><PhoneOutlinedIcon /></InputAdornment> }} />
<FormControl fullWidth>
<InputLabel>Home Hub</InputLabel>
<Select value={form.hub} label="Home Hub" onChange={e => set('hub', e.target.value)}>
{HUB_OPTIONS.map(h => <MenuItem key={h} value={h}>{h}</MenuItem>)}
</Select>
</FormControl>
</Stack>
)}
{/* STEP 1 */}
{activeStep === 1 && (
<Stack spacing={3}>
<Autocomplete multiple value={form.zones} options={ALL_ZONES}
onChange={(_, v) => set('zones', v)}
renderInput={(params) => <TextField {...params} label="Service Zones" error={!!errors.zones} helperText={errors.zones} />}
renderTags={(value, getTagProps) => value.map((option, i) => <Chip {...getTagProps({ index: i })} label={option} color="primary" variant="outlined" />)}
/>
{mode === 'edit' && (
<FormControl fullWidth>
<InputLabel>Status</InputLabel>
<Select value={form.status} onChange={e => set('status', e.target.value)}>
{STATUS_OPTIONS.map(s => (
<MenuItem key={s} value={s}>
<StatusPill status={s} size="sm" />
</MenuItem>
))}
</Select>
</FormControl>
)}
</Stack>
)}
{/* STEP 2 */}
{activeStep === 2 && (
<Stack spacing={3}>
<FormControl fullWidth error={!!errors.vehicle}>
<InputLabel>Vehicle Type</InputLabel>
<Select value={form.vehicle} label="Vehicle Type" onChange={e => set('vehicle', e.target.value)}>
{VEHICLE_TYPES.map(v => {
const IconComp = VEHICLES[v].icon;
return (
<MenuItem key={v} value={v}>
<Stack direction="row" gap={1.5} alignItems="center">
<IconComp />
<Box>
{v}
<Typography variant="caption" display="block">{VEHICLES[v].capacity} parcels</Typography>
</Box>
</Stack>
</MenuItem>
);
})}
</Select>
{errors.vehicle && <Typography variant="caption" color="error" sx={{ mt: 1 }}>{errors.vehicle}</Typography>}
</FormControl>
<TextField fullWidth label="Vehicle Registration No." value={form.vehicleNo}
onChange={e => set('vehicleNo', e.target.value.toUpperCase())} error={!!errors.vehicleNo}
helperText={errors.vehicleNo || (form.vehicle === 'Cycle' ? 'Optional for cycles' : '')}
placeholder="DL-00-AB-0000" sx={{ '& input': { fontFamily: 'monospace' } }} />
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
<FormControlLabel
control={<Switch checked={form.verified} onChange={e => set('verified', e.target.checked)} color="success" />}
label={
<Stack direction="row" gap={1.5} alignItems="center">
<GppGoodOutlinedIcon sx={{ color: form.verified ? '#1E8E3E' : '#9AA0A6' }} />
<Box>
<Typography fontWeight={600}>KYC Verified</Typography>
<Typography variant="caption" color="text.secondary">Driving licence & ID proof validated</Typography>
</Box>
</Stack>
}
/>
</Paper>
</Stack>
)}
</DialogContent>
<Divider />
<DialogActions sx={{ px: 4, py: 3 }}>
<Button onClick={onClose} sx={{ borderRadius: 2 }}>Cancel</Button>
<Box sx={{ flex: 1 }} />
{activeStep > 0 && <Button onClick={back} variant="outlined" sx={{ borderRadius: 2 }}>Back</Button>}
{activeStep < STEPS.length - 1 ? (
<Button onClick={next} variant="contained" sx={{ borderRadius: 2, bgcolor: '#C01227', '&:hover': { bgcolor: '#9E0E20' } }}>
Continue
</Button>
) : (
<Button onClick={submit} variant="contained" startIcon={<CheckCircleOutlinedIcon />} sx={{ borderRadius: 2, bgcolor: '#C01227', '&:hover': { bgcolor: '#9E0E20' } }}>
{mode === 'add' ? 'Add Miler' : 'Save Changes'}
</Button>
)}
</DialogActions>
</Dialog>
);
}
// Profile Drawer - view + inline edit, in the same right-side sheet
function ProfileDrawer({ rider: riderProp, onClose, onSave, startInEdit = false }) {
const [editing, setEditing] = useState(startInEdit);
// Retain the last opened miler so the sheet keeps rendering its content while it
// slides OUT — the prop becomes null on close, and unmounting here (an early
// `return null`) would kill the exit animation and make closing feel instant.
const [retained, setRetained] = useState(riderProp);
const [form, setForm] = useState(riderProp || {});
// Reset the editable copy + mode whenever a different miler is opened.
useEffect(() => {
if (riderProp) {
setRetained(riderProp);
setForm(riderProp);
setEditing(startInEdit);
}
}, [riderProp, startInEdit]);
const open = Boolean(riderProp); // drives the slide; false triggers the exit anim
const rider = riderProp || retained; // keep content during the close transition
// NOTE: we deliberately do NOT early-return when there's no rider. The Drawer stays
// mounted (open=false) from first render, so the first click is a real false→true
// transition and animates — otherwise the first open mounts already-open and skips it.
const sr = rider ? successRate(rider) : 0;
const setField = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }));
const cancelEdit = () => { setForm(rider); setEditing(false); };
const saveEdit = () => {
const updated = { ...rider, ...form, capacity: VEHICLES[form.vehicle]?.capacity ?? rider.capacity };
onSave(updated);
setEditing(false);
};
return (
<Drawer
anchor="right"
open={open}
onClose={onClose}
// Slow the slide a touch (default ~225ms feels abrupt) for a smoother open/close.
transitionDuration={{ enter: 400, exit: 340 }}
sx={{
// This build renders temporary drawers at zIndex.drawer (1200), which sits
// BELOW the app bar (drawer + 1). On mobile the sheet starts at top:0, so its
// Close button would hide under the app bar. Lift the whole modal above it.
zIndex: (t) => t.zIndex.modal,
'& .MuiDrawer-paper': {
width: { xs: '100%', sm: 520 },
maxWidth: '100%',
// Run the sheet the full height of the viewport so there is no empty
// strip above the red header (zIndex.modal keeps Close clickable).
top: 0,
height: '100%',
borderTopLeftRadius: { sm: 16 },
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
boxShadow: '-8px 0 30px rgba(0,0,0,0.12)',
},
}}
>
{rider && (<>
{/* Header */}
<Box sx={{ background: 'linear-gradient(135deg, #C01227 0%, #8D0E1D 100%)', px: 2.75, py: 2.25, color: '#fff', flexShrink: 0 }}>
<Button onClick={onClose} startIcon={<ArrowBackRoundedIcon />} size="small"
sx={{ color: 'rgba(255,255,255,0.95)', textTransform: 'none', fontWeight: 600, mb: 1.25, ml: -0.5, '&:hover': { bgcolor: 'rgba(255,255,255,0.12)' } }}>
Close
</Button>
<Typography variant="h6" sx={{ fontWeight: 800, lineHeight: 1.25,fontSize: '1.5rem' }}>{form.name || rider.name}</Typography>
<Typography variant="body2" sx={{ opacity: 0.9, fontFamily: 'monospace', mb: 0.25 }}>{rider.id}</Typography>
<Stack direction="row" alignItems="center" spacing={0.75} useFlexGap sx={{ flexWrap: 'wrap', mt: 2 }}>
<Chip size="small" label={form.status || rider.status}
sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700 }} />
{rider.verified && (
<Chip size="small" icon={<VerifiedOutlinedIcon sx={{ color: '#fff !important' }} />} label="Verified"
sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700 }} />
)}
<Chip size="small" label={editing ? 'Editing…' : `${rider.rating} ★ · ${sr}%`}
sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700 }} />
</Stack>
</Box>
{/* Content */}
<Box sx={{ p: 2, bgcolor: '#F8F9FB', flex: 1, overflow: 'auto' }}>
{editing ? (
/* ── Inline edit form (same sheet) ── */
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, border: '1px solid #E2E8F0', bgcolor: '#FFFFFF' }}>
<Typography variant="overline" sx={{ color: '#6C757D', fontWeight: 700, letterSpacing: 1, display: 'block', mb: 2.5, fontSize: '0.75rem' }}>
EDIT MILER DETAILS
</Typography>
<Stack spacing={2.5}>
<TextField label="Full name" value={form.name || ''} onChange={setField('name')} fullWidth size="small" />
<TextField label="Phone" value={form.phone || ''} onChange={setField('phone')} fullWidth size="small" />
<FormControl fullWidth size="small">
<InputLabel>Status</InputLabel>
<Select label="Status" value={form.status || ''} onChange={setField('status')}>
{STATUS_OPTIONS.map((s) => <MenuItem key={s} value={s}>{s}</MenuItem>)}
</Select>
</FormControl>
<FormControl fullWidth size="small">
<InputLabel>Vehicle</InputLabel>
<Select label="Vehicle" value={form.vehicle || ''} onChange={setField('vehicle')}>
{VEHICLE_TYPES.map((v) => <MenuItem key={v} value={v}>{v}</MenuItem>)}
</Select>
</FormControl>
<TextField label="Vehicle number" value={form.vehicleNo || ''} onChange={setField('vehicleNo')} fullWidth size="small" />
<FormControl fullWidth size="small">
<InputLabel>Hub</InputLabel>
<Select label="Hub" value={form.hub || ''} onChange={setField('hub')}>
{HUB_OPTIONS.map((h) => <MenuItem key={h} value={h}>{h}</MenuItem>)}
</Select>
</FormControl>
<Autocomplete
multiple
options={ALL_ZONES}
value={form.zones || []}
onChange={(_, v) => setForm((f) => ({ ...f, zones: v }))}
renderInput={(params) => <TextField {...params} label="Zones" size="small" placeholder="Add zone" />}
/>
</Stack>
</Paper>
) : (
<Grid container spacing={1.75}>
{/* Today's Performance */}
<Grid size={12}>
<Typography variant="overline" sx={{ color: '#6C757D', fontWeight: 700, letterSpacing: 1, display: 'block', mb: 1.5, fontSize: '0.75rem' }}>
TODAY'S PERFORMANCE
</Typography>
<Grid container spacing={1.5}>
{[
{ label: "Picked up", value: rider.deliveriesDone, icon: CheckCircleOutlinedIcon, color: "#16A34A", bg: "#DCFCE7" },
{ label: "Failed", value: rider.deliveriesFailed, icon: WarningAmberOutlinedIcon, color: "#DC2626", bg: "#FEE2E2" },
{ label: "COP", value: inr(rider.codCollected), icon: PaymentsOutlinedIcon, color: "#D97706", bg: "#FEF3C7" },
].map((stat, i) => (
<Grid size={4} key={i}>
<StatCard icon={stat.icon} label={stat.label} value={stat.value} color={stat.color} bg={stat.bg} />
</Grid>
))}
</Grid>
</Grid>
{/* Live Load Panel */}
<Grid size={12}>
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, border: '1px solid #E2E8F0', bgcolor: '#FFFFFF' }}>
<Typography variant="overline" sx={{ color: '#6C757D', fontWeight: 700, letterSpacing: 1, display: 'block', mb: 1.75, fontSize: '0.75rem' }}>
LIVE LOAD
</Typography>
<CapacityBar rider={rider} showLabel />
<Divider sx={{ my: 2 }} />
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ gap: 2, width: '100%' }}>
<Typography variant="body2" sx={{ color: '#6C757D', fontWeight: 600 }}>Pending pickups</Typography>
<Typography sx={{ fontSize: '1.5rem', fontWeight: 800, color: '#1E293B', lineHeight: 1 }}>{rider.pickupsPending}</Typography>
</Stack>
</Paper>
</Grid>
{/* Miler Details Panel */}
<Grid size={12}>
<Paper elevation={0} sx={{ p: 2, borderRadius: 2, border: '1px solid #E2E8F0', bgcolor: '#FFFFFF' }}>
<Typography variant="overline" sx={{ color: '#6C757D', fontWeight: 700, letterSpacing: 1, display: 'block', mb: 2, fontSize: '0.75rem' }}>
MILER DETAILS
</Typography>
<Grid container spacing={2.5}>
{[
{ icon: PhoneOutlinedIcon, label: "Phone", value: rider.phone },
{ icon: HomeWorkOutlinedIcon, label: "Hub", value: rider.hub },
{ icon: PlaceOutlinedIcon, label: "Zones", value: rider.zones?.join(', ') || 'N/A' },
{ icon: BadgeOutlinedIcon, label: "Vehicle", value: `${rider.vehicle} • ${rider.vehicleNo}` },
{ icon: AccessTimeOutlinedIcon, label: "Check-in", value: `${rider.checkInTime} (${rider.hoursToday}h)` },
].map((detail, idx) => (
<Grid size={6} key={idx}>
<Stack direction="row" spacing={1.5} alignItems="flex-start">
<detail.icon sx={{ color: '#94A3B8', fontSize: 20, mt: '2px' }} />
<Box sx={{ minWidth: 0 }}>
<Typography variant="caption" sx={{ color: '#6C757D', display: 'block', fontSize: '0.72rem', lineHeight: 1.3 }}>{detail.label}</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, color: '#334155', display: 'block', wordBreak: 'break-word' }}>{detail.value}</Typography>
</Box>
</Stack>
</Grid>
))}
</Grid>
</Paper>
</Grid>
</Grid>
)}
</Box>
{/* Footer */}
<Box sx={{ p: 2.5, borderTop: '1px solid #E9ECEF', bgcolor: '#fff', display: 'flex', gap: 1.5, justifyContent: 'flex-end', flexShrink: 0 }}>
{editing ? (
<>
<Button fullWidth variant="outlined" onClick={cancelEdit} sx={{ borderRadius: 2 }}>
Cancel
</Button>
<Button fullWidth variant="contained" startIcon={<CheckCircleOutlinedIcon />} onClick={saveEdit} sx={{ borderRadius: 2, bgcolor: '#C01227', '&:hover': { bgcolor: '#9E0E1F' } }}>
Save Changes
</Button>
</>
) : (
<>
<Button fullWidth variant="outlined" startIcon={<CallOutlinedIcon />} href={`tel:${rider.phone}`} sx={{ borderRadius: 2 }}>
Call Miler
</Button>
<Button fullWidth variant="contained" startIcon={<EditOutlinedIcon />} onClick={() => setEditing(true)} sx={{ borderRadius: 2, bgcolor: '#C01227', '&:hover': { bgcolor: '#9E0E1F' } }}>
Edit Details
</Button>
</>
)}
</Box>
</>)}
</Drawer>
);
}
// Delete Dialog
function DeleteDialog({ rider, onClose, onConfirm }) {
return (
<Dialog open={!!rider} onClose={onClose} maxWidth="xs" fullWidth>
<DialogContent sx={{ textAlign: 'center', pt: 5, pb: 3 }}>
<Avatar sx={{ mx: 'auto', bgcolor: '#FCE8E6', width: 70, height: 70 }}>
<DeleteOutlineIcon sx={{ color: '#D93025', fontSize: 36 }} />
</Avatar>
<Typography variant="h5" fontWeight={700} mt={3}>Remove Miler?</Typography>
<Typography color="text.secondary" sx={{ mt: 1 }}>
{rider?.name} will be permanently removed from the roster.
</Typography>
</DialogContent>
<DialogActions sx={{ px: 4, pb: 4, justifyContent: 'center', gap: 2 }}>
<Button onClick={onClose} variant="outlined" sx={{ borderRadius: 2 }}>Cancel</Button>
<Button onClick={onConfirm} variant="contained" color="error" sx={{ borderRadius: 2 }}>Yes, Remove</Button>
</DialogActions>
</Dialog>
);
}
// Miler Card (Grid)
function RiderCard({ rider, onView, onMenu }) {
const pct = loadPct(rider);
const pctColor =
pct > 85 ? "#D93025" : pct > 60 ? "#F29900" : "#1A73E8";
return (
<Card
elevation={0}
onClick={() => onView(rider)}
sx={{
height: "100%",
borderRadius: 2,
border: "1px solid #E8EAED",
cursor: "pointer",
transition: "all .25s ease",
bgcolor: "#fff",
"&:hover": {
transform: "translateY(-4px)",
borderColor: "#C01227",
boxShadow: "0 16px 40px rgba(0,0,0,.08)",
},
}}
>
<CardContent
sx={{
p: 4,
"&:last-child": {
pb: 4,
},
}}
>
{/* Header */}
<Stack
direction="row"
spacing={2.5}
alignItems="flex-start"
>
<RiderAvatar rider={rider} size={54} />
<Box
sx={{
flex: 1,
minWidth: 0,
}}
>
<Stack
direction="row"
spacing={0.75}
alignItems="center"
>
<Typography
fontWeight={700}
fontSize={18}
noWrap
>
{rider.name}
</Typography>
{rider.verified && (
<VerifiedOutlinedIcon
sx={{
fontSize: 18,
color: "#1A73E8",
}}
/>
)}
</Stack>
<Typography
sx={{
mt: 0.5,
fontSize: 13,
color: "#9AA0A6",
fontFamily: "monospace",
letterSpacing: ".5px",
}}
>
{rider.id}
</Typography>
</Box>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
onMenu(e, rider);
}}
>
<MoreVertOutlinedIcon />
</IconButton>
</Stack>
{/* Status */}
<Box sx={{ mt: 3 }}>
<StatusPill status={rider.status} size="sm" />
</Box>
<Divider sx={{ my: 3 }} />
{/* Load */}
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
sx={{ mb: 1.5 }}
>
<Typography
fontSize={13}
fontWeight={600}
color="text.secondary"
>
Today's Load
</Typography>
<Typography
fontWeight={700}
fontSize={14}
>
{rider.assigned}/{rider.capacity}
<Box
component="span"
sx={{
color: pctColor,
ml: 1,
}}
>
{pct}%
</Box>
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={pct}
sx={{
height: 8,
borderRadius: 2,
bgcolor: "#ECEFF3",
"& .MuiLinearProgress-bar": {
bgcolor: pctColor,
borderRadius: 2,
},
}}
/>
</Box>
{/* Zones */}
<Box sx={{ mt: 3.5 }}>
<Typography
variant="caption"
sx={{
display: "block",
color: "#6B7280",
fontWeight: 600,
mb: 1.5,
letterSpacing: ".3px",
}}
>
SERVICE ZONES
</Typography>
<Stack
direction="row"
spacing={1}
useFlexGap
sx={{ flexWrap: 'wrap' }}
>
{rider.zones.slice(0, 3).map((z) => (
<Chip
key={z}
label={z}
size="small"
sx={{
bgcolor: "#F4F6F8",
color: "#495057",
borderRadius: 2,
fontWeight: 600,
px: 0.5,
}}
/>
))}
{rider.zones.length > 3 && (
<Chip
label={`+${rider.zones.length - 3}`}
size="small"
sx={{
bgcolor: "#EEF2FF",
color: "#4F46E5",
borderRadius: 2,
fontWeight: 700,
}}
/>
)}
</Stack>
</Box>
</CardContent>
</Card>
);
}
// ════════════════════════════════════════════════════════════════════════════════
// MAIN COMPONENT
// ════════════════════════════════════════════════════════════════════════════════
export default function Riders() {
const theme = useTheme();
const isMdDown = useMediaQuery(theme.breakpoints.down('md'));
const isLgDown = useMediaQuery(theme.breakpoints.down('lg'));
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);
const [deleteTarget, setDeleteTarget] = useState(null);
const [menuAnchor, setMenuAnchor] = useState(null);
const [menuRider, setMenuRider] = useState(null);
const [snack, setSnack] = useState({ open: false, msg: '', severity: 'success' });
const toast = (msg, severity = 'success') => setSnack({ open: true, msg, severity });
const effectiveView = isLgDown ? 'grid' : view;
const kpi = useMemo(() => {
const onDuty = riders.filter(r => !['Offline', 'Suspended'].includes(r.status)).length;
const onField = riders.filter(r => ['On Pickup', 'Returning to Hub'].includes(r.status)).length;
const idle = riders.filter(r => r.status === 'Idle').length;
const slaRisk = riders.filter(r => loadPct(r) > 85 || r.deliveriesFailed >= 5).length;
const codToCollect = riders.reduce((s, r) => s + r.codPending, 0);
return { onDuty, onField, idle, slaRisk, codToCollect, total: riders.length };
}, [riders]);
const filtered = useMemo(() => {
const q = search.trim().toLowerCase();
return riders.filter(r => {
const matchSearch = !q ||
r.name.toLowerCase().includes(q) ||
r.id.toLowerCase().includes(q) ||
r.phone.includes(q) ||
r.zones.some(z => z.toLowerCase().includes(q));
const matchStatus = statusFilter === 'All' || r.status === statusFilter;
const matchVehicle = vehicleFilter === 'All' || r.vehicle === vehicleFilter;
return matchSearch && matchStatus && matchVehicle;
});
}, [riders, search, statusFilter, vehicleFilter]);
// 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');
}
};
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 });
// Edit now opens the same right-side sheet in edit mode (no separate modal).
const openEdit = (r) => {
setMenuAnchor(null);
setProfile(r);
setProfileEdit(true);
};
const openView = (r) => {
setMenuAnchor(null);
setProfile(r);
setProfileEdit(false);
};
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);
setMenuRider(r);
};
const STATUS_CHIPS = ['All', 'On Pickup', 'Idle', 'On Break', 'Offline', 'Suspended'];
return (
<Box sx={{ pt: { xs: 1, md: 1 }, pb: 1 }}>
{/* Header */}
{/* ── Improved Header with better gaps ── */}
<Stack
direction={{ xs: 'column', md: 'row' }}
justifyContent="space-between"
alignItems={{ xs: 'stretch', md: 'center' }}
gap={{ xs: 2.5, md: 3 }}
mb={4}
>
{/* Title & Avatar Branding Group */}
<Stack
direction="row"
alignItems="center"
spacing={5} // Increase this value (5 = 40px)
sx={{ minWidth: 0 }}
>
<Avatar
sx={{
bgcolor: alpha('#C01227', 0.1),
color: '#C01227',
width: 56,
height: 56,
borderRadius: 2,
flexShrink: 0
}}
>
<GroupsOutlinedIcon sx={{ fontSize: 30 }} />
</Avatar>
<Box
sx={{
minWidth: 0,
ml: 1 // Additional left margin for extra spacing
}}
>
<Typography
variant="h4"
sx={{
fontWeight: 800,
letterSpacing: '-0.4px',
lineHeight: 1.2,
mb: 1,
fontSize: {
xs: '1.6rem',
sm: '2rem'
}
}}
>
Milers
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{
fontWeight: 500,
lineHeight: 1.6
}}
>
{kpi.total} milers who's working and where
</Typography>
</Box>
</Stack>
{/* Actions Group - pushed to the far right with clear spacing between buttons */}
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={2.5}
alignItems="center"
justifyContent="flex-end"
sx={{
flexShrink: 0,
ml: { md: "auto" },
}}
>
<Button
variant="outlined"
startIcon={<DownloadOutlinedIcon />}
onClick={() => toast("Exported as CSV")}
sx={{
width: { xs: "100%", sm: "auto" },
borderRadius: 2,
px: 3,
height: 48,
textTransform: "none",
fontWeight: 600,
mr: { sm: 1 }, // Extra gap on desktop
}}
>
Export Roster
</Button>
<Button
variant="contained"
startIcon={<AddOutlinedIcon />}
onClick={openAdd}
sx={{
width: { xs: "100%", sm: "auto" },
borderRadius: 2,
bgcolor: "#C01227",
px: 3.5,
height: 48,
textTransform: "none",
fontWeight: 600,
boxShadow: "none",
"&:hover": {
bgcolor: "#9E0E1F",
boxShadow: "none",
},
}}
>
Add New Miler
</Button>
</Stack>
</Stack>
{/* KPIs */}
<Grid container spacing={{ xs: 1.5, sm: 2 }} sx={{ mb: 3 }}>
{[
{ icon: GroupsOutlinedIcon, label: 'On Duty', value: kpi.onDuty, sub: `of ${kpi.total}`, color: '#1A73E8', bg: '#E8F0FE', trend: true },
{ icon: RouteOutlinedIcon, label: 'In Field', value: kpi.onField, sub: 'Active pickups', color: '#1E8E3E', bg: '#E6F4EA' },
{ icon: AccessTimeOutlinedIcon, label: 'Available', value: kpi.idle, sub: 'Ready for assignment', color: '#B06000', bg: '#FEF7E0' },
{ icon: WarningAmberOutlinedIcon, label: 'SLA Risk', value: kpi.slaRisk, sub: 'Overloaded milers', color: '#D93025', bg: '#FCE8E6' },
{ icon: PaymentsOutlinedIcon, label: 'COP Pending', value: inr(kpi.codToCollect), sub: 'To be collected', color: '#8E24AA', bg: '#F3E5F5' },
].map((k, i) => (
<Grid size={{ xs: 6, md: 2.4 }} key={i}>
<KpiCard {...k} />
</Grid>
))}
</Grid>
{/* Main Card */}
<Card sx={{ borderRadius: 2, border: '1px solid #ECEEF1', overflow: 'hidden', boxShadow: '0 4px 20px rgba(0,0,0,0.04)' }}>
{/* Toolbar */}
<Box sx={{ p: { xs: 2.5, md: 2 }, borderBottom: '1px solid #F1F3F5' } } >
<Stack
direction={{ xs: "column", lg: "row" }}
spacing={1}
alignItems="center"
sx={{ mb: 2 }}
>
<TextField
fullWidth
size="medium"
placeholder="Search milers by name, ID, phone or zone..."
value={search}
onChange={(e) => setSearch(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchOutlinedIcon />
</InputAdornment>
),
sx: { borderRadius: 2 },
}}
sx={{
maxWidth: { lg: 420 },
}}
/>
<FormControl
sx={{
minWidth: 220,
}}
>
<Select
value={vehicleFilter}
onChange={(e) => setVehicleFilter(e.target.value)}
sx={{ borderRadius: 2 }}
>
<MenuItem value="All">All Vehicles</MenuItem>
{VEHICLE_TYPES.map((v) => (
<MenuItem key={v} value={v}>
{v}
</MenuItem>
))}
</Select>
</FormControl>
<Box flex={1} />
{!isLgDown && (
<ToggleButtonGroup
exclusive
value={view}
onChange={(_, v) => v && setView(v)}
sx={{ borderRadius: 2 }}
>
<ToggleButton value="table">
<TableRowsOutlinedIcon />
</ToggleButton>
<ToggleButton value="grid">
<GridViewOutlinedIcon />
</ToggleButton>
</ToggleButtonGroup>
)}
</Stack>
{/* Status Chips */}
<Stack direction="row" sx={{ flexWrap: 'wrap', columnGap: 1.25, rowGap: 1.5, mt: 4 }}>
{STATUS_CHIPS.map(s => {
const active = statusFilter === s;
const meta = STATUS_META[s] || {};
// Each status carries its own colour; "All" falls back to a neutral dark.
const accent = meta.color || '#1A1A2E';
const tint = meta.bg || '#F8F9FB';
return (
<Chip
key={s}
label={s}
clickable
onClick={() => setStatusFilter(s)}
icon={meta.color ? <CircleRoundedIcon sx={{ fontSize: 10, color: active ? '#fff' : meta.color }} /> : undefined}
sx={{
height: 38,
px: 1,
borderRadius: 2,
fontSize: '0.82rem',
fontWeight: active ? 700 : 600,
bgcolor: active ? accent : tint,
color: active ? '#fff' : accent,
border: active ? 'none' : `1px solid ${alpha(accent, 0.25)}`,
'& .MuiChip-icon': { color: active ? '#fff' : meta.color },
'& .MuiChip-label': { px: 1.25 },
'&:hover': { bgcolor: active ? accent : alpha(accent, 0.14) }
}}
/>
);
})}
</Stack>
</Box>
{/* Content Area */}
{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>
<Button onClick={() => { setSearch(''); setStatusFilter('All'); setVehicleFilter('All'); }} sx={{ mt: 3 }} variant="outlined">
Clear all filters
</Button>
</Box>
) : effectiveView === 'table' ? (
<TableContainer>
<Table>
<TableHead>
<TableRow sx={{ bgcolor: '#FAFBFC' }}>
<TableCell sx={{ fontWeight: 700, color: '#6C757D' }}>Miler</TableCell>
<TableCell sx={{ fontWeight: 700, color: '#6C757D' }}>Status</TableCell>
<TableCell sx={{ fontWeight: 700, color: '#6C757D' }}>Zones</TableCell>
<TableCell sx={{ fontWeight: 700, color: '#6C757D' }}>Vehicle</TableCell>
<TableCell sx={{ fontWeight: 700, color: '#6C757D' }}>Load</TableCell>
<TableCell sx={{ fontWeight: 700, color: '#6C757D' }}>Today</TableCell>
<TableCell align="right" />
</TableRow>
</TableHead>
<TableBody>
{filtered.map(rider => (
<TableRow key={rider.id} hover sx={{ cursor: 'pointer' }} onClick={() => openView(rider)}>
<TableCell>
<Stack direction="row" gap={2} alignItems="center" sx={{ mb: 1 }}>
<Box sx={{ mr: 1 }}>
<RiderAvatar rider={rider} size={42} />
</Box>
<Box>
<Typography fontWeight={600}>{rider.name}</Typography>
<Typography variant="caption" sx={{ color: '#9AA0A6' }}>{rider.id}</Typography>
</Box>
</Stack>
</TableCell>
<TableCell><StatusPill status={rider.status} /></TableCell>
<TableCell>
<Stack direction="row" sx={{ flexWrap: 'wrap', gap: 0.5 }}>
{rider.zones.map(z => <Chip key={z} label={z} size="small" variant="outlined" />)}
</Stack>
</TableCell>
<TableCell><VehicleCell vehicle={rider.vehicle} vehicleNo={rider.vehicleNo} /></TableCell>
<TableCell><CapacityBar rider={rider} /></TableCell>
<TableCell>
<Stack direction="row" spacing={4}>
<Box>
<Typography variant="caption" color="text.secondary">Done</Typography>
<Typography fontWeight={700} color="#1E8E3E">{rider.deliveriesDone}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">COP</Typography>
<Typography fontWeight={700}>{inr(rider.codCollected)}</Typography>
</Box>
</Stack>
</TableCell>
<TableCell align="right">
<Stack direction="row" gap={1} justifyContent="flex-end">
<Tooltip title="View"><IconButton onClick={(e) => { e.stopPropagation(); openView(rider); }}><VisibilityOutlinedIcon /></IconButton></Tooltip>
<Tooltip title="Edit"><IconButton onClick={(e) => { e.stopPropagation(); openEdit(rider); }}><EditOutlinedIcon /></IconButton></Tooltip>
<IconButton onClick={(e) => { e.stopPropagation(); openMenu(e, rider); }}><MoreVertOutlinedIcon /></IconButton>
</Stack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Box sx={{ p: { xs: 2.5, md: 4 } }}>
<Grid container spacing={{ xs: 2.5, md: 3 }}>
{filtered.map(r => (
<Grid size={{ xs: 12, sm: 6, lg: 4 }} key={r.id}>
<RiderCard rider={r} onView={openView} onMenu={openMenu} />
</Grid>
))}
</Grid>
</Box>
)}
{filtered.length > 0 && (
<Box sx={{ px: 4, py: 2.5, borderTop: '1px solid #F1F3F5', bgcolor: '#FAFBFC' }}>
<Typography variant="body2" color="text.secondary">
Showing {filtered.length} of {riders.length} milers
</Typography>
</Box>
)}
</Card>
{/* Context Menu */}
<Menu anchorEl={menuAnchor} open={Boolean(menuAnchor)} onClose={() => setMenuAnchor(null)} PaperProps={{ sx: { borderRadius: 2 } }}>
<MenuItem onClick={() => openView(menuRider)}><ListItemIcon><VisibilityOutlinedIcon /></ListItemIcon>View Profile</MenuItem>
<MenuItem onClick={() => openEdit(menuRider)}><ListItemIcon><EditOutlinedIcon /></ListItemIcon>Edit</MenuItem>
<MenuItem component="a" href={`tel:${menuRider?.phone}`}><ListItemIcon><CallOutlinedIcon /></ListItemIcon>Call</MenuItem>
<Divider />
<MenuItem onClick={() => { setDeleteTarget(menuRider); setMenuAnchor(null); }} sx={{ color: '#D93025' }}>
<ListItemIcon><DeleteOutlineIcon sx={{ color: '#D93025' }} /></ListItemIcon>Remove Miler
</MenuItem>
</Menu>
{/* Dialogs */}
<RiderFormDialog open={formDialog.open} mode={formDialog.mode} initial={formDialog.initial} onClose={() => setFormDialog({ open: false, mode: 'add', initial: null })} onSave={handleSave} />
<ProfileDrawer
rider={profile}
startInEdit={profileEdit}
onClose={() => { setProfile(null); setProfileEdit(false); }}
onSave={saveProfileEdit}
/>
<DeleteDialog rider={deleteTarget} onClose={() => setDeleteTarget(null)} onConfirm={handleDelete} />
{/* Toast */}
<Snackbar open={snack.open} autoHideDuration={2500} onClose={() => setSnack(s => ({ ...s, open: false }))}>
<Alert severity={snack.severity} variant="filled" sx={{ borderRadius: 2 }}>{snack.msg}</Alert>
</Snackbar>
</Box>
);
}