@burdenoff/microfe-movethewheels 2026.925.1 → 2026.925.2

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.
@@ -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
- { value: 'available', label: 'Available' },
45
- { value: 'busy', label: 'On delivery' },
46
- { value: 'on_break', label: 'On break' },
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
- // vehicle list is joined client-side into an id -> number lookup.
78
- const { vehicles } = useVehicles({ limit: 100 });
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.ratings.overall > 0 ? (
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.ratings.overall.toFixed(1)}</span>
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: 'Join Date',
185
- accessorKey: 'joinDate',
196
+ header: 'Hire Date',
197
+ accessorKey: 'hireDate',
186
198
  sortable: true,
187
- cell: (row) => formatDate(row.joinDate, 'short'),
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
 
@@ -302,20 +302,17 @@ export default function FleetPage() {
302
302
  </Avatar>
303
303
  <div>
304
304
  <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>
305
+ <p className="text-sm text-text-secondary">{driver.employeeId}</p>
309
306
  </div>
310
307
  </div>
311
308
  <div className="text-right">
312
309
  <StatusBadge status={driver.status} type="driver" size="sm" />
313
- <div className="mt-1 flex items-center gap-1 justify-end">
314
- <Star size={12} className="text-status-warning-text fill-current" />
315
- <span className="text-sm font-medium">
316
- {driver.ratings.overall.toFixed(1)}
317
- </span>
318
- </div>
310
+ {driver.rating > 0 && (
311
+ <div className="mt-1 flex items-center gap-1 justify-end">
312
+ <Star size={12} className="text-status-warning-text fill-current" />
313
+ <span className="text-sm font-medium">{driver.rating.toFixed(1)}</span>
314
+ </div>
315
+ )}
319
316
  </div>
320
317
  </div>
321
318
  </div>
@@ -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 fleet</Label>
581
- <Input
582
- id="assign-fleet-search"
583
- value={fleetSearch}
584
- onChange={(event) => setFleetSearch(event.target.value)}
585
- placeholder="Vehicle number, driver name or ID"
586
- />
581
+ <Label htmlFor="assign-fleet-search">Search vehicles &amp; 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';
@@ -276,13 +277,20 @@ function NewRouteForm({ editId }: { editId?: string }) {
276
277
  </div>
277
278
  </div>
278
279
  <div className="space-y-2">
279
- <Label htmlFor="fleet-search">Search fleet</Label>
280
- <Input
281
- id="fleet-search"
282
- value={fleetSearch}
283
- onChange={(e) => setFleetSearch(e.target.value)}
284
- placeholder="Vehicle number, driver name or ID"
285
- />
280
+ <Label htmlFor="fleet-search">Search vehicles &amp; drivers</Label>
281
+ <div className="relative">
282
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" />
283
+ <Input
284
+ id="fleet-search"
285
+ value={fleetSearch}
286
+ onChange={(e) => setFleetSearch(e.target.value)}
287
+ placeholder="Vehicle number, driver name or employee ID"
288
+ className="pl-9"
289
+ />
290
+ </div>
291
+ <p className="text-xs text-text-secondary">
292
+ Narrows both pickers below as you type — up to 20 matches at a time.
293
+ </p>
286
294
  </div>
287
295
  <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
288
296
  <div className="space-y-2">
@@ -25,7 +25,8 @@ import {
25
25
  AvatarFallback,
26
26
  } from '../components/ui';
27
27
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
28
- import { useVehicle, useVehicleDrivers, useFleet } from '../hooks/useFleet';
28
+ import { useVehicle, useVehicleDrivers, useFleet, toApiVehicleStatus } from '../hooks/useFleet';
29
+ import type { VehicleStatus } from '../types';
29
30
  import { useRecordVehicleService } from '../hooks/useFleetIntelligence';
30
31
  import { VehicleServiceCard } from '../components/VehicleServiceCard';
31
32
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
@@ -47,6 +48,19 @@ const VEHICLE_TYPE_OPTIONS = [
47
48
  { value: 'DRONE', label: 'Drone' },
48
49
  ];
49
50
 
51
+ const VEHICLE_STATUS_OPTIONS: Array<{ value: VehicleStatus; label: string }> = [
52
+ { value: 'available', label: 'Available' },
53
+ { value: 'assigned', label: 'In Transit' },
54
+ { value: 'maintenance', label: 'Maintenance' },
55
+ { value: 'out_of_service', label: 'Out of Service' },
56
+ ];
57
+
58
+ const OWNERSHIP_TYPE_OPTIONS = [
59
+ { value: 'owned', label: 'Owned' },
60
+ { value: 'leased', label: 'Leased' },
61
+ { value: 'rented', label: 'Rented' },
62
+ ];
63
+
50
64
  function NewVehicleForm({ editId }: { editId?: string }) {
51
65
  const { basePath, navigate } = useMoveTheWheels();
52
66
  const { createVehicle, updateVehicle } = useFleet();
@@ -58,6 +72,11 @@ function NewVehicleForm({ editId }: { editId?: string }) {
58
72
  make: '',
59
73
  model: '',
60
74
  licensePlate: '',
75
+ ownershipType: '',
76
+ year: '',
77
+ status: 'available' as VehicleStatus,
78
+ fuelType: '',
79
+ fuelEfficiencyLPer100Km: '',
61
80
  });
62
81
  const [submitting, setSubmitting] = useState(false);
63
82
  const [error, setError] = useState<string | null>(null);
@@ -70,6 +89,14 @@ function NewVehicleForm({ editId }: { editId?: string }) {
70
89
  make: vehicle.make ?? '',
71
90
  model: vehicle.model ?? '',
72
91
  licensePlate: vehicle.licensePlate ?? '',
92
+ ownershipType: vehicle.ownershipType ?? '',
93
+ year: vehicle.year ? String(vehicle.year) : '',
94
+ status: vehicle.status,
95
+ fuelType: vehicle.fuelType ?? '',
96
+ fuelEfficiencyLPer100Km:
97
+ vehicle.fuelEfficiencyLPer100Km !== undefined
98
+ ? String(vehicle.fuelEfficiencyLPer100Km)
99
+ : '',
73
100
  });
74
101
  }
