Files
doormile_app_web/src/pages/riders/Riders.jsx
2026-06-05 17:28:05 +05:30

222 lines
11 KiB
JavaScript

import { useState, useMemo, Fragment } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Grid, Card, Stack, Button, TextField, MenuItem, InputAdornment, Box, Tabs, Tab,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, Tooltip,
TablePagination, Typography, Chip, Collapse
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import PowerSettingsNewOutlinedIcon from '@mui/icons-material/PowerSettingsNewOutlined';
import PageHeader from '@/components/PageHeader';
import StatCard from '@/components/StatCard';
import StatusChip from '@/components/StatusChip';
import UserAvatar from '@/components/UserAvatar';
import TabLabelCount from '@/components/TabLabelCount';
import { riders, riderLogs, locations } from '@/data/mock';
import { inr } from '@/utils/format';
const TABS = [
{ key: 'all', label: 'ALL' },
{ key: 'active', label: 'Active' }
];
export default function Riders() {
const navigate = useNavigate();
const [tab, setTab] = useState(0);
const [search, setSearch] = useState('');
const [location, setLocation] = useState('all');
const [page, setPage] = useState(0);
const [rpp, setRpp] = useState(5);
const [expanded, setExpanded] = useState(null);
const tabKey = TABS[tab].key;
const filtered = useMemo(
() =>
riders.filter((r) => {
const matchTab = tabKey === 'all' ? true : r.status !== 'offline';
const matchLocation = location === 'all' || r.address.includes(location);
const matchSearch =
!search ||
[r.id, r.userId, r.name, r.phone, r.address, r.vehicle, r.vehicleNo]
.join(' ')
.toLowerCase()
.includes(search.toLowerCase());
return matchTab && matchLocation && matchSearch;
}),
[tabKey, location, search]
);
const paged = filtered.slice(page * rpp, page * rpp + rpp);
const counts = {
total: riders.length,
active: riders.filter((r) => r.status === 'online').length,
onDelivery: riders.filter((r) => r.status === 'on-delivery').length,
offline: riders.filter((r) => r.status === 'offline').length,
all: riders.length,
activeTab: riders.filter((r) => r.status !== 'offline').length
};
return (
<>
<PageHeader
title="Riders"
breadcrumbs={[{ label: 'Riders' }]}
action={
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'center' }}>
<TextField select size="small" value={location} onChange={(e) => setLocation(e.target.value)} sx={{ minWidth: 170 }} label="Location">
<MenuItem value="all">All Locations</MenuItem>
{locations.map((l) => <MenuItem key={l} value={l}>{l}</MenuItem>)}
</TextField>
<Button variant="contained" startIcon={<AddIcon />} onClick={() => navigate('/riders/create')}>
Add Rider
</Button>
</Stack>
}
/>
<Grid container spacing={2.5} sx={{ mb: 1 }}>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Total Riders" value={counts.total} icon={GroupsOutlinedIcon} caption="All registered" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Active" value={counts.active} icon={CheckCircleOutlineIcon} color="success" caption="Online now" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="On Delivery" value={counts.onDelivery} icon={TwoWheelerOutlinedIcon} color="info" caption="In transit" /></Grid>
<Grid item xs={12} sm={6} lg={3}><StatCard title="Offline" value={counts.offline} icon={PowerSettingsNewOutlinedIcon} color="error" caption="Not available" /></Grid>
</Grid>
<Card sx={{ mt: 1.5 }}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5} sx={{ p: 2 }} alignItems={{ md: 'center' }}>
<TextField
size="small" placeholder="Search riders…" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }}
sx={{ minWidth: 240 }}
InputProps={{ startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment> }}
/>
<Box sx={{ flexGrow: 1 }} />
</Stack>
<Box sx={{ px: 2, borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tab} onChange={(_, v) => { setTab(v); setPage(0); }}>
{TABS.map((t, i) => (
<Tab
key={t.key}
label={<TabLabelCount label={t.label} count={t.key === 'all' ? counts.all : counts.activeTab} active={tab === i} />}
/>
))}
</Tabs>
</Box>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>S.NO</TableCell>
<TableCell>User ID</TableCell>
<TableCell>Rider</TableCell>
<TableCell>Address</TableCell>
<TableCell>Vehicle</TableCell>
<TableCell>Shift</TableCell>
<TableCell>Time</TableCell>
<TableCell align="right">Fare</TableCell>
<TableCell align="right">Fuel</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paged.map((r, i) => (
<Fragment key={r.id}>
<TableRow hover>
<TableCell>{page * rpp + i + 1}</TableCell>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{r.userId}</TableCell>
<TableCell>
<Stack direction="row" spacing={1.25} alignItems="center">
<UserAvatar name={r.name} size={32} />
<Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>{r.name}</Typography>
<Typography variant="caption" color="text.secondary">{r.phone}</Typography>
</Box>
</Stack>
</TableCell>
<TableCell>{r.address}</TableCell>
<TableCell>
<Typography variant="body2">{r.vehicle}</Typography>
<Typography variant="caption" color="text.secondary">{r.vehicleNo}</Typography>
</TableCell>
<TableCell>{r.shift}</TableCell>
<TableCell>
<Stack direction="row" spacing={0.75} alignItems="center">
<Chip size="small" label={r.start} sx={{ bgcolor: 'success.lighter', color: 'success.main', fontWeight: 600 }} />
<Chip size="small" label={r.end} sx={{ bgcolor: 'error.lighter', color: 'error.main', fontWeight: 600 }} />
</Stack>
</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>{inr(r.fare)}</TableCell>
<TableCell align="right">{inr(r.fuel)}</TableCell>
<TableCell><StatusChip status={r.status} /></TableCell>
<TableCell align="center">
<Tooltip title="Edit">
<IconButton size="small" onClick={() => navigate(`/riders/${r.id}/edit`)}><EditOutlinedIcon fontSize="small" /></IconButton>
</Tooltip>
<Tooltip title={expanded === r.id ? 'Hide logs' : 'View logs'}>
<IconButton size="small" onClick={() => setExpanded(expanded === r.id ? null : r.id)}>
{expanded === r.id ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={11} sx={{ p: 0, border: 0 }}>
<Collapse in={expanded === r.id} timeout="auto" unmountOnExit>
<Box sx={{ p: 2.5, bgcolor: 'grey.50' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1.5 }}>Rider Logs</Typography>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Location</TableCell>
<TableCell>Battery</TableCell>
<TableCell>Charging</TableCell>
<TableCell>Speed</TableCell>
<TableCell>Accuracy</TableCell>
<TableCell>Time</TableCell>
<TableCell>Order</TableCell>
<TableCell>Status</TableCell>
</TableRow>
</TableHead>
<TableBody>
{riderLogs.map((log, li) => (
<TableRow key={li}>
<TableCell>{log.location}</TableCell>
<TableCell>{log.battery}</TableCell>
<TableCell>{log.charging}</TableCell>
<TableCell>{log.speed}</TableCell>
<TableCell>{log.accuracy}</TableCell>
<TableCell>{log.time}</TableCell>
<TableCell sx={{ fontWeight: 600, color: 'primary.main' }}>{log.order}</TableCell>
<TableCell><StatusChip status={log.status} /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
</Fragment>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div" count={filtered.length} page={page} onPageChange={(_, p) => setPage(p)}
rowsPerPage={rpp} onRowsPerPageChange={(e) => { setRpp(+e.target.value); setPage(0); }} rowsPerPageOptions={[5, 10, 25]}
/>
</Card>
</>
);
}