@burdenoff/microfe-movethewheels 2026.924.2 → 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.
Files changed (47) hide show
  1. package/dist/components/OrderItemRow.js +195 -109
  2. package/dist/components/StatusBadge.js +31 -31
  3. package/dist/constants/index.js +11 -3
  4. package/dist/generated/wspace-operations.d.ts +7 -0
  5. package/dist/hooks/useFleet.js +85 -76
  6. package/dist/hooks/useOrders.js +8 -4
  7. package/dist/hooks/useTracking.js +127 -66
  8. package/dist/movethewheels/components/OrderItemRow.d.ts +1 -2
  9. package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
  10. package/dist/movethewheels/constants/index.d.ts +1 -0
  11. package/dist/movethewheels/hooks/useFleet.d.ts +41 -1
  12. package/dist/movethewheels/hooks/useOrders.d.ts +1 -0
  13. package/dist/movethewheels/hooks/useTracking.d.ts +2 -2
  14. package/dist/movethewheels/types/index.d.ts +17 -39
  15. package/dist/node_modules/lucide-react/dist/esm/icons/sticky-note.js +11 -0
  16. package/dist/node_modules/lucide-react/dist/esm/icons/user-x.js +30 -0
  17. package/dist/pages/CreateOrderPage.js +597 -420
  18. package/dist/pages/DriverDetailsPage.js +702 -421
  19. package/dist/pages/DriversPage.js +106 -117
  20. package/dist/pages/FleetPage.js +4 -10
  21. package/dist/pages/OrderDetailsPage.js +290 -262
  22. package/dist/pages/OrdersListPage.js +4 -0
  23. package/dist/pages/RouteDetailsPage.js +264 -252
  24. package/dist/pages/TrackingPage.js +205 -195
  25. package/dist/pages/VehicleDetailsPage.js +403 -243
  26. package/dist/utils/mapMarkers.js +2 -1
  27. package/package.json +1 -1
  28. package/src/generated/wspace-operations.ts +10 -3
  29. package/src/movethewheels/components/OrderItemRow.tsx +162 -99
  30. package/src/movethewheels/components/StatusBadge.tsx +11 -2
  31. package/src/movethewheels/constants/index.ts +13 -0
  32. package/src/movethewheels/hooks/useFleet.ts +163 -44
  33. package/src/movethewheels/hooks/useOrders.ts +17 -3
  34. package/src/movethewheels/hooks/useTracking.ts +208 -27
  35. package/src/movethewheels/pages/CreateOrderPage.tsx +372 -263
  36. package/src/movethewheels/pages/DriverDetailsPage.tsx +516 -156
  37. package/src/movethewheels/pages/DriversPage.tsx +28 -11
  38. package/src/movethewheels/pages/FleetPage.tsx +7 -10
  39. package/src/movethewheels/pages/OrderDetailsPage.tsx +67 -30
  40. package/src/movethewheels/pages/OrdersListPage.tsx +1 -0
  41. package/src/movethewheels/pages/RouteDetailsPage.tsx +15 -7
  42. package/src/movethewheels/pages/TrackingPage.tsx +44 -14
  43. package/src/movethewheels/pages/VehicleDetailsPage.tsx +137 -15
  44. package/src/movethewheels/types/index.ts +17 -38
  45. package/src/movethewheels/utils/mapMarkers.ts +1 -0
  46. package/src/operations/wspace/movethewheels/queries/dashboard.graphql +5 -0
  47. package/src/operations/wspace/movethewheels/queries/logistics.graphql +2 -2
@@ -14,6 +14,9 @@ import {
14
14
  Edit,
15
15
  MapPin,
16
16
  Save,
17
+ UserX,
18
+ Truck,
19
+ StickyNote,
17
20
  } from 'lucide-react';
18
21
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
19
22
  import { StatusBadge } from '../components/StatusBadge';
@@ -23,34 +26,116 @@ import {
23
26
  CardHeader,
24
27
  CardTitle,
25
28
  Button,
26
- Badge,
27
- Progress,
28
29
  Input,
29
30
  Label,
31
+ Textarea,
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
30
37
  } from '../components/ui';
31
- import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
32
- import { useDriver, useFleet } from '../hooks/useFleet';
38
+ import {
39
+ EmphasisPanel,
40
+ NextSteps,
41
+ PagePurpose,
42
+ ResponsiveDialog,
43
+ ResponsiveDialogContent,
44
+ ResponsiveDialogHeader,
45
+ ResponsiveDialogFooter,
46
+ ResponsiveDialogTitle,
47
+ ResponsiveDialogDescription,
48
+ } from '@burdenoff/fe-libs/ui';
49
+ import { EntityCombobox } from '../components/EntityCombobox';
50
+ import {
51
+ useDriver,
52
+ useFleet,
53
+ useVehicle,
54
+ useVehicles,
55
+ toApiDriverStatus,
56
+ type UpdateDriverInput,
57
+ } from '../hooks/useFleet';
58
+ import { useDebouncedValue } from '../hooks/useDebouncedValue';
33
59
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
34
60
  import { formatCurrency, formatDate } from '../utils/formatters';