75
102
  }, [editId, vehicle]);
@@ -85,23 +112,25 @@ function NewVehicleForm({ editId }: { editId?: string }) {
85
112
  setSubmitting(true);
86
113
  setError(null);
87
114
  try {
115
+ const payload = {
116
+ vehicleNumber: form.vehicleNumber.trim(),
117
+ type: form.type,
118
+ make: form.make.trim() || undefined,
119
+ model: form.model.trim() || undefined,
120
+ licensePlate: form.licensePlate.trim() || undefined,
121
+ ownershipType: form.ownershipType || undefined,
122
+ year: form.year ? Number(form.year) : undefined,
123
+ status: toApiVehicleStatus(form.status),
124
+ fuelType: form.fuelType || undefined,
125
+ fuelEfficiencyLPer100Km: form.fuelEfficiencyLPer100Km
126
+ ? Number(form.fuelEfficiencyLPer100Km)
127
+ : undefined,
128
+ };
88
129
  if (editId) {
89
- const updated = await updateVehicle(editId, {
90
- vehicleNumber: form.vehicleNumber.trim(),
91
- type: form.type,
92
- make: form.make.trim() || undefined,
93
- model: form.model.trim() || undefined,
94
- licensePlate: form.licensePlate.trim() || undefined,
95
- });
130
+ const updated = await updateVehicle(editId, payload);
96
131
  navigate?.(joinPath(basePath, `fleet/vehicles/${updated.id}`));
97
132
  } else {
98
- const created = await createVehicle({
99
- vehicleNumber: form.vehicleNumber.trim(),
100
- type: form.type,
101
- make: form.make.trim() || undefined,
102
- model: form.model.trim() || undefined,
103
- licensePlate: form.licensePlate.trim() || undefined,
104
- });
133
+ const created = await createVehicle(payload);
105
134
  navigate?.(joinPath(basePath, `fleet/vehicles/${created.id}`));
106
135
  }
107
136
  } catch (err) {
@@ -207,6 +236,83 @@ function NewVehicleForm({ editId }: { editId?: string }) {
207
236
  />
208
237
  </div>
209
238
  </div>
239
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
240
+ <div className="space-y-2">
241
+ <Label htmlFor="ownershipType">Ownership</Label>
242
+ <Select
243
+ value={form.ownershipType || undefined}
244
+ onValueChange={(value) => setForm((prev) => ({ ...prev, ownershipType: value }))}
245
+ >
246
+ <SelectTrigger id="ownershipType">
247
+ <SelectValue placeholder="Select ownership" />
248
+ </SelectTrigger>
249
+ <SelectContent>
250
+ {OWNERSHIP_TYPE_OPTIONS.map((option) => (
251
+ <SelectItem key={option.value} value={option.value}>
252
+ {option.label}
253
+ </SelectItem>
254
+ ))}
255
+ </SelectContent>
256
+ </Select>
257
+ </div>
258
+ <div className="space-y-2">
259
+ <Label htmlFor="year">Year</Label>
260
+ <Input
261
+ id="year"
262
+ type="number"
263
+ min={1980}
264
+ max={new Date().getFullYear() + 1}
265
+ value={form.year}
266
+ onChange={(e) => setForm((prev) => ({ ...prev, year: e.target.value }))}
267
+ placeholder={String(new Date().getFullYear())}
268
+ />
269
+ </div>
270
+ <div className="space-y-2">
271
+ <Label htmlFor="vehicleStatus">Status</Label>
272
+ <Select
273
+ value={form.status}
274
+ onValueChange={(value) =>
275
+ setForm((prev) => ({ ...prev, status: value as VehicleStatus }))
276
+ }
277
+ >
278
+ <SelectTrigger id="vehicleStatus">
279
+ <SelectValue />
280
+ </SelectTrigger>
281
+ <SelectContent>
282
+ {VEHICLE_STATUS_OPTIONS.map((option) => (
283
+ <SelectItem key={option.value} value={option.value}>
284
+ {option.label}
285
+ </SelectItem>
286
+ ))}
287
+ </SelectContent>
288
+ </Select>
289
+ </div>
290
+ </div>
291
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
292
+ <div className="space-y-2">
293
+ <Label htmlFor="fuelType">Fuel Type</Label>
294
+ <Input
295
+ id="fuelType"
296
+ value={form.fuelType}
297
+ onChange={(e) => setForm((prev) => ({ ...prev, fuelType: e.target.value }))}
298
+ placeholder="Diesel"
299
+ />
300
+ </div>
301
+ <div className="space-y-2">
302
+ <Label htmlFor="fuelEfficiency">Fuel Efficiency (L/100km)</Label>
303
+ <Input
304
+ id="fuelEfficiency"
305
+ type="number"
306
+ min={0}
307
+ step={0.1}
308
+ value={form.fuelEfficiencyLPer100Km}
309
+ onChange={(e) =>
310
+ setForm((prev) => ({ ...prev, fuelEfficiencyLPer100Km: e.target.value }))
311
+ }
312
+ placeholder="12.5"
313
+ />
314
+ </div>
315
+ </div>
210
316
  </div>
211
317
  </EmphasisPanel>
212
318
  </form>
@@ -394,6 +500,22 @@ export default function VehicleDetailsPage() {
394
500
  </p>
395
501
  </div>
396
502
  )}
503
+ {(vehicle.fuelType || vehicle.fuelEfficiencyLPer100Km !== undefined) && (
504
+ <div className="grid grid-cols-2 gap-4">
505
+ {vehicle.fuelType && (
506
+ <div>
507
+ <p className="text-sm text-text-secondary">Fuel Type</p>
508
+ <p className="font-medium">{vehicle.fuelType}</p>
509
+ </div>
510
+ )}
511
+ {vehicle.fuelEfficiencyLPer100Km !== undefined && (
512
+ <div>
513
+ <p className="text-sm text-text-secondary">Efficiency</p>
514
+ <p className="font-medium">{vehicle.fuelEfficiencyLPer100Km} L/100km</p>
515
+ </div>
516
+ )}
517
+ </div>
518
+ )}
397
519
  </CardContent>
398
520
  </Card>
399
521
 
@@ -346,6 +346,9 @@ export interface Vehicle {
346
346
  nextMaintenance?: Date;
347
347
  /** Odometer reading (km) at which the next service is due. */
348
348
  nextMaintenanceMileage?: number;
349
+ fuelType?: string;
350
+ /** Fuel consumption, litres per 100km. */
351
+ fuelEfficiencyLPer100Km?: number;
349
352
  fuelLevel?: number;
350
353
  mileage?: number;
351
354
  documents: VehicleDocument[];
@@ -353,7 +356,7 @@ export interface Vehicle {
353
356
  }
354
357
 
355
358
  // Driver type
356
- export type DriverStatus = 'available' | 'busy' | 'offline' | 'on_break';
359
+ export type DriverStatus = 'available' | 'busy' | 'offline' | 'on_break' | 'inactive';
357
360
 
358
361
  export interface DriverDocument {
359
362
  id: string;
@@ -377,44 +380,19 @@ export interface Driver {
377
380
  assignedVehicle?: string;
378
381
  status: DriverStatus;
379
382
  currentLocation?: Location;
380
- joinDate: Date;
381
- baseSalary: number;
382
- completedOrders: number;
383
- todayOrders: number;
384
- specializations: string[];
385
- workingHours: {
386
- startTime: string;
387
- endTime: string;
388
- workingDays: string[];
389
- maxHoursPerDay: number;
390
- maxHoursPerWeek: number;
391
- currentHoursToday: number;
392
- currentHoursThisWeek: number;
393
- };
394
- ratings: {
395
- overall: number;
396
- punctuality: number;
397
- communication: number;
398
- carefulHandling: number;
399
- professionalism: number;
400
- onTimeDelivery: number;
401
- totalRatings: number;
402
- };
383
+ /** MtwDriver.rating — a single overall rating; the backend does not track per-category breakdowns. */
384
+ rating: number;
385
+ /** Free-text HR/performance notes. */
386
+ notes?: string;
387
+ hireDate?: Date;
388
+ baseSalary?: number;
389
+ bonusYtd?: number;
390
+ payCurrency?: string;
391
+ /** Set and in the future means the driver is currently on leave. */
392
+ onLeaveUntil?: Date;
393
+ terminatedAt?: Date;
394
+ terminationReason?: string;
403
395
  documents: DriverDocument[];
404
- earnings: {
405
- totalEarnings: number;
406
- weeklyEarnings: number;
407
- monthlyEarnings: number;
408
- pendingPayments: number;
409
- completedDeliveries: number;
410
- bonuses: number;
411
- };
412
- compliance: {
413
- hoursOfService: boolean;
414
- documentation: boolean;
415
- vehicleInspection: boolean;
416
- lastUpdate: Date;
417
- };
418
396
  createdAt: Date;
419
397
  }
420
398