import * as React from 'react'; import { useState, useEffect, useRef, Fragment } from 'react'; import Geocode from 'react-geocode'; import { useNavigate } from 'react-router-dom'; import { Avatar, Paper, Stack, Typography, Table, TableCell, TableBody, TableHead, IconButton, TableRow, Tooltip, TableContainer, Backdrop, Collapse, Grid, Box, Skeleton, Divider } from '@mui/material'; var utc = require('dayjs/plugin/utc'); import dayjs from 'dayjs'; dayjs.extend(utc); import { MdDirectionsBike, MdMyLocation, MdPersonPin, MdCheckCircle, MdCancel, MdGroups, MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp, MdLocationOn, MdBatteryStd, MdPowerSettingsNew, MdSpeed, MdGpsFixed, MdAccessTime, MdInventory2, MdTwoWheeler } from 'react-icons/md'; import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import CircularLoader from 'components/CircularLoader'; import { fetchAllRiders, getallridersummary, getriderstatus } from 'pages/api/api'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; import { OpenToast } from 'components/third-party/OpenToast'; import axios from 'axios'; // ============================================================================ // Design tokens — shared with the deliveries / tenants / customers pages so // every surface (header, KPI tiles, table, badges, dialog) speaks the same // visual language. Brand purple `#662582` is the canonical primary; status // colours are semantic and distinct from the brand. // ============================================================================ const DT = { radiusPill: 999, radiusCard: 16, shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)', shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)', shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)', textPrimary: '#0f172a', textSecondary: '#64748b', textMuted: '#94a3b8', borderSubtle: '#e2e8f0', divider: '#f1f5f9', surface: '#ffffff', surfaceAlt: '#f8fafc' }; const a = (c, suffix) => `${c}${suffix}`; const tint = (c) => a(c, '08'); const soft = (c) => a(c, '18'); const ring = (c) => a(c, '26'); const edge = (c) => a(c, '55'); const BRAND = '#662582'; const BRAND_LIGHT = '#9255AB'; const SoftPaper = (props) => ( ); const AccentAvatar = ({ color, selected, size = 24, children }) => ( {children} ); // Status palette — semantic only (do NOT swap for brand purple). Used by the // per-row status badges and the lifecycle tabs (ALL, Active). const STATUS_META = { active: { label: 'Active', color: '#10b981', icon: MdCheckCircle }, inactive: { label: 'Inactive', color: '#ef4444', icon: MdCancel }, online: { label: 'Online', color: '#10b981', icon: MdCheckCircle }, offline: { label: 'Offline', color: '#ef4444', icon: MdCancel }, idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } }; // Pill-tab definitions for the rider listing tabs. Keeps brand purple for the // "ALL" view and emerald for "Active" so the colour matches the count's meaning. const TAB_META = [ { key: 0, label: 'All Riders', color: BRAND, icon: MdGroups, countKey: 'total' }, { key: 1, label: 'Active', color: '#10b981', icon: MdCheckCircle, countKey: 'active' } ]; const KPI_META = (summary) => [ { key: 'total', label: 'Total Riders', color: BRAND, icon: MdGroups, value: summary?.total ?? 0 }, { key: 'active', label: 'Active Riders', color: '#10b981', icon: MdCheckCircle, value: summary?.active ?? 0 }, { key: 'inactive', label: 'Inactive Riders', color: '#ef4444', icon: MdCancel, value: summary?.inactive ?? 0 } ]; const Riders = () => { const navigate = useNavigate(); const loadMoreRef = useRef(); const containerRef = useRef(); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [locaName, setLocoName] = useState('All'); const [appId, setAppId] = useState(0); const [tabvalue, setTabvalue] = useState(0); const roleid = localStorage.getItem('roleid'); const [logsRow, setLogsRow] = useState(null); const [riderLogsdata, setRiderLogsdata] = useState(null); Geocode.setApiKey(process.env.REACT_APP_GOOGLE_MAPS_API_KEY); const handleChangetab = (i) => { setTabvalue(i); setLogsRow(null); }; // ==============================|| getallridersummary||============================== // const { data: allRidersSummary, isLoading: riderSummarysLoading } = useQuery({ queryKey: ['allriders', appId, tabvalue], queryFn: getallridersummary }); // ==============================|| getRiderLogs (riders)||============================== // const getRiderLogs = async (userid) => { try { const res = await axios.get(`${process.env.REACT_APP_URL}/utils/getriderperiodiclogs?userid=${userid}`); if (res.data.data.length == 0) { setLogsRow(null); OpenToast(res.data.message, 'error', 2000); } else { setRiderLogsdata(res.data.data); } } catch (err) { OpenToast(err.message, 'error', 2000); } }; // ==============================|| getriderstatus||============================== // const { data: ridersStatus, isLoading: riderStatusLoading, isError: riderstatusIsError, error: riderStatusError } = useQuery({ queryKey: ['ridersStatus'], queryFn: getriderstatus }); // ==============================|| fetchAllRiders||============================== // const { data: allRidersData, isLoading: allRidersLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ['allriders', appId, debouncedSearch, tabvalue], queryFn: fetchAllRiders, getNextPageParam: (lastPage, pages) => (lastPage.details?.length ? pages.length + 1 : undefined) }); const rows = allRidersData?.pages.flatMap((page) => page.details) || []; useEffect(() => { if (!hasNextPage) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { fetchNextPage(); } }, { root: document.querySelector('.MuiTableContainer-root'), rootMargin: '0px', threshold: 1.0 } ); if (loadMoreRef.current) observer.observe(loadMoreRef.current); return () => { if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); }; }, [hasNextPage, fetchNextPage]); const handleScroll = (event) => { const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; if (scrollTop + clientHeight >= scrollHeight - 50) { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); } } }; const errMessage = riderstatusIsError ? riderStatusError : null; useEffect(() => { if (errMessage) { OpenToast(errMessage, 'error', 2000); } }, [errMessage]); // Per-row status meta — falls back to "unknown" if the rider's state key // isn't in the palette (e.g. brand-new status string from the backend). const getRowStatusMeta = (row) => { if (tabvalue == 0) { const key = (row.status || '').toLowerCase() === 'active' ? 'active' : 'inactive'; return STATUS_META[key]; } const state = ridersStatus?.find((s) => s.userid === row.userid); const key = (state?.status || 'unknown').toLowerCase(); return STATUS_META[key] || STATUS_META.unknown; }; return ( <> theme.zIndex.drawer + 1 }} open={allRidersLoading || riderSummarysLoading || riderStatusLoading} > {/* ============================================= || Header | ============================================= */} Riders Live · {locaName || 'All Zones'} } placeholder="Select Zone" paperComponent={SoftPaper} sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} /> {/* ============================================= || KPI Cards | ============================================= */} {KPI_META(allRidersSummary).map((item) => { const Icon = item.icon; return ( {item.label} {riderSummarysLoading ? ( ) : ( {item.value} )} ); })} {/* ============================================= || Status Tabs + Search || ============================================= */} {TAB_META.map((t) => { const Icon = t.icon; const active = tabvalue === t.key; const count = allRidersSummary?.[t.countKey] ?? 0; return ( handleChangetab(t.key)} sx={{ display: 'inline-flex', alignItems: 'center', gap: { xs: 0.625, md: 0.875 }, pl: 0.5, pr: { xs: 1, md: 1.25 }, py: 0.5, flexShrink: 0, cursor: 'pointer', borderRadius: 999, border: `1.5px solid ${active ? t.color : edge(t.color)}`, bgcolor: active ? t.color : tint(t.color), color: active ? '#fff' : t.color, fontWeight: 700, boxShadow: active ? `0 6px 18px ${ring(t.color)}` : 'none', transition: 'all 0.18s', '&:hover': { borderColor: t.color, boxShadow: active ? `0 6px 18px ${ring(t.color)}` : `0 0 0 3px ${ring(t.color)}` } }} > {t.label} {riderSummarysLoading ? : count} ); })} {/* ============================================= || Table || ============================================= */} # ID Rider Address Vehicle Shift Time Fare Fuel Status {roleid == 1 && Action} {allRidersLoading && } {rows?.length === 0 && !allRidersLoading && ( No riders to show {searchword ? 'Try a different keyword.' : `No ${tabvalue === 0 ? '' : 'active '}riders for this zone.`} )} {rows?.length !== 0 && rows?.map((row, index) => { const statusMeta = getRowStatusMeta(row); const StatusIcon = statusMeta.icon; const expanded = logsRow === row.userid; return ( {String(index + 1).padStart(2, '0')} #{row?.userid} {(row.fullname || row.username || '?').charAt(0).toUpperCase()} {row.username || '—'} {row.contactno || '—'} {row.suburb || (row.address ? row.address.slice(0, 20) + '…' : '—')} {row.city || ''} {row.vehicleno || '—'} #{row.shiftid ?? '—'} {row.starttime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.starttime}`).format('hh:mm A') : '—'} {row.endtime ? dayjs(`${dayjs().format('MM-DD-YYYY')} ${row.endtime}`).format('hh:mm A') : '—'} {row.basefare ?? '—'} {row.fuelcharge ?? '—'} {statusMeta.label} {roleid == 1 && ( { navigate('/nearle/riders/edit', { state: { riderdata: row } }); }} > {tabvalue != 0 && ( { if (row.userid == logsRow) { setLogsRow(null); } else { setLogsRow(row.userid); getRiderLogs(row.userid); } }} > {expanded ? : } )} )} {/* ============ Collapsible row — live rider logs ============ */} {expanded && tabvalue !== 0 && ( Live telemetry — {row.username || `Rider #${row.userid}`} )} ); })} {rows?.length !== 0 && (
{isFetchingNextPage || hasNextPage ? ( ) : ( No more riders )}
)}
); }; // Inline stat chip used in the rider-logs collapse row. Mirrors the StatChip // pattern from the pricing page so the telemetry block reads at a glance. const LogChip = ({ color, icon: Icon, label, value }) => ( {label} {value} ); export default Riders;