35
61
  import { joinPath } from '../utils/navigation';
36
62
  import { FormError } from '../components/FormError';
37
63
  import { DocumentsCard } from '../components/DocumentsCard';
64
+ import { DRIVER_STATUS_LABELS } from '../constants';
65
+ import type { DriverStatus } from '../types';
66
+
67
+ const STATUS_SELECT_OPTIONS: Array<{ value: DriverStatus; label: string }> = (
68
+ Object.entries(DRIVER_STATUS_LABELS) as Array<[DriverStatus, string]>
69
+ ).map(([value, label]) => ({ value, label }));
70
+
71
+ const toDateInput = (date: Date | undefined): string =>
72
+ date ? date.toISOString().slice(0, 10) : '';
73
+ /** Noon UTC avoids the date shifting a day in either direction across timezones. */
74
+ const fromDateInput = (value: string): string | undefined =>
75
+ value ? `${value}T12:00:00.000Z` : undefined;
76
+
77
+ interface DriverFormState {
78
+ employeeId: string;
79
+ firstName: string;
80
+ lastName: string;
81
+ email: string;
82
+ phone: string;
83
+ licenseNumber: string;
84
+ licenseClass: string;
85
+ licenseExpiry: string;
86
+ status: DriverStatus;
87
+ vehicleId: string;
88
+ rating: string;
89
+ notes: string;
90
+ hireDate: string;
91
+ baseSalary: string;
92
+ bonusYtd: string;
93
+ payCurrency: string;
94
+ onLeaveUntil: string;
95
+ }
96
+
97
+ const emptyForm: DriverFormState = {
98
+ employeeId: '',
99
+ firstName: '',
100
+ lastName: '',
101
+ email: '',
102
+ phone: '',
103
+ licenseNumber: '',
104
+ licenseClass: '',
105
+ licenseExpiry: '',
106
+ status: 'available',
107
+ vehicleId: '',
108
+ rating: '',
109
+ notes: '',
110
+ hireDate: '',
111
+ baseSalary: '',
112
+ bonusYtd: '',
113
+ payCurrency: '',
114
+ onLeaveUntil: '',
115
+ };
38
116
 
39
117
  function NewDriverForm({ editId }: { editId?: string }) {
40
118
  const { basePath, navigate } = useMoveTheWheels();
41
119
  const { createDriver, updateDriver } = useFleet();
42
- const { driver } = useDriver(editId);
120
+ const { driver, refetch: refetchDriver } = useDriver(editId);
43
121
  const isEditing = !!editId;
44
- const [form, setForm] = useState({
45
- employeeId: '',
46
- firstName: '',
47
- lastName: '',
48
- email: '',
49
- phone: '',
50
- licenseNumber: '',
51
- });
122
+ const [form, setForm] = useState<DriverFormState>(emptyForm);
52
123
  const [submitting, setSubmitting] = useState(false);
53
124
  const [error, setError] = useState<string | null>(null);
125
+ const [dismissOpen, setDismissOpen] = useState(false);
126
+ const [dismissReason, setDismissReason] = useState('');
127
+ const [dismissing, setDismissing] = useState(false);
128
+ const [dismissError, setDismissError] = useState<string | null>(null);
129
+
130
+ const [vehicleSearch, setVehicleSearch] = useState('');
131
+ const debouncedVehicleSearch = useDebouncedValue(vehicleSearch);
132
+ const { vehicles, isLoading: vehiclesLoading } = useVehicles({
133
+ limit: 20,
134
+ search: debouncedVehicleSearch || undefined,
135
+ });
136
+ const { vehicle: selectedVehicle, isLoading: selectedVehicleLoading } = useVehicle(
137
+ form.vehicleId || undefined
138
+ );
54
139
 
55
140
  useEffect(() => {
56
141
  if (!editId || !driver) return;
@@ -62,39 +147,59 @@ function NewDriverForm({ editId }: { editId?: string }) {
62
147
  email: driver.email || '',
63
148
  phone: driver.phone || '',
64
149
  licenseNumber: driver.licenseNumber || '',
150
+ licenseClass: driver.licenseType || '',
151
+ licenseExpiry: toDateInput(driver.licenseExpiry),
152
+ status: driver.status,
153
+ vehicleId: driver.assignedVehicle || '',
154
+ rating: driver.rating ? String(driver.rating) : '',
155
+ notes: driver.notes || '',
156
+ hireDate: toDateInput(driver.hireDate),
157
+ baseSalary: driver.baseSalary !== undefined ? String(driver.baseSalary) : '',
158
+ bonusYtd: driver.bonusYtd !== undefined ? String(driver.bonusYtd) : '',
159
+ payCurrency: driver.payCurrency || '',
160
+ onLeaveUntil: toDateInput(driver.onLeaveUntil),
65
161
  });
66
162
  }, [editId, driver]);
