updates on the ui changes and frully removed the mui and converted into astryx and color change
This commit is contained in:
@@ -1,60 +1,33 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { MdDirectionsCar, MdEdit, MdDeleteOutline, MdAdd, MdBatteryStd } 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 { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||
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 DT = {
|
||||
radiusCard: 14,
|
||||
radiusField: 10,
|
||||
textPrimary: '#0f172a',
|
||||
textSecondary: '#64748b',
|
||||
textMuted: '#94a3b8',
|
||||
borderSubtle: '#e2e8f0',
|
||||
borderHover: '#cbd5e1',
|
||||
divider: '#f1f5f9',
|
||||
surface: '#ffffff',
|
||||
surfaceAlt: '#f8fafc'
|
||||
};
|
||||
const a = (c, suffix) => `${c}${suffix}`;
|
||||
const soft = (c) => a(c, '18');
|
||||
const ring = (c) => a(c, '26');
|
||||
const BRAND = '#C01227';
|
||||
const VEHICLE_TYPES = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'];
|
||||
const BRAND = DT.brand;
|
||||
const VEHICLE_TYPE_OPTIONS = ['Bike', 'Scooter', 'Bicycle', 'Car', 'Van'].map((v) => ({ value: v, label: v }));
|
||||
|
||||
const emptyForm = {
|
||||
vehicleid: null,
|
||||
@@ -73,8 +46,6 @@ const emptyForm = {
|
||||
};
|
||||
|
||||
const Vehicles = () => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const queryClient = useQueryClient();
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('');
|
||||
@@ -154,6 +125,8 @@ const Vehicles = () => {
|
||||
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');
|
||||
@@ -180,195 +153,152 @@ const Vehicles = () => {
|
||||
title="Vehicles"
|
||||
subtitle="Live · Fleet Directory"
|
||||
live
|
||||
action={
|
||||
<Button variant="contained" startIcon={<MdAdd size={16} />} onClick={openCreate} sx={{ bgcolor: BRAND, whiteSpace: 'nowrap' }}>
|
||||
New Vehicle
|
||||
</Button>
|
||||
}
|
||||
action={<Button label="New Vehicle" variant="primary" icon={<MdAdd size={16} />} onClick={openCreate} />}
|
||||
/>
|
||||
|
||||
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||
<Grid item xs={4}>
|
||||
<StatCard title="Total Vehicles" value={stats.total} icon={<MdDirectionsCar size={20} />} color={BRAND} />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<StatCard title="Available" value={stats.active} icon={<MdDirectionsCar size={20} />} color="#10b981" />
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<StatCard title="Low Battery" value={stats.lowBattery} icon={<MdBatteryStd size={20} />} color="#f59e0b" />
|
||||
</Grid>
|
||||
<Grid columns={{ minWidth: 220 }} gap={3}>
|
||||
<StatCard title="Total Vehicles" value={stats.total} icon={<MdDirectionsCar size={20} />} color={BRAND} />
|
||||
<StatCard title="Available" value={stats.active} icon={<MdDirectionsCar size={20} />} color={STATUS.delivered} />
|
||||
<StatCard title="Low Battery" value={stats.lowBattery} icon={<MdBatteryStd size={20} />} color={STATUS.pending} />
|
||||
</Grid>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{ mt: { xs: 1.5, md: 2 }, p: { xs: 1, md: 1.5 }, borderTopLeftRadius: DT.radiusCard / 8, borderTopRightRadius: DT.radiusCard / 8, border: '1px solid', borderColor: DT.borderSubtle, borderBottom: 0, background: '#fff' }}
|
||||
>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} alignItems={{ xs: 'stretch', sm: 'center' }} justifyContent="space-between" spacing={1.25}>
|
||||
<Typography variant="body2" sx={{ color: DT.textPrimary, fontWeight: 700 }}>
|
||||
{vehicles.length} total · {rows.length} shown
|
||||
</Typography>
|
||||
<Box sx={{ width: { xs: '100%', sm: 280 } }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search vehicles (ctrl+k)"
|
||||
sx={{
|
||||
m: 0,
|
||||
width: '100%',
|
||||
borderRadius: DT.radiusField + 'px',
|
||||
bgcolor: DT.surface,
|
||||
'& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 },
|
||||
'&:hover fieldset': { borderColor: DT.borderHover },
|
||||
'&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 1.5 },
|
||||
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<VStack gap={0} padding={0} style={{ marginTop: 16 }}>
|
||||
<Card padding={3} elevation="none" style={{ borderBottomLeftRadius: 0, borderBottomRightRadius: 0, borderBottom: 'none' }}>
|
||||
<HStack justify="between" vAlign="center" gap={3} wrap="wrap">
|
||||
<Text weight="bold">
|
||||
{vehicles.length} total · {rows.length} shown
|
||||
</Text>
|
||||
<div style={{ width: 280, maxWidth: '100%' }}>
|
||||
<DebounceSearchBar
|
||||
value={searchword}
|
||||
onChange={setSearchword}
|
||||
onDebouncedChange={setDebouncedSearch}
|
||||
placeholder="Search vehicles (ctrl+k)"
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</Card>
|
||||
|
||||
<Paper elevation={0} sx={{ borderBottomLeftRadius: DT.radiusCard / 8, borderBottomRightRadius: DT.radiusCard / 8, border: '1px solid', borderColor: DT.borderSubtle, overflow: 'hidden', background: '#fff' }}>
|
||||
<TableContainer sx={{ maxHeight: { xs: 'calc(100vh - 260px)', md: 'calc(100vh - 190px)' } }}>
|
||||
<Table stickyHeader sx={{ minWidth: isMobile ? 900 : 960 }}>
|
||||
<TableHead>
|
||||
<TableRow sx={{ '& th': { backgroundColor: DT.surfaceAlt, color: DT.textSecondary, fontSize: 11, fontWeight: 800, letterSpacing: 0.6, textTransform: 'uppercase', whiteSpace: 'nowrap', borderBottom: `1px solid ${DT.borderSubtle}` } }}>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>Vehicle No</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Partner</TableCell>
|
||||
<TableCell align="center">Max Weight</TableCell>
|
||||
<TableCell align="center">Battery</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading && <OrdersTableSkeleton col={6} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<Card padding={0} elevation="none" style={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, overflow: 'hidden' }}>
|
||||
<TableScroll minWidth={960}>
|
||||
<Table density="balanced" dividers="rows" hasHover>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} sx={{ py: 6 }}>
|
||||
<Stack alignItems="center" spacing={1.5}>
|
||||
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||
<MdDirectionsCar size={28} />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
No vehicles to show
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableHeaderCell>#</TableHeaderCell>
|
||||
<TableHeaderCell>Vehicle No</TableHeaderCell>
|
||||
<TableHeaderCell>Type</TableHeaderCell>
|
||||
<TableHeaderCell>Partner</TableHeaderCell>
|
||||
<TableHeaderCell>Max Weight</TableHeaderCell>
|
||||
<TableHeaderCell>Battery</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((row, index) => (
|
||||
<TableRow key={row.vehicleid || index} sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` }, '&:hover': { backgroundColor: DT.surfaceAlt } }}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||
{row.vehicleno}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{row.vehicletype || '—'}</TableCell>
|
||||
<TableCell>{partnerMap.get(row.partnerid)?.partnername || (row.partnerid ? `Partner #${row.partnerid}` : '—')}</TableCell>
|
||||
<TableCell align="center">{row.maxweight ?? '—'}</TableCell>
|
||||
<TableCell align="center">{row.batterypercentage != null ? `${row.batterypercentage}%` : '—'}</TableCell>
|
||||
<TableCell>{row.status || '—'}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
|
||||
<Tooltip title="Edit vehicle">
|
||||
<IconButton size="small" onClick={() => openEdit(row)} sx={{ color: BRAND }}>
|
||||
<MdEdit size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete vehicle">
|
||||
<IconButton size="small" onClick={() => handleDelete(row)} sx={{ color: '#ef4444' }}>
|
||||
<MdDeleteOutline size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading && <TableLoader rows={5} columns={8} />}
|
||||
{rows.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8}>
|
||||
<EmptyState title="No vehicles to show" icon={<MdDirectionsCar size={28} />} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
) : (
|
||||
rows.map((row, index) => (
|
||||
<TableRow key={row.vehicleid || index}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Text weight="bold">{row.vehicleno}</Text>
|
||||
</TableCell>
|
||||
<TableCell>{row.vehicletype || '—'}</TableCell>
|
||||
<TableCell>
|
||||
{partnerMap.get(row.partnerid)?.partnername || (row.partnerid ? `Partner #${row.partnerid}` : '—')}
|
||||
</TableCell>
|
||||
<TableCell>{row.maxweight ?? '—'}</TableCell>
|
||||
<TableCell>{row.batterypercentage != null ? `${row.batterypercentage}%` : '—'}</TableCell>
|
||||
<TableCell>{row.status || '—'}</TableCell>
|
||||
<TableCell>
|
||||
<HStack gap={1} justify="end">
|
||||
<IconButton
|
||||
label="Edit vehicle"
|
||||
tooltip="Edit vehicle"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<MdEdit size={16} />}
|
||||
onClick={() => openEdit(row)}
|
||||
/>
|
||||
<IconButton
|
||||
label="Delete vehicle"
|
||||
tooltip="Delete vehicle"
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
icon={<MdDeleteOutline size={16} />}
|
||||
onClick={() => handleDelete(row)}
|
||||
/>
|
||||
</HStack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableScroll>
|
||||
</Card>
|
||||
</VStack>
|
||||
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 700 }}>{form.vehicleid ? 'Edit Vehicle' : 'New Vehicle'}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={2} sx={{ mt: 0.5 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Vehicle No</InputLabel>
|
||||
<TextField value={form.vehicleno} onChange={(e) => setForm((f) => ({ ...f, vehicleno: e.target.value }))} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Vehicle Type</InputLabel>
|
||||
<Select fullWidth value={form.vehicletype} onChange={(e) => setForm((f) => ({ ...f, vehicletype: e.target.value }))}>
|
||||
{VEHICLE_TYPES.map((v) => (
|
||||
<MenuItem key={v} value={v}>
|
||||
{v}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Partner</InputLabel>
|
||||
<Autocomplete
|
||||
options={partners || []}
|
||||
getOptionLabel={(option) => option.partnername || ''}
|
||||
value={form.partner}
|
||||
onChange={(e, value) => setForm((f) => ({ ...f, partner: value }))}
|
||||
renderInput={(params) => <TextField {...params} placeholder="Choose partner" />}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Status</InputLabel>
|
||||
{/* 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. */}
|
||||
<TextField fullWidth value={form.status} disabled />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Max Weight (kg)</InputLabel>
|
||||
<TextField type="number" value={form.maxweight} onChange={(e) => setForm((f) => ({ ...f, maxweight: e.target.value }))} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Max Volume</InputLabel>
|
||||
<TextField type="number" value={form.maxvolume} onChange={(e) => setForm((f) => ({ ...f, maxvolume: e.target.value }))} />
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Stack spacing={1}>
|
||||
<InputLabel>Battery %</InputLabel>
|
||||
<TextField
|
||||
type="number"
|
||||
value={form.batterypercentage}
|
||||
onChange={(e) => setForm((f) => ({ ...f, batterypercentage: e.target.value }))}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Dialog isOpen={dialogOpen} onOpenChange={(next) => !next && setDialogOpen(false)} width={640} purpose="form">
|
||||
<DialogHeader title={form.vehicleid ? 'Edit Vehicle' : 'New Vehicle'} />
|
||||
<VStack gap={4} padding={4}>
|
||||
<Grid columns={{ minWidth: 240 }} gap={3}>
|
||||
<TextInput label="Vehicle No" value={form.vehicleno} onChange={set('vehicleno')} isRequired />
|
||||
<Selector label="Vehicle Type" options={VEHICLE_TYPE_OPTIONS} value={form.vehicletype} onChange={set('vehicletype')} />
|
||||
<Selector
|
||||
label="Partner"
|
||||
placeholder="Choose partner"
|
||||
hasSearch
|
||||
hasClear
|
||||
options={(partners || []).map((p) => ({ 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. */}
|
||||
<TextInput
|
||||
label="Status"
|
||||
value={form.status}
|
||||
onChange={set('status')}
|
||||
isDisabled
|
||||
disabledMessage="Only 'Available' is accepted by the API"
|
||||
/>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
|
||||
<Button variant="contained" onClick={handleSave} sx={{ bgcolor: BRAND }}>
|
||||
{form.vehicleid ? 'Save Changes' : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
<Grid columns={{ minWidth: 180 }} gap={3}>
|
||||
<NumberInput
|
||||
label="Max Weight (kg)"
|
||||
value={form.maxweight === '' ? undefined : Number(form.maxweight)}
|
||||
onChange={(v) => set('maxweight')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
<NumberInput
|
||||
label="Max Volume"
|
||||
value={form.maxvolume === '' ? undefined : Number(form.maxvolume)}
|
||||
onChange={(v) => set('maxvolume')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
<NumberInput
|
||||
label="Battery %"
|
||||
value={form.batterypercentage === '' ? undefined : Number(form.batterypercentage)}
|
||||
onChange={(v) => set('batterypercentage')(v ?? '')}
|
||||
isOptional
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<HStack gap={2} justify="end">
|
||||
<Button label="Cancel" variant="secondary" onClick={() => setDialogOpen(false)} />
|
||||
<Button label={form.vehicleid ? 'Save Changes' : 'Create'} variant="primary" onClick={handleSave} />
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user