@burdenoff/microfe-movethewheels 2026.925.1 → 2026.925.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/StatusBadge.js +31 -31
- package/dist/constants/index.js +19 -5
- package/dist/hooks/useCustomers.js +0 -3
- package/dist/hooks/useFleet.js +197 -175
- package/dist/hooks/useInventory.js +144 -145
- package/dist/hooks/useRoutes.js +5 -14
- package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
- package/dist/movethewheels/constants/index.d.ts +2 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +53 -1
- package/dist/movethewheels/hooks/useInventory.d.ts +10 -0
- package/dist/movethewheels/types/index.d.ts +29 -108
- package/dist/node_modules/lucide-react/dist/esm/icons/sticky-note.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/user-x.js +30 -0
- package/dist/pages/CustomerDetailsPage.js +666 -344
- package/dist/pages/CustomersPage.js +71 -80
- package/dist/pages/DriverDetailsPage.js +702 -421
- package/dist/pages/DriversPage.js +106 -117
- package/dist/pages/FleetPage.js +5 -13
- package/dist/pages/InventoryPage.js +4 -4
- package/dist/pages/OrderDetailsPage.js +279 -274
- package/dist/pages/RouteDetailsPage.js +300 -263
- package/dist/pages/RoutesPage.js +130 -67
- package/dist/pages/VehicleDetailsPage.js +562 -287
- package/dist/pages/VehiclesPage.js +119 -70
- package/dist/pages/WarehouseDetailsPage.js +410 -370
- package/dist/pages/WarehousesPage.js +131 -82
- package/package.json +1 -1
- package/src/movethewheels/components/StatusBadge.tsx +16 -2
- package/src/movethewheels/constants/index.ts +21 -0
- package/src/movethewheels/hooks/useCustomers.ts +0 -3
- package/src/movethewheels/hooks/useFleet.ts +236 -62
- package/src/movethewheels/hooks/useInventory.ts +76 -32
- package/src/movethewheels/hooks/useRoutes.ts +17 -12
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +376 -109
- package/src/movethewheels/pages/CustomersPage.tsx +6 -15
- package/src/movethewheels/pages/DriverDetailsPage.tsx +516 -156
- package/src/movethewheels/pages/DriversPage.tsx +28 -11
- package/src/movethewheels/pages/FleetPage.tsx +9 -11
- package/src/movethewheels/pages/InventoryPage.tsx +14 -5
- package/src/movethewheels/pages/OrderDetailsPage.tsx +12 -7
- package/src/movethewheels/pages/RouteDetailsPage.tsx +89 -41
- package/src/movethewheels/pages/RoutesPage.tsx +66 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +282 -20
- package/src/movethewheels/pages/VehiclesPage.tsx +68 -3
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +213 -135
- package/src/movethewheels/pages/WarehousesPage.tsx +77 -16
- package/src/movethewheels/types/index.ts +30 -108
|
@@ -37,14 +37,17 @@ import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
|
37
37
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
38
38
|
import { formatDate } from '../utils/formatters';
|
|
39
39
|
import { joinPath } from '../utils/navigation';
|
|
40
|
+
import { DRIVER_STATUS_LABELS } from '../constants';
|
|
40
41
|
import type { Driver, DriverStatus } from '../types';
|
|
41
42
|
|
|
43
|
+
// Sourced from the same DRIVER_STATUS_LABELS map the roster table's
|
|
44
|
+
// StatusBadge renders from, so this list can never drift from the table
|
|
45
|
+
// again (BOFF-7909).
|
|
42
46
|
const STATUS_OPTIONS: Array<{ value: DriverStatus | 'all'; label: string }> = [
|
|
43
47
|
{ value: 'all', label: 'All statuses' },
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ value: 'offline', label: 'Off duty' },
|
|
48
|
+
...(Object.entries(DRIVER_STATUS_LABELS) as Array<[DriverStatus, string]>).map(
|
|
49
|
+
([value, label]) => ({ value, label })
|
|
50
|
+
),
|
|
48
51
|
];
|
|
49
52
|
|
|
50
53
|
export default function DriversPage() {
|
|
@@ -74,8 +77,17 @@ export default function DriversPage() {
|
|
|
74
77
|
// Exact ON_BREAK / OFF_DUTY / INACTIVE counts — fleetStats cannot split these.
|
|
75
78
|
const { counts } = useDriverStatusCounts();
|
|
76
79
|
// Drivers carry only `vehicleId`; the roster shows the fleet number, so the
|
|
77
|
-
//
|
|
78
|
-
|
|
80
|
+
// exact vehicles referenced on this page are looked up by id — this stays
|
|
81
|
+
// correct regardless of total fleet size (a flat top-N page would silently
|
|
82
|
+
// miss drivers assigned to a vehicle outside the first N, BOFF-7909).
|
|
83
|
+
const assignedVehicleIds = useMemo(
|
|
84
|
+
() =>
|
|
85
|
+
Array.from(
|
|
86
|
+
new Set(drivers.map((driver) => driver.assignedVehicle).filter(Boolean))
|
|
87
|
+
) as string[],
|
|
88
|
+
[drivers]
|
|
89
|
+
);
|
|
90
|
+
const { vehicles } = useVehicles({ ids: assignedVehicleIds });
|
|
79
91
|
const vehicleNumberById = useMemo(
|
|
80
92
|
() => new Map(vehicles.map((vehicle) => [vehicle.id, vehicle.vehicleNumber])),
|
|
81
93
|
[vehicles]
|
|
@@ -154,10 +166,10 @@ export default function DriversPage() {
|
|
|
154
166
|
id: 'rating',
|
|
155
167
|
header: 'Rating',
|
|
156
168
|
cell: (row) =>
|
|
157
|
-
row.
|
|
169
|
+
row.rating > 0 ? (
|
|
158
170
|
<div className="flex items-center gap-1">
|
|
159
171
|
<Star size={14} className="text-status-warning-text fill-current" />
|
|
160
|
-
<span className="font-medium">{row.
|
|
172
|
+
<span className="font-medium">{row.rating.toFixed(1)}</span>
|
|
161
173
|
</div>
|
|
162
174
|
) : (
|
|
163
175
|
<span className="text-sm text-text-secondary">Not rated</span>
|
|
@@ -181,10 +193,15 @@ export default function DriversPage() {
|
|
|
181
193
|
},
|
|
182
194
|
{
|
|
183
195
|
id: 'joined',
|
|
184
|
-
header: '
|
|
185
|
-
accessorKey: '
|
|
196
|
+
header: 'Hire Date',
|
|
197
|
+
accessorKey: 'hireDate',
|
|
186
198
|
sortable: true,
|
|
187
|
-
cell: (row) =>
|
|
199
|
+
cell: (row) =>
|
|
200
|
+
row.hireDate ? (
|
|
201
|
+
formatDate(row.hireDate, 'short')
|
|
202
|
+
) : (
|
|
203
|
+
<span className="text-text-secondary">Not recorded</span>
|
|
204
|
+
),
|
|
188
205
|
},
|
|
189
206
|
];
|
|
190
207
|
|
|
@@ -244,7 +244,8 @@ export default function FleetPage() {
|
|
|
244
244
|
<VehicleTypeBadge type={vehicle.type} size="sm" />
|
|
245
245
|
</div>
|
|
246
246
|
<p className="text-sm text-text-secondary">
|
|
247
|
-
{vehicle.make} {vehicle.model}
|
|
247
|
+
{vehicle.make} {vehicle.model}
|
|
248
|
+
{vehicle.year ? ` (${vehicle.year})` : ''}
|
|
248
249
|
</p>
|
|
249
250
|
</div>
|
|
250
251
|
</div>
|
|
@@ -302,20 +303,17 @@ export default function FleetPage() {
|
|
|
302
303
|
</Avatar>
|
|
303
304
|
<div>
|
|
304
305
|
<p className="font-medium">{driver.fullName}</p>
|
|
305
|
-
<p className="text-sm text-text-secondary">
|
|
306
|
-
{driver.employeeId} - {driver.completedOrders}{' '}
|
|
307
|
-
{t('movethewheels.fleet.drivers.deliveries', 'deliveries')}
|
|
308
|
-
</p>
|
|
306
|
+
<p className="text-sm text-text-secondary">{driver.employeeId}</p>
|
|
309
307
|
</div>
|
|
310
308
|
</div>
|
|
311
309
|
<div className="text-right">
|
|
312
310
|
<StatusBadge status={driver.status} type="driver" size="sm" />
|
|
313
|
-
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
{driver.
|
|
317
|
-
</
|
|
318
|
-
|
|
311
|
+
{driver.rating > 0 && (
|
|
312
|
+
<div className="mt-1 flex items-center gap-1 justify-end">
|
|
313
|
+
<Star size={12} className="text-status-warning-text fill-current" />
|
|
314
|
+
<span className="text-sm font-medium">{driver.rating.toFixed(1)}</span>
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
319
317
|
</div>
|
|
320
318
|
</div>
|
|
321
319
|
</div>
|
|
@@ -53,7 +53,7 @@ interface StockRow {
|
|
|
53
53
|
onHand: number;
|
|
54
54
|
allocated: number;
|
|
55
55
|
available: number;
|
|
56
|
-
reorderLevel
|
|
56
|
+
reorderLevel?: number;
|
|
57
57
|
stockState: 'out_of_stock' | 'low' | 'in_stock';
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -140,10 +140,15 @@ export default function InventoryPage() {
|
|
|
140
140
|
allocated: item.reservedQuantity,
|
|
141
141
|
available: item.availableQuantity,
|
|
142
142
|
reorderLevel: item.reorderLevel,
|
|
143
|
+
// Matches the backend's lowStock rule (inventory.service.ts): only
|
|
144
|
+
// flag "low" when a reorder point is actually configured — items with
|
|
145
|
+
// none set are never considered low stock, even at 0 available,
|
|
146
|
+
// otherwise this table disagreed with the dashboard's low-stock count
|
|
147
|
+
// for the same item (BOFF-7909 follow-up).
|
|
143
148
|
stockState:
|
|
144
149
|
item.quantity === 0
|
|
145
150
|
? 'out_of_stock'
|
|
146
|
-
: item.availableQuantity <= item.reorderLevel
|
|
151
|
+
: item.reorderLevel !== undefined && item.availableQuantity <= item.reorderLevel
|
|
147
152
|
? 'low'
|
|
148
153
|
: 'in_stock',
|
|
149
154
|
}));
|
|
@@ -196,7 +201,7 @@ export default function InventoryPage() {
|
|
|
196
201
|
header: t('movethewheels.inventory.stock.reorderPoint', 'Reorder point'),
|
|
197
202
|
accessorKey: 'reorderLevel',
|
|
198
203
|
sortable: true,
|
|
199
|
-
cell: (row) => (row.reorderLevel
|
|
204
|
+
cell: (row) => (row.reorderLevel !== undefined ? row.reorderLevel.toLocaleString() : '—'),
|
|
200
205
|
},
|
|
201
206
|
{
|
|
202
207
|
id: 'state',
|
|
@@ -501,8 +506,12 @@ export default function InventoryPage() {
|
|
|
501
506
|
</p>
|
|
502
507
|
</div>
|
|
503
508
|
<div className="text-right">
|
|
504
|
-
<Badge
|
|
505
|
-
{
|
|
509
|
+
<Badge
|
|
510
|
+
variant={
|
|
511
|
+
warehouse.status.toUpperCase() === 'ACTIVE' ? 'default' : 'secondary'
|
|
512
|
+
}
|
|
513
|
+
>
|
|
514
|
+
{warehouse.status.toUpperCase() === 'ACTIVE'
|
|
506
515
|
? t('movethewheels.inventory.warehouses.active', 'Active')
|
|
507
516
|
: t('movethewheels.inventory.warehouses.inactive', 'Inactive')}
|
|
508
517
|
</Badge>
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
ArrowRight,
|
|
24
24
|
XCircle,
|
|
25
25
|
UserCheck,
|
|
26
|
+
Search,
|
|
26
27
|
} from 'lucide-react';
|
|
27
28
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
28
29
|
import { StatusBadge, PriorityBadge, PaymentStatusBadge } from '../components/StatusBadge';
|
|
@@ -577,13 +578,17 @@ export default function OrderDetailsPage() {
|
|
|
577
578
|
|
|
578
579
|
<div className="space-y-3">
|
|
579
580
|
<div className="space-y-1.5">
|
|
580
|
-
<Label htmlFor="assign-fleet-search">Search
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
581
|
+
<Label htmlFor="assign-fleet-search">Search vehicles & drivers</Label>
|
|
582
|
+
<div className="relative">
|
|
583
|
+
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" />
|
|
584
|
+
<Input
|
|
585
|
+
id="assign-fleet-search"
|
|
586
|
+
value={fleetSearch}
|
|
587
|
+
onChange={(event) => setFleetSearch(event.target.value)}
|
|
588
|
+
placeholder="Vehicle number, driver name or employee ID"
|
|
589
|
+
className="pl-9"
|
|
590
|
+
/>
|
|
591
|
+
</div>
|
|
587
592
|
</div>
|
|
588
593
|
<div className="space-y-1.5">
|
|
589
594
|
<Label htmlFor="assign-vehicle">Vehicle</Label>
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
ArrowUp,
|
|
19
19
|
ArrowDown,
|
|
20
20
|
Fuel,
|
|
21
|
+
Search,
|
|
21
22
|
} from 'lucide-react';
|
|
22
23
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
23
24
|
import { StatusBadge } from '../components/StatusBadge';
|
|
@@ -36,7 +37,17 @@ import {
|
|
|
36
37
|
SelectTrigger,
|
|
37
38
|
SelectValue,
|
|
38
39
|
} from '../components/ui';
|
|
39
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
EmphasisPanel,
|
|
42
|
+
NextSteps,
|
|
43
|
+
PagePurpose,
|
|
44
|
+
ResponsiveDialog,
|
|
45
|
+
ResponsiveDialogContent,
|
|
46
|
+
ResponsiveDialogHeader,
|
|
47
|
+
ResponsiveDialogFooter,
|
|
48
|
+
ResponsiveDialogTitle,
|
|
49
|
+
ResponsiveDialogDescription,
|
|
50
|
+
} from '@burdenoff/fe-libs/ui';
|
|
40
51
|
import { RouteMap, type RouteStop as MapRouteStop } from '../components/maps';
|
|
41
52
|
import { firstRealLatLng } from '../utils/mapMarkers';
|
|
42
53
|
import { EntityCombobox } from '../components/EntityCombobox';
|
|
@@ -276,13 +287,20 @@ function NewRouteForm({ editId }: { editId?: string }) {
|
|
|
276
287
|
</div>
|
|
277
288
|
</div>
|
|
278
289
|
<div className="space-y-2">
|
|
279
|
-
<Label htmlFor="fleet-search">Search
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
290
|
+
<Label htmlFor="fleet-search">Search vehicles & drivers</Label>
|
|
291
|
+
<div className="relative">
|
|
292
|
+
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" />
|
|
293
|
+
<Input
|
|
294
|
+
id="fleet-search"
|
|
295
|
+
value={fleetSearch}
|
|
296
|
+
onChange={(e) => setFleetSearch(e.target.value)}
|
|
297
|
+
placeholder="Vehicle number, driver name or employee ID"
|
|
298
|
+
className="pl-9"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
<p className="text-xs text-text-secondary">
|
|
302
|
+
Narrows both pickers below as you type — up to 20 matches at a time.
|
|
303
|
+
</p>
|
|
286
304
|
</div>
|
|
287
305
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
288
306
|
<div className="space-y-2">
|
|
@@ -417,7 +435,7 @@ export default function RouteDetailsPage() {
|
|
|
417
435
|
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
418
436
|
const { basePath, navigate } = useMoveTheWheels();
|
|
419
437
|
const { route, isLoading, error, refetch } = useRoute(routeId);
|
|
420
|
-
const { optimizeRoute, updateRoute } = useRoutes();
|
|
438
|
+
const { optimizeRoute, updateRoute, deleteRoute } = useRoutes();
|
|
421
439
|
const suggestions = useAssignmentSuggestions(routeId);
|
|
422
440
|
const { t: translate } = useI18n();
|
|
423
441
|
const t = (key: string, fallback: string): string => {
|
|
@@ -427,10 +445,27 @@ export default function RouteDetailsPage() {
|
|
|
427
445
|
const { message: notice, notify } = useNotice();
|
|
428
446
|
const [optimizeError, setOptimizeError] = useState<string | null>(null);
|
|
429
447
|
const [optimizing, setOptimizing] = useState(false);
|
|
448
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
449
|
+
const [deleting, setDeleting] = useState(false);
|
|
450
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
430
451
|
|
|
431
452
|
const goBack = () => navigate?.(joinPath(basePath, 'routes'));
|
|
432
453
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
433
454
|
|
|
455
|
+
const handleDelete = async () => {
|
|
456
|
+
if (!routeId) return;
|
|
457
|
+
setDeleting(true);
|
|
458
|
+
setDeleteError(null);
|
|
459
|
+
try {
|
|
460
|
+
await deleteRoute(routeId);
|
|
461
|
+
setDeleteOpen(false);
|
|
462
|
+
goBack();
|
|
463
|
+
} catch (err) {
|
|
464
|
+
setDeleteError(err instanceof Error ? err.message : 'Failed to delete route.');
|
|
465
|
+
setDeleting(false);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
434
469
|
const handleOptimize = async () => {
|
|
435
470
|
if (!routeId) return;
|
|
436
471
|
setOptimizing(true);
|
|
@@ -709,18 +744,6 @@ export default function RouteDetailsPage() {
|
|
|
709
744
|
</CardTitle>
|
|
710
745
|
</CardHeader>
|
|
711
746
|
<CardContent className="space-y-3">
|
|
712
|
-
{route.startTime && (
|
|
713
|
-
<div>
|
|
714
|
-
<p className="text-sm text-text-secondary">Started</p>
|
|
715
|
-
<p>{formatDate(route.startTime, 'full')}</p>
|
|
716
|
-
</div>
|
|
717
|
-
)}
|
|
718
|
-
{route.endTime && (
|
|
719
|
-
<div>
|
|
720
|
-
<p className="text-sm text-text-secondary">Completed</p>
|
|
721
|
-
<p>{formatDate(route.endTime, 'full')}</p>
|
|
722
|
-
</div>
|
|
723
|
-
)}
|
|
724
747
|
<div>
|
|
725
748
|
<p className="text-sm text-text-secondary">Created</p>
|
|
726
749
|
<p>{formatDate(route.createdAt, 'full')}</p>
|
|
@@ -728,28 +751,53 @@ export default function RouteDetailsPage() {
|
|
|
728
751
|
</CardContent>
|
|
729
752
|
</Card>
|
|
730
753
|
|
|
731
|
-
|
|
732
|
-
<
|
|
733
|
-
<
|
|
734
|
-
<
|
|
735
|
-
</
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
)}
|
|
754
|
+
<Card className="border-status-error-border">
|
|
755
|
+
<CardHeader>
|
|
756
|
+
<CardTitle className="flex items-center gap-2 text-status-error-text">
|
|
757
|
+
<Trash2 size={18} /> Delete Route
|
|
758
|
+
</CardTitle>
|
|
759
|
+
</CardHeader>
|
|
760
|
+
<CardContent>
|
|
761
|
+
<p className="mb-3 text-sm text-text-secondary">
|
|
762
|
+
Removes this route. It can no longer be dispatched. This cannot be undone from here.
|
|
763
|
+
</p>
|
|
764
|
+
<Button
|
|
765
|
+
variant="outline"
|
|
766
|
+
className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
|
|
767
|
+
onClick={() => setDeleteOpen(true)}
|
|
768
|
+
>
|
|
769
|
+
<Trash2 size={16} /> Delete this route
|
|
770
|
+
</Button>
|
|
771
|
+
</CardContent>
|
|
772
|
+
</Card>
|
|
751
773
|
</div>
|
|
752
774
|
</div>
|
|
775
|
+
|
|
776
|
+
<ResponsiveDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
777
|
+
<ResponsiveDialogContent>
|
|
778
|
+
<ResponsiveDialogHeader>
|
|
779
|
+
<ResponsiveDialogTitle>Delete this route?</ResponsiveDialogTitle>
|
|
780
|
+
<ResponsiveDialogDescription>
|
|
781
|
+
This removes {route.name} and cannot be undone from here.
|
|
782
|
+
</ResponsiveDialogDescription>
|
|
783
|
+
</ResponsiveDialogHeader>
|
|
784
|
+
<div className="px-1">
|
|
785
|
+
<FormError message={deleteError} />
|
|
786
|
+
</div>
|
|
787
|
+
<ResponsiveDialogFooter>
|
|
788
|
+
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleting}>
|
|
789
|
+
Cancel
|
|
790
|
+
</Button>
|
|
791
|
+
<Button
|
|
792
|
+
className="bg-status-error-text text-text-inverse hover:opacity-90"
|
|
793
|
+
onClick={handleDelete}
|
|
794
|
+
disabled={deleting}
|
|
795
|
+
>
|
|
796
|
+
{deleting ? 'Deleting...' : 'Delete route'}
|
|
797
|
+
</Button>
|
|
798
|
+
</ResponsiveDialogFooter>
|
|
799
|
+
</ResponsiveDialogContent>
|
|
800
|
+
</ResponsiveDialog>
|
|
753
801
|
</PageLayout>
|
|
754
802
|
);
|
|
755
803
|
}
|
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import { Plus, RefreshCw, Navigation } from 'lucide-react';
|
|
6
|
+
import { Plus, RefreshCw, Navigation, Filter } from 'lucide-react';
|
|
7
7
|
import { PageLayout } from '../components/PageLayout';
|
|
8
8
|
import { DataTable, type Column } from '../components/DataTable';
|
|
9
9
|
import { StatusBadge } from '../components/StatusBadge';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Badge,
|
|
12
|
+
Button,
|
|
13
|
+
Card,
|
|
14
|
+
CardContent,
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '../components/ui';
|
|
11
21
|
import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
12
22
|
import { useRoutes, useRouteStats } from '../hooks/useRoutes';
|
|
13
23
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
@@ -17,17 +27,28 @@ import { joinPath } from '../utils/navigation';
|
|
|
17
27
|
import { ROUTE_STATUS_COLORS } from '../constants';
|
|
18
28
|
import type { Route } from '../types';
|
|
19
29
|
|
|
30
|
+
const STATUS_OPTIONS: Array<{ value: Route['status'] | 'all'; label: string }> = [
|
|
31
|
+
{ value: 'all', label: 'All statuses' },
|
|
32
|
+
{ value: 'planned', label: 'Planned' },
|
|
33
|
+
{ value: 'in_progress', label: 'In Progress' },
|
|
34
|
+
{ value: 'completed', label: 'Completed' },
|
|
35
|
+
{ value: 'cancelled', label: 'Cancelled' },
|
|
36
|
+
];
|
|
37
|
+
|
|
20
38
|
export default function RoutesPage() {
|
|
21
39
|
const { basePath, navigate } = useMoveTheWheels();
|
|
22
40
|
const [page, setPage] = useState(0);
|
|
23
41
|
const [pageSize, setPageSize] = useState(25);
|
|
24
42
|
const [search, setSearch] = useState('');
|
|
43
|
+
const [statusFilter, setStatusFilter] = useState<Route['status'] | 'all'>('all');
|
|
25
44
|
const debouncedSearch = useDebouncedValue(search);
|
|
26
45
|
const { routes, totalRoutes, hasMore, isLoading, error, refetch } = useRoutes({
|
|
27
46
|
limit: pageSize,
|
|
28
47
|
offset: page * pageSize,
|
|
29
48
|
search: debouncedSearch || undefined,
|
|
49
|
+
status: statusFilter === 'all' ? undefined : statusFilter,
|
|
30
50
|
});
|
|
51
|
+
const filtersActive = statusFilter !== 'all' || search.length > 0;
|
|
31
52
|
// Bounded aggregate for the tiles + subtitle — not the truncated page.
|
|
32
53
|
const { stats } = useRouteStats();
|
|
33
54
|
|
|
@@ -126,6 +147,49 @@ export default function RoutesPage() {
|
|
|
126
147
|
</div>
|
|
127
148
|
</EmphasisPanel>
|
|
128
149
|
|
|
150
|
+
{/* Filters */}
|
|
151
|
+
<Card>
|
|
152
|
+
<CardContent className="p-4">
|
|
153
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
154
|
+
<div className="flex items-center gap-2">
|
|
155
|
+
<Filter size={16} className="text-text-secondary" />
|
|
156
|
+
<span className="text-sm font-medium">Filters:</span>
|
|
157
|
+
</div>
|
|
158
|
+
<Select
|
|
159
|
+
value={statusFilter}
|
|
160
|
+
onValueChange={(value) => {
|
|
161
|
+
setStatusFilter(value as Route['status'] | 'all');
|
|
162
|
+
setPage(0);
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<SelectTrigger className="w-[180px]">
|
|
166
|
+
<SelectValue placeholder="Status" />
|
|
167
|
+
</SelectTrigger>
|
|
168
|
+
<SelectContent>
|
|
169
|
+
{STATUS_OPTIONS.map((option) => (
|
|
170
|
+
<SelectItem key={option.value} value={option.value}>
|
|
171
|
+
{option.label}
|
|
172
|
+
</SelectItem>
|
|
173
|
+
))}
|
|
174
|
+
</SelectContent>
|
|
175
|
+
</Select>
|
|
176
|
+
{filtersActive && (
|
|
177
|
+
<Button
|
|
178
|
+
variant="ghost"
|
|
179
|
+
size="sm"
|
|
180
|
+
onClick={() => {
|
|
181
|
+
setStatusFilter('all');
|
|
182
|
+
setSearch('');
|
|
183
|
+
setPage(0);
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
Clear filters
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
</CardContent>
|
|
191
|
+
</Card>
|
|
192
|
+
|
|
129
193
|
<DataTable
|
|
130
194
|
columns={columns}
|
|
131
195
|
data={routes}
|