67
163
 
68
164
  const goBack = () => navigate?.(joinPath(basePath, 'fleet/drivers'));
69
165
 
166
+ const buildPayload = (): UpdateDriverInput => ({
167
+ employeeId: form.employeeId.trim(),
168
+ firstName: form.firstName.trim(),
169
+ lastName: form.lastName.trim(),
170
+ email: form.email.trim() || undefined,
171
+ phone: form.phone.trim() || undefined,
172
+ licenseNumber: form.licenseNumber.trim() || undefined,
173
+ licenseClass: form.licenseClass.trim() || undefined,
174
+ licenseExpiry: fromDateInput(form.licenseExpiry),
175
+ status: toApiDriverStatus(form.status),
176
+ vehicleId: form.vehicleId || undefined,
177
+ rating: form.rating ? Number(form.rating) : undefined,
178
+ notes: form.notes.trim() || undefined,
179
+ hireDate: fromDateInput(form.hireDate),
180
+ baseSalary: form.baseSalary ? Number(form.baseSalary) : undefined,
181
+ bonusYtd: form.bonusYtd ? Number(form.bonusYtd) : undefined,
182
+ payCurrency: form.payCurrency.trim() || undefined,
183
+ });
184
+
70
185
  const handleSubmit = async (e: React.FormEvent) => {
71
186
  e.preventDefault();
72
187
  if (!form.employeeId.trim() || !form.firstName.trim() || !form.lastName.trim()) {
73
188
  setError('Employee ID, first name and last name are required.');
74
189
  return;
75
190
  }
191
+ if (!form.phone.trim()) {
192
+ setError('Phone is required so this driver can be reached.');
193
+ return;
194
+ }
76
195
  setSubmitting(true);
77
196
  setError(null);
78
197
  try {
79
198
  if (editId) {
80
- const updated = await updateDriver(editId, {
81
- employeeId: form.employeeId.trim(),
82
- firstName: form.firstName.trim(),
83
- lastName: form.lastName.trim(),
84
- email: form.email.trim() || undefined,
85
- phone: form.phone.trim() || undefined,
86
- licenseNumber: form.licenseNumber.trim() || undefined,
87
- });
199
+ const updated = await updateDriver(editId, buildPayload());
88
200
  navigate?.(joinPath(basePath, `fleet/drivers/${updated.id}`));
89
201
  } else {
90
- const created = await createDriver({
91
- employeeId: form.employeeId.trim(),
92
- firstName: form.firstName.trim(),
93
- lastName: form.lastName.trim(),
94
- email: form.email.trim() || undefined,
95
- phone: form.phone.trim() || undefined,
96
- licenseNumber: form.licenseNumber.trim() || undefined,
97
- });
202
+ const created = await createDriver(buildPayload());
98
203
  navigate?.(joinPath(basePath, `fleet/drivers/${created.id}`));
99
204
  }
100
205
  } catch (err) {
@@ -104,6 +209,31 @@ function NewDriverForm({ editId }: { editId?: string }) {
104
209
  }
105
210
  };
106
211
 
212
+ const handleDismiss = async () => {
213
+ if (!editId || !dismissReason.trim()) {
214
+ setDismissError('A reason is required to dismiss a driver.');
215
+ return;
216
+ }
217
+ setDismissing(true);
218
+ setDismissError(null);
219
+ try {
220
+ await updateDriver(editId, {
221
+ ...buildPayload(),
222
+ terminatedAt: new Date().toISOString(),
223
+ terminationReason: dismissReason.trim(),
224
+ });
225
+ setDismissOpen(false);
226
+ await refetchDriver();
227
+ navigate?.(joinPath(basePath, `fleet/drivers/${editId}`));
228
+ } catch (err) {
229
+ setDismissError(err instanceof Error ? err.message : 'Failed to dismiss driver.');
230
+ } finally {
231
+ setDismissing(false);
232
+ }
233
+ };
234
+
235
+ const alreadyDismissed = isEditing && Boolean(driver?.terminatedAt);
236
+
107
237
  return (
108
238
  <PageLayout
109
239
  title={isEditing ? 'Edit Driver' : 'Add Driver'}
@@ -128,11 +258,21 @@ function NewDriverForm({ editId }: { editId?: string }) {
128
258
  }
129
259
  >
130
260
  <PagePurpose icon={<Users size={18} />}>
131
- Add a driver so they can be assigned to orders and routes. An employee ID and name get them
132
- on the roster — add contact details and license number to keep the record complete.
261
+ Add a driver so they can be assigned to orders and routes. An employee ID, name and phone
262
+ get them on the roster — add licensing, a vehicle and HR details to keep the record
263
+ complete.
133
264
  </PagePurpose>
134
265
 
135
- <form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
266
+ {alreadyDismissed && driver?.terminatedAt && (
267
+ <div className="rounded-card border border-status-error-border bg-status-error-bg-subtle p-4 text-sm text-status-error-text">
268
+ <p className="font-medium">
269
+ This driver was dismissed on {formatDate(driver.terminatedAt, 'full')}.
270
+ </p>
271
+ {driver.terminationReason && <p className="mt-1">Reason: {driver.terminationReason}</p>}
272
+ </div>
273
+ )}
274
+
275
+ <form onSubmit={handleSubmit} className="max-w-3xl space-y-6">
136
276
  <FormError message={error} />
137
277
  <EmphasisPanel>
138
278
  <h2 className="text-base font-semibold text-text-primary mb-4">Driver Details</h2>
@@ -187,14 +327,39 @@ function NewDriverForm({ editId }: { editId?: string }) {
187
327
  />
188
328
  </div>
189
329
  <div className="space-y-2">
190
- <Label htmlFor="phone">Phone</Label>
330
+ <Label htmlFor="phone">
331
+ Phone <span className="text-status-error-text">*</span>
332
+ </Label>
191
333
  <Input
192
334
  id="phone"
193
335
  value={form.phone}
194
336
  onChange={(e) => setForm((prev) => ({ ...prev, phone: e.target.value }))}
195
337
  placeholder="+1-555-0100"
338
+ required
196
339
  />
197
340
  </div>
341
+ <div className="space-y-2">
342
+ <Label htmlFor="status">Status</Label>
343
+ <Select
344
+ value={form.status}
345
+ onValueChange={(value) =>
346
+ setForm((prev) => ({ ...prev, status: value as DriverStatus }))
347
+ }
348
+ >
349
+ <SelectTrigger id="status">
350
+ <SelectValue />
351
+ </SelectTrigger>
352
+ <SelectContent>
353
+ {STATUS_SELECT_OPTIONS.map((option) => (
354
+ <SelectItem key={option.value} value={option.value}>
355
+ {option.label}
356
+ </SelectItem>
357
+ ))}
358
+ </SelectContent>
359
+ </Select>
360
+ </div>
361
+ </div>
362
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
198
363
  <div className="space-y-2">
199
364
  <Label htmlFor="licenseNumber">License Number</Label>
200
365
  <Input
@@ -204,10 +369,218 @@ function NewDriverForm({ editId }: { editId?: string }) {
204
369
  placeholder="D1234567"
205
370
  />
206
371
  </div>
372
+ <div className="space-y-2">
373
+ <Label htmlFor="licenseClass">License Class</Label>
374
+ <Input
375
+ id="licenseClass"
376
+ value={form.licenseClass}
377
+ onChange={(e) => setForm((prev) => ({ ...prev, licenseClass: e.target.value }))}
378
+ placeholder="Class A"
379
+ />
380
+ </div>
381
+ <div className="space-y-2">
382
+ <Label htmlFor="licenseExpiry">License Expiry</Label>
383
+ <Input
384
+ id="licenseExpiry"
385
+ type="date"
386
+ value={form.licenseExpiry}
387
+ onChange={(e) => setForm((prev) => ({ ...prev, licenseExpiry: e.target.value }))}
388
+ />
389
+ </div>
390
+ </div>
391
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
392
+ <div className="space-y-2">
393
+ <Label htmlFor="vehicleSearch">Search vehicles</Label>
394
+ <Input
395
+ id="vehicleSearch"
396
+ value={vehicleSearch}
397
+ onChange={(e) => setVehicleSearch(e.target.value)}
398
+ placeholder="Vehicle number or type"
399
+ />
400
+ </div>
401
+ <div className="space-y-2">
402
+ <Label htmlFor="vehicle">Assigned Vehicle</Label>
403
+ <EntityCombobox
404
+ id="vehicle"
405
+ value={form.vehicleId}
406
+ onChange={(value) => setForm((prev) => ({ ...prev, vehicleId: value }))}
407
+ options={vehicles}
408
+ loading={vehiclesLoading}
409
+ getId={(vehicle) => vehicle.id}
410
+ getLabel={(vehicle) => `${vehicle.vehicleNumber} (${vehicle.type})`}
411
+ renderOption={(vehicle) => (
412
+ <>
413
+ <Truck size={14} className="mr-2 inline text-text-secondary" />
414
+ {vehicle.vehicleNumber} ({vehicle.type})
415
+ </>
416
+ )}
417
+ selectedOption={selectedVehicle}
418
+ selectedLoading={selectedVehicleLoading}
419
+ placeholder="Unassigned"
420
+ emptyText="No vehicles match that search."
421
+ />
422
+ </div>
423
+ </div>
424
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
425
+ <div className="space-y-2">
426
+ <Label htmlFor="rating">Rating (0–5)</Label>
427
+ <Input
428
+ id="rating"
429
+ type="number"
430
+ min={0}
431
+ max={5}
432
+ step={0.1}
433
+ value={form.rating}
434
+ onChange={(e) => setForm((prev) => ({ ...prev, rating: e.target.value }))}
435
+ placeholder="4.8"
436
+ />
437
+ </div>
438
+ <div className="space-y-2">
439
+ <Label htmlFor="hireDate">Hire Date</Label>
440
+ <Input
441
+ id="hireDate"
442
+ type="date"
443
+ value={form.hireDate}
444
+ onChange={(e) => setForm((prev) => ({ ...prev, hireDate: e.target.value }))}
445
+ />
446
+ </div>
447
+ <div className="space-y-2">
448
+ <Label htmlFor="onLeaveUntil">On Leave Until</Label>
449
+ <Input
450
+ id="onLeaveUntil"
451
+ type="date"
452
+ value={form.onLeaveUntil}
453
+ onChange={(e) => setForm((prev) => ({ ...prev, onLeaveUntil: e.target.value }))}
454
+ />
455
+ </div>
207
456
  </div>
208
457
  </div>
209
458
  </EmphasisPanel>
459
+
460
+ <Card>
461
+ <CardHeader>
462
+ <CardTitle className="flex items-center gap-2">
463
+ <DollarSign size={18} /> Compensation
464
+ </CardTitle>
465
+ </CardHeader>
466
+ <CardContent>
467
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
468
+ <div className="space-y-2">
469
+ <Label htmlFor="baseSalary">Base Salary (annual)</Label>
470
+ <Input
471
+ id="baseSalary"
472
+ type="number"
473
+ min={0}
474
+ step={0.01}
475
+ value={form.baseSalary}
476
+ onChange={(e) => setForm((prev) => ({ ...prev, baseSalary: e.target.value }))}
477
+ placeholder="55000"
478
+ />
479
+ </div>
480
+ <div className="space-y-2">
481
+ <Label htmlFor="bonusYtd">Bonus (YTD)</Label>
482
+ <Input
483
+ id="bonusYtd"
484
+ type="number"
485
+ min={0}
486
+ step={0.01}
487
+ value={form.bonusYtd}
488
+ onChange={(e) => setForm((prev) => ({ ...prev, bonusYtd: e.target.value }))}
489
+ placeholder="1200"
490
+ />
491
+ </div>
492
+ <div className="space-y-2">
493
+ <Label htmlFor="payCurrency">Currency</Label>
494
+ <Input
495
+ id="payCurrency"
496
+ value={form.payCurrency}
497
+ onChange={(e) => setForm((prev) => ({ ...prev, payCurrency: e.target.value }))}
498
+ placeholder="USD"
499
+ maxLength={10}
500
+ />
501
+ </div>
502
+ </div>
503
+ </CardContent>
504
+ </Card>
505
+
506
+ <Card>
507
+ <CardHeader>
508
+ <CardTitle className="flex items-center gap-2">
509
+ <StickyNote size={18} /> HR Notes
510
+ </CardTitle>
511
+ </CardHeader>
512
+ <CardContent>
513
+ <Textarea
514
+ value={form.notes}
515
+ onChange={(e) => setForm((prev) => ({ ...prev, notes: e.target.value }))}
516
+ placeholder="Performance notes, disciplinary history, anything worth tracking for this driver..."
517
+ rows={4}
518
+ />
519
+ </CardContent>
520
+ </Card>
521
+
522
+ {isEditing && !alreadyDismissed && (
523
+ <Card className="border-status-error-border">
524
+ <CardHeader>
525
+ <CardTitle className="flex items-center gap-2 text-status-error-text">
526
+ <UserX size={18} /> Dismiss Driver
527
+ </CardTitle>
528
+ </CardHeader>
529
+ <CardContent>
530
+ <p className="mb-3 text-sm text-text-secondary">
531
+ Dismissing a driver marks them as no longer employed. Their delivery and route
532
+ history stays visible; they can no longer be assigned new work. This cannot be
533
+ undone from here.
534
+ </p>
535
+ <Button
536
+ type="button"
537
+ variant="outline"
538
+ className="border-status-error-border text-status-error-text hover:bg-status-error-bg-subtle"
539
+ onClick={() => setDismissOpen(true)}
540
+ >
541
+ <UserX size={16} /> Dismiss this driver
542
+ </Button>
543
+ </CardContent>
544
+ </Card>
545
+ )}
210
546
  </form>
547
+
548
+ <ResponsiveDialog open={dismissOpen} onOpenChange={setDismissOpen}>
549
+ <ResponsiveDialogContent>
550
+ <ResponsiveDialogHeader>
551
+ <ResponsiveDialogTitle>Dismiss this driver?</ResponsiveDialogTitle>
552
+ <ResponsiveDialogDescription>
553
+ This records the driver as dismissed and requires a reason. It cannot be undone from
554
+ here.
555
+ </ResponsiveDialogDescription>
556
+ </ResponsiveDialogHeader>
557
+ <div className="space-y-2 px-1">
558
+ <Label htmlFor="dismissReason">
559
+ Reason <span className="text-status-error-text">*</span>
560
+ </Label>
561
+ <Textarea
562
+ id="dismissReason"
563
+ value={dismissReason}
564
+ onChange={(e) => setDismissReason(e.target.value)}
565
+ placeholder="e.g. Repeated policy violations, voluntary resignation, performance..."
566
+ rows={3}
567
+ />
568
+ <FormError message={dismissError} />
569
+ </div>
570
+ <ResponsiveDialogFooter>
571
+ <Button variant="outline" onClick={() => setDismissOpen(false)} disabled={dismissing}>
572
+ Cancel
573
+ </Button>
574
+ <Button
575
+ className="bg-status-error-text text-text-inverse hover:opacity-90"
576
+ onClick={handleDismiss}
577
+ disabled={dismissing}
578
+ >
579
+ {dismissing ? 'Dismissing...' : 'Confirm dismissal'}
580
+ </Button>
581
+ </ResponsiveDialogFooter>
582
+ </ResponsiveDialogContent>
583
+ </ResponsiveDialog>
211
584
  </PageLayout>
212
585
  );
213
586
  }
@@ -217,6 +590,7 @@ export default function DriverDetailsPage() {
217
590
  const isEditRoute = useLocation().pathname.endsWith('/edit');
218
591
  const { basePath, navigate } = useMoveTheWheels();
219
592
  const { driver, isLoading, error, refetch } = useDriver(driverId);
593
+ const { vehicle: assignedVehicle } = useVehicle(driver?.assignedVehicle);
220
594
 
221
595
  const goBack = () => navigate?.(joinPath(basePath, 'fleet/drivers'));
222
596
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
@@ -226,6 +600,13 @@ export default function DriverDetailsPage() {
226
600
  if (isLoading) return <LoadingState message="Loading driver details..." />;
227
601
  if (error || !driver) return <ErrorState title="Driver not found" onRetry={refetch} />;
228
602
 
603
+ const isDismissed = Boolean(driver.terminatedAt);
604
+ const onLeaveUntil =
605
+ driver.onLeaveUntil && driver.onLeaveUntil.getTime() > new Date().getTime()
606
+ ? driver.onLeaveUntil
607
+ : undefined;
608
+ const hasCompensation = driver.baseSalary !== undefined || driver.bonusYtd !== undefined;
609
+
229
610
  return (
230
611
  <PageLayout
231
612
  title={driver.fullName}
@@ -244,20 +625,34 @@ export default function DriverDetailsPage() {
244
625
  </div>
245
626
  }
246
627
  headerContent={
247
- <div className="flex items-center gap-4">
628
+ <div className="flex flex-wrap items-center gap-4">
248
629
  <StatusBadge status={driver.status} type="driver" />
249
- <div className="flex items-center gap-1">
250
- <Star size={16} className="text-status-warning-text fill-current" />
251
- <span className="font-medium">{driver.ratings.overall.toFixed(1)}</span>
252
- <span className="text-text-secondary">({driver.ratings.totalRatings} ratings)</span>
253
- </div>
630
+ {isDismissed && (
631
+ <StatusBadge
632
+ status="dismissed"
633
+ label="Dismissed"
634
+ customColors={{ bg: 'bg-status-error-bg-subtle', text: 'text-status-error-text' }}
635
+ />
636
+ )}
637
+ {onLeaveUntil && !isDismissed && (
638
+ <StatusBadge
639
+ status="on_leave"
640
+ label={`On leave until ${formatDate(onLeaveUntil, 'short')}`}
641
+ customColors={{ bg: 'bg-status-warning-bg-subtle', text: 'text-status-warning-text' }}
642
+ />
643
+ )}
644
+ {driver.rating > 0 && (
645
+ <div className="flex items-center gap-1">
646
+ <Star size={16} className="text-status-warning-text fill-current" />
647
+ <span className="font-medium">{driver.rating.toFixed(1)}</span>
648
+ </div>
649
+ )}
254
650
  </div>
255
651
  }
256
652
  >
257
653
  <PagePurpose icon={<Users size={18} />}>
258
- A driver&apos;s full profile — performance ratings, working hours, documents, earnings and
259
- live location. Use it to confirm someone is road-ready and compliant before assigning them
260
- an order or route.
654
+ A driver&apos;s full profile — licensing, assigned vehicle, documents and HR record. Use it
655
+ to confirm someone is road-ready and compliant before assigning them an order or route.
261
656
  </PagePurpose>
262
657
 
263
658
  <NextSteps
@@ -280,75 +675,24 @@ export default function DriverDetailsPage() {
280
675
  {
281
676
  id: 'edit',
282
677
  label: 'Update driver record',
283
- description: 'Refresh documents, shift or compliance details.',
678
+ description: 'Refresh documents, license or HR details.',
284
679
  icon: <Edit size={16} />,
285
680
  onClick: () => {},
286
681
  },
287
682
  ]}
288
683
  />
289
684
 
685
+ {isDismissed && (
686
+ <div className="rounded-card border border-status-error-border bg-status-error-bg-subtle p-4 text-sm text-status-error-text">
687
+ <p className="font-medium">
688
+ Dismissed {driver.terminatedAt && formatDate(driver.terminatedAt, 'full')}
689
+ </p>
690
+ {driver.terminationReason && <p className="mt-1">Reason: {driver.terminationReason}</p>}
691
+ </div>
692
+ )}
693
+
290
694
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
291
695
  <div className="lg:col-span-2 space-y-6">
292
- {/* Ratings — the page's single emphasis zone */}
293
- <EmphasisPanel>
294
- <h2 className="flex items-center gap-2 text-base font-semibold text-text-primary mb-4">
295
- <Star size={18} /> Ratings
296
- </h2>
297
- <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
298
- {[
299
- { label: 'Punctuality', value: driver.ratings.punctuality },
300
- { label: 'Communication', value: driver.ratings.communication },
301
- { label: 'Handling', value: driver.ratings.carefulHandling },
302
- { label: 'Professionalism', value: driver.ratings.professionalism },
303
- { label: 'On-Time Delivery', value: driver.ratings.onTimeDelivery },
304
- ].map((rating) => (
305
- <div key={rating.label}>
306
- <p className="text-sm text-text-secondary">{rating.label}</p>
307
- <div className="flex items-center gap-2 mt-1">
308
- <Progress value={rating.value * 20} className="flex-1 h-2" />
309
- <span className="text-sm font-medium">{rating.value.toFixed(1)}</span>
310
- </div>
311
- </div>
312
- ))}
313
- </div>
314
- </EmphasisPanel>
315
-
316
- {/* Working Hours */}
317
- <Card>
318
- <CardHeader>
319
- <CardTitle className="flex items-center gap-2">
320
- <Clock size={18} /> Working Hours
321
- </CardTitle>
322
- </CardHeader>
323
- <CardContent>
324
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
325
- <div>
326
- <p className="text-sm text-text-secondary">Shift</p>
327
- <p className="font-medium">
328
- {driver.workingHours.startTime} - {driver.workingHours.endTime}
329
- </p>
330
- </div>
331
- <div>
332
- <p className="text-sm text-text-secondary">Working Days</p>
333
- <p className="font-medium">{driver.workingHours.workingDays.length} days/week</p>
334
- </div>
335
- <div>
336
- <p className="text-sm text-text-secondary">Hours Today</p>
337
- <p className="font-medium">
338
- {driver.workingHours.currentHoursToday}h / {driver.workingHours.maxHoursPerDay}h
339
- </p>
340
- </div>
341
- <div>
342
- <p className="text-sm text-text-secondary">Hours This Week</p>
343
- <p className="font-medium">
344
- {driver.workingHours.currentHoursThisWeek}h /{' '}
345
- {driver.workingHours.maxHoursPerWeek}h
346
- </p>
347
- </div>
348
- </div>
349
- </CardContent>
350
- </Card>
351
-
352
696
  {/* License — real driver credential fields */}
353
697
  <Card>
354
698
  <CardHeader>
@@ -372,60 +716,95 @@ export default function DriverDetailsPage() {
372
716
  </CardContent>
373
717
  </Card>
374
718
 
719
+ {driver.notes && (
720
+ <Card>
721
+ <CardHeader>
722
+ <CardTitle className="flex items-center gap-2">
723
+ <StickyNote size={18} /> HR Notes
724
+ </CardTitle>
725
+ </CardHeader>
726
+ <CardContent>
727
+ <p className="text-sm whitespace-pre-wrap">{driver.notes}</p>
728
+ </CardContent>
729
+ </Card>
730
+ )}
731
+
375
732
  {/* Documents — real files via wspace-files-svc */}
376
733
  <DocumentsCard entityType="driver" entityId={driver.id} />
377
734
  </div>
378
735
 
379
736
  <div className="space-y-6">
380
- {/* Stats */}
737
+ {/* Assigned Vehicle */}
738
+ <Card>
739
+ <CardHeader>
740
+ <CardTitle className="flex items-center gap-2">
741
+ <Truck size={18} /> Assigned Vehicle
742
+ </CardTitle>
743
+ </CardHeader>
744
+ <CardContent>
745
+ {driver.assignedVehicle ? (
746
+ assignedVehicle ? (
747
+ <button
748
+ type="button"
749
+ className="text-sm font-medium text-primary hover:underline"
750
+ onClick={() => goTo(`fleet/vehicles/${assignedVehicle.id}`)}
751
+ >
752
+ {assignedVehicle.vehicleNumber} ({assignedVehicle.type})
753
+ </button>
754
+ ) : (
755
+ <p className="text-sm text-text-secondary">Loading…</p>
756
+ )
757
+ ) : (
758
+ <p className="text-sm text-text-secondary">No vehicle assigned</p>
759
+ )}
760
+ </CardContent>
761
+ </Card>
762
+
763
+ {/* Record */}
381
764
  <Card>
382
765
  <CardHeader>
383
- <CardTitle>Statistics</CardTitle>
766
+ <CardTitle>Record</CardTitle>
384
767
  </CardHeader>
385
768
  <CardContent className="space-y-4">
386
769
  <div>
387
- <p className="text-sm text-text-secondary">Total Deliveries</p>
388
- <p className="text-2xl font-bold">{driver.completedOrders.toLocaleString()}</p>
389
- </div>
390
- <div>
391
- <p className="text-sm text-text-secondary">Today's Deliveries</p>
392
- <p className="text-2xl font-bold">{driver.todayOrders}</p>
393
- </div>
394
- <div>
395
- <p className="text-sm text-text-secondary">Join Date</p>
396
- <p className="font-medium">{formatDate(driver.joinDate)}</p>
770
+ <p className="text-sm text-text-secondary">Hire Date</p>
771
+ <p className="font-medium">
772
+ {driver.hireDate ? formatDate(driver.hireDate) : 'Not recorded'}
773
+ </p>
397
774
  </div>
398
775
  </CardContent>
399
776
  </Card>
400
777
 
401
- {/* Earnings */}
778
+ {/* Compensation — real fields, no fabricated earnings breakdown */}
402
779
  <Card>
403
780
  <CardHeader>
404
781
  <CardTitle className="flex items-center gap-2">
405
- <DollarSign size={18} /> Earnings
782
+ <DollarSign size={18} /> Compensation
406
783
  </CardTitle>
407
784
  </CardHeader>
408
785
  <CardContent className="space-y-3">
409
- <div>
410
- <p className="text-sm text-text-secondary">Total Earnings</p>
411
- <p className="text-2xl font-bold">
412
- {formatCurrency(driver.earnings.totalEarnings)}
413
- </p>
414
- </div>
415
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
416
- <div>
417
- <p className="text-sm text-text-secondary">This Week</p>
418
- <p className="font-medium">{formatCurrency(driver.earnings.weeklyEarnings)}</p>
419
- </div>
420
- <div>
421
- <p className="text-sm text-text-secondary">This Month</p>
422
- <p className="font-medium">{formatCurrency(driver.earnings.monthlyEarnings)}</p>
423
- </div>
424
- </div>
425
- <div>
426
- <p className="text-sm text-text-secondary">Base Salary</p>
427
- <p className="font-medium">{formatCurrency(driver.baseSalary)}/year</p>
428
- </div>
786
+ {hasCompensation ? (
787
+ <>
788
+ {driver.baseSalary !== undefined && (
789
+ <div>
790
+ <p className="text-sm text-text-secondary">Base Salary</p>
791
+ <p className="font-medium">
792
+ {formatCurrency(driver.baseSalary, driver.payCurrency || undefined)}/year
793
+ </p>
794
+ </div>
795
+ )}
796
+ {driver.bonusYtd !== undefined && (
797
+ <div>
798
+ <p className="text-sm text-text-secondary">Bonus (YTD)</p>
799
+ <p className="font-medium">
800
+ {formatCurrency(driver.bonusYtd, driver.payCurrency || undefined)}
801
+ </p>
802
+ </div>
803
+ )}
804
+ </>
805
+ ) : (
806
+ <p className="text-sm text-text-secondary">Not recorded</p>
807
+ )}
429
808
  </CardContent>
430
809
  </Card>
431
810
 
@@ -447,25 +826,6 @@ export default function DriverDetailsPage() {
447
826
  </CardContent>
448
827
  </Card>
449
828
  )}
450
-
451
- {/* Compliance */}
452
- <Card>
453
- <CardHeader>
454
- <CardTitle>Compliance Status</CardTitle>
455
- </CardHeader>
456
- <CardContent className="space-y-2">
457
- {Object.entries(driver.compliance)
458
- .filter(([k]) => k !== 'lastUpdate')
459
- .map(([key, value]) => (
460
- <div key={key} className="flex items-center justify-between">
461
- <span className="text-sm capitalize">{key.replace(/([A-Z])/g, ' $1')}</span>
462
- <Badge variant={value ? 'default' : 'destructive'}>
463
- {value ? 'Compliant' : 'Non-compliant'}
464
- </Badge>
465
- </div>
466
- ))}
467
- </CardContent>
468
- </Card>
469
829
  </div>
470
830
  </div>
471
831
  </PageLayout>