import React, { useMemo, useState } from 'react'; import { MdDirectionsCar, MdEdit, MdDeleteOutline, MdAdd, MdBatteryStd, MdBatteryFull } from 'react-icons/md'; import { Grid } from '@astryxdesign/core/Grid'; import { HStack } from '@astryxdesign/core/HStack'; import { VStack } from '@astryxdesign/core/VStack'; import { Card } from '@astryxdesign/core/Card'; import { Text } from '@astryxdesign/core/Text'; import { Button } from '@astryxdesign/core/Button'; import { IconButton } from '@astryxdesign/core/IconButton'; import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; import { TextInput } from '@astryxdesign/core/TextInput'; import { NumberInput } from '@astryxdesign/core/NumberInput'; import { Selector } from '@astryxdesign/core/Selector'; import { EmptyState } from '@astryxdesign/core/EmptyState'; import { Table, TableHeader, TableBody, TableRow, TableCell, TableHeaderCell } from '@astryxdesign/core/Table'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import Loader from 'components/Loader'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; import TableLoader from 'components/nearle_components/TableLoader'; import { getVehicles, createVehicle, updateVehicle, deleteVehicle, getPartners } from 'pages/api/doormileApi'; import { enqueueSnackbar } from 'notistack'; import { DT, STATUS } from 'themes/dt/tokens'; import { TableScroll } from 'themes/dt/primitives'; const BRAND = DT.brand; const VEHICLE_TYPE_OPTIONS = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'].map((v) => ({ value: v, label: v })); const emptyForm = { vehicleid: null, vehicleno: '', vehicletype: 'Bike', maxweight: '', maxvolume: '', partner: null, batterypercentage: '', // Confirmed live: POST /admin/vehicles rejects "active"/"Active"/ // "inactive"/"Inactive"/"Unavailable"/"Maintenance"/"Offline" — the only // value that worked was "Available". Status is a required field with a // narrow, still not fully mapped enum, so this only offers the one // confirmed-working value rather than guessing a second one that fails. status: 'Available' }; const Vehicles = () => { const queryClient = useQueryClient(); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(emptyForm); const { data: vehicles = [], isLoading } = useQuery({ queryKey: ['admin-vehicles'], queryFn: getVehicles }); const { data: partners = [] } = useQuery({ queryKey: ['admin-partners'], queryFn: getPartners }); const partnerMap = useMemo(() => new Map((partners || []).map((p) => [p.partnerid, p])), [partners]); const rows = useMemo(() => { if (!debouncedSearch) return vehicles; const q = debouncedSearch.toLowerCase().trim(); return vehicles.filter((row) => [row.vehicleno, row.vehicletype, row.status].filter(Boolean).some((f) => String(f).toLowerCase().includes(q))); }, [vehicles, debouncedSearch]); const stats = useMemo(() => { const total = vehicles.length; // Only 'Available' is a confirmed-valid status on this resource (see the // comment on emptyForm.status above) — no vehicle can ever have a status // of 'active', so counting against that string always read 0. const active = vehicles.filter((v) => String(v.status).toLowerCase() === 'available').length; const lowBattery = vehicles.filter((v) => Number(v.batterypercentage) > 0 && Number(v.batterypercentage) < 20).length; const fullBattery = vehicles.filter((v) => Number(v.batterypercentage) >= 80).length; return { total, active, lowBattery, fullBattery }; }, [vehicles]); const opentoast = (message, variant = 'error') => enqueueSnackbar(message, { variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); const saveMutation = useMutation({ mutationFn: (payload) => (payload.vehicleid ? updateVehicle(payload.vehicleid, payload.data) : createVehicle(payload.data)), onSuccess: (res) => { if (res.success) { opentoast(form.vehicleid ? 'Vehicle updated' : 'Vehicle created', 'success'); queryClient.invalidateQueries({ queryKey: ['admin-vehicles'] }); setDialogOpen(false); } else { opentoast(res.message || 'Failed to save vehicle'); } }, onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to save vehicle') }); const deleteMutation = useMutation({ mutationFn: (id) => deleteVehicle(id), onSuccess: (res) => { if (res.success) { opentoast('Vehicle deleted', 'success'); queryClient.invalidateQueries({ queryKey: ['admin-vehicles'] }); } else { opentoast(res.message || 'Failed to delete vehicle'); } }, onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to delete vehicle') }); const openCreate = () => { setForm(emptyForm); setDialogOpen(true); }; const openEdit = (row) => { setForm({ vehicleid: row.vehicleid, vehicleno: row.vehicleno || '', vehicletype: row.vehicletype || 'Bike', maxweight: row.maxweight ?? '', maxvolume: row.maxvolume ?? '', partner: partnerMap.get(row.partnerid) || null, batterypercentage: row.batterypercentage ?? '', status: row.status || 'Available' }); setDialogOpen(true); }; const handleDelete = (row) => { if (window.confirm(`Delete vehicle "${row.vehicleno}"?`)) deleteMutation.mutate(row.vehicleid); }; const set = (key) => (value) => setForm((fm) => ({ ...fm, [key]: value })); const handleSave = () => { if (!form.vehicleno) { opentoast('Fill Vehicle No'); return; } const { vehicleid, partner, ...rest } = form; saveMutation.mutate({ vehicleid, data: { ...rest, maxweight: Number(rest.maxweight) || 0, maxvolume: Number(rest.maxvolume) || 0, batterypercentage: Number(rest.batterypercentage) || 0, partnerid: partner?.partnerid } }); }; return ( <> {(isLoading || saveMutation.isPending || deleteMutation.isPending) && } } onClick={openCreate} />} /> } color={BRAND} /> } color={STATUS.delivered} /> } color={STATUS.pending} /> } color={STATUS.active} /> {vehicles.length} total · {rows.length} shown
# Vehicle No Type Partner Max Weight Battery Status Actions {isLoading && } {rows.length === 0 && !isLoading ? ( } /> ) : ( rows.map((row, index) => ( {index + 1} {row.vehicleno} {row.vehicletype || '—'} {partnerMap.get(row.partnerid)?.partnername || (row.partnerid ? `Partner #${row.partnerid}` : '—')} {row.maxweight ?? '—'} {row.batterypercentage != null ? `${row.batterypercentage}%` : '—'} {row.status || '—'} } onClick={() => openEdit(row)} /> } onClick={() => handleDelete(row)} /> )) )}
!next && setDialogOpen(false)} width={640} purpose="form"> ({ value: String(p.partnerid), label: p.partnername || `Partner #${p.partnerid}` }))} value={form.partner ? String(form.partner.partnerid) : undefined} onChange={(v) => set('partner')(v ? partnerMap.get(Number(v)) || null : null)} /> {/* Only "Available" is confirmed to work against POST /admin/vehicles (Active/Inactive/Unavailable/Maintenance/Offline all rejected live) — not offering a second option that's known to fail. On edit this preserves whatever the vehicle's real status already is. */} set('maxweight')(v ?? '')} isOptional /> set('maxvolume')(v ?? '')} isOptional /> set('batterypercentage')(v ?? '')} isOptional /> ); }; export default Vehicles;