@burdenoff/microfe-movethewheels 2026.703.1 → 2026.703.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.
Files changed (72) hide show
  1. package/dist/{AIAssistantPage-C3ovZ0Fh.js → AIAssistantPage-C3cPsWt-.js} +1 -1
  2. package/dist/{AnalyticsPage-O8SpY-V4.js → AnalyticsPage-Dz6g2MvK.js} +6 -6
  3. package/dist/{CreateOrderPage-CB1_-_mN.js → CreateOrderPage-C5iwnjp4.js} +29 -58
  4. package/dist/CustomerDetailsPage-B6nwFyLO.js +406 -0
  5. package/dist/{CustomersPage-DRaLuHMC.js → CustomersPage-BKxMS1AV.js} +5 -5
  6. package/dist/{DashboardPage-DPjhNfKB.js → DashboardPage-BVjQi0EB.js} +11 -11
  7. package/dist/DriverDetailsPage-CN3WQ0B9.js +507 -0
  8. package/dist/{DriversPage-C2Bk0w4Q.js → DriversPage-CH9rjQC-.js} +6 -6
  9. package/dist/{FinancePage-lmp0Sdsw.js → FinancePage-DEDykSOP.js} +5 -5
  10. package/dist/{FleetPage-DZn5pIqd.js → FleetPage-DqkFi3s3.js} +5 -5
  11. package/dist/FormError-BlOjnXOq.js +15 -0
  12. package/dist/{ImportExportPage-BIUJZc7O.js → ImportExportPage-DKJRCezl.js} +1 -1
  13. package/dist/{InventoryPage-Cxh1zYJd.js → InventoryPage-e0ze63iX.js} +6 -6
  14. package/dist/{LiveTrackingPage-BY1yAkIt.js → LiveTrackingPage-DCX-K-LR.js} +5 -5
  15. package/dist/{MarketplacePage-RMuL17EA.js → MarketplacePage-CIQ97_zp.js} +1 -1
  16. package/dist/{OrderDetailsPage-C2Mf4KSy.js → OrderDetailsPage-DYiZ-9UK.js} +5 -5
  17. package/dist/{OrdersListPage-BrPBtnhp.js → OrdersListPage-Cn90P1Xx.js} +4 -4
  18. package/dist/ProductDetailsPage-C9uOiYLp.js +350 -0
  19. package/dist/{ProductsPage-I5DilIoY.js → ProductsPage-QJfHHpqB.js} +4 -4
  20. package/dist/{ReportsPage-BDXNmUng.js → ReportsPage-Zp53q3_8.js} +2 -2
  21. package/dist/RouteDetailsPage-DUStXSf9.js +531 -0
  22. package/dist/{RoutesPage-lqX9ylHP.js → RoutesPage-DMfoicM1.js} +3 -3
  23. package/dist/{SettingsPage-CQVIOaNs.js → SettingsPage-Bn-v0mbQ.js} +1 -1
  24. package/dist/{TrackingPage-BXrjz7V8.js → TrackingPage-W-jtdM3Y.js} +4 -4
  25. package/dist/VehicleDetailsPage-Bceo5oz3.js +410 -0
  26. package/dist/{VehiclesPage-6W39XT5W.js → VehiclesPage-ApKThYeG.js} +5 -5
  27. package/dist/WarehouseDetailsPage-L_6sWigM.js +493 -0
  28. package/dist/{WarehousesPage-CHvBqNaQ.js → WarehousesPage-DCbXXXWE.js} +3 -3
  29. package/dist/index.js +154 -138
  30. package/dist/trash-2-BKhar6Hs.js +32 -0
  31. package/dist/{useFleet-DiAC-IiH.js → useFleet-DKqXZ16m.js} +77 -56
  32. package/dist/{useInventory-DKoks37x.js → useInventory-DkyiVY_A.js} +73 -53
  33. package/dist/{useTracking-GJify4HN.js → useTracking-B-va1kO3.js} +2 -2
  34. package/package.json +1 -1
  35. package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
  36. package/src/movethewheels/components/FormError.tsx +25 -0
  37. package/src/movethewheels/hooks/useFleet.ts +107 -0
  38. package/src/movethewheels/hooks/useInventory.ts +107 -0
  39. package/src/movethewheels/pages/CustomerDetailsPage.tsx +153 -20
  40. package/src/movethewheels/pages/CustomersPage.tsx +1 -1
  41. package/src/movethewheels/pages/DriverDetailsPage.tsx +159 -2
  42. package/src/movethewheels/pages/DriversPage.tsx +6 -2
  43. package/src/movethewheels/pages/InventoryPage.tsx +1 -1
  44. package/src/movethewheels/pages/OrdersListPage.tsx +1 -1
  45. package/src/movethewheels/pages/ProductDetailsPage.tsx +146 -3
  46. package/src/movethewheels/pages/ProductsPage.tsx +6 -2
  47. package/src/movethewheels/pages/RouteDetailsPage.tsx +285 -25
  48. package/src/movethewheels/pages/VehicleDetailsPage.tsx +158 -2
  49. package/src/movethewheels/pages/VehiclesPage.tsx +6 -2
  50. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +224 -2
  51. package/src/movethewheels/pages/WarehousesPage.tsx +5 -1
  52. package/dist/CustomerDetailsPage-BaeKEI1D.js +0 -263
  53. package/dist/DriverDetailsPage-CSSQTOzn.js +0 -328
  54. package/dist/ProductDetailsPage-CezzzZcj.js +0 -193
  55. package/dist/RouteDetailsPage-BcomL20M.js +0 -263
  56. package/dist/VehicleDetailsPage-DZjLcJgr.js +0 -218
  57. package/dist/WarehouseDetailsPage-C10pgRGW.js +0 -236
  58. /package/dist/{MetricCard-7aYgzZHK.js → MetricCard-BY0QrMD4.js} +0 -0
  59. /package/dist/{StatusBadge-NpTeHVlw.js → StatusBadge-BZurYHMy.js} +0 -0
  60. /package/dist/{formatters-DhKZ-eA1.js → formatters-CF2S2l1m.js} +0 -0
  61. /package/dist/{trending-up-B7W-GL6P.js → trending-up-Dohgd4t_.js} +0 -0
  62. /package/dist/{triangle-alert-BonHFFNz.js → triangle-alert-i-XQL9Rd.js} +0 -0
  63. /package/dist/{truck-DrlfSpRC.js → truck-C0v_hbLR.js} +0 -0
  64. /package/dist/{useAnalytics-CgdzUfY_.js → useAnalytics-DS8n0xgg.js} +0 -0
  65. /package/dist/{useCustomers-QrlozybF.js → useCustomers-BmTNGxiI.js} +0 -0
  66. /package/dist/{useImportExport-BLMnAKCC.js → useImportExport-DHr-SWSw.js} +0 -0
  67. /package/dist/{useOrders-D3UZVy8Y.js → useOrders-Cfk1GLiq.js} +0 -0
  68. /package/dist/{useReports-Puq4DCH9.js → useReports-CTqZr-AB.js} +0 -0
  69. /package/dist/{useRoutes-Do57LvRb.js → useRoutes-BQWO607N.js} +0 -0
  70. /package/dist/{user-CjSoudeu.js → user-jyFU3lXk.js} +0 -0
  71. /package/dist/{users-BAM4gHTn.js → users-DZ-ilS6B.js} +0 -0
  72. /package/dist/{warehouse-C7MSr47B.js → warehouse-6m5kXMLu.js} +0 -0
@@ -7,6 +7,24 @@ interface UseFleetOptions {
7
7
  filters?: FleetFilter;
8
8
  }
9
9
 
10
+ export interface CreateVehicleInput {
11
+ vehicleNumber: string;
12
+ /** VehicleType enum value, e.g. `TRUCK`, `VAN`. */
13
+ type: string;
14
+ make?: string;
15
+ model?: string;
16
+ licensePlate?: string;
17
+ }
18
+
19
+ export interface CreateDriverInput {
20
+ employeeId: string;
21
+ firstName: string;
22
+ lastName: string;
23
+ email?: string;
24
+ phone?: string;
25
+ licenseNumber?: string;
26
+ }
27
+
10
28
  interface UseFleetResult {
11
29
  vehicles: Vehicle[];
12
30
  drivers: Driver[];
@@ -15,6 +33,8 @@ interface UseFleetResult {
15
33
  isLoading: boolean;
16
34
  error: Error | null;
17
35
  refetch: () => void;
36
+ createVehicle: (input: CreateVehicleInput) => Promise<Vehicle>;
37
+ createDriver: (input: CreateDriverInput) => Promise<Driver>;
18
38
  }
19
39
 
20
40
  interface ApiVehicle {
@@ -189,6 +209,46 @@ const UPDATE_DRIVER_STATUS_MUTATION = `
189
209
  }
190
210
  `;
191
211
 
212
+ const CREATE_VEHICLE_MUTATION = `
213
+ mutation CreateVehicle($input: CreateVehicleInput!) {
214
+ createVehicle(input: $input) {
215
+ id
216
+ vehicleNumber
217
+ type
218
+ make
219
+ model
220
+ year
221
+ capacity
222
+ status
223
+ currentLocation
224
+ fuelLevel
225
+ mileage
226
+ createdAt
227
+ updatedAt
228
+ }
229
+ }
230
+ `;
231
+
232
+ const CREATE_DRIVER_MUTATION = `
233
+ mutation CreateDriver($input: CreateDriverInput!) {
234
+ createDriver(input: $input) {
235
+ id
236
+ userId
237
+ firstName
238
+ lastName
239
+ employeeId
240
+ licenseNumber
241
+ licenseClass
242
+ licenseExpiry
243
+ status
244
+ currentLocation
245
+ rating
246
+ vehicleId
247
+ createdAt
248
+ }
249
+ }
250
+ `;
251
+
192
252
  const FLEET_STATS_QUERY = `
193
253
  query GetFleetStats {
194
254
  fleetStats {
@@ -416,6 +476,51 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
416
476
  void load();
417
477
  }, [load]);
418
478
 
479
+ const createVehicle = useCallback(
480
+ async (input: CreateVehicleInput): Promise<Vehicle> => {
481
+ const data = await executeGraphQL<{ createVehicle: ApiVehicle }>(
482
+ graphQLOptions,
483
+ CREATE_VEHICLE_MUTATION,
484
+ {
485
+ input: {
486
+ vehicleNumber: input.vehicleNumber,
487
+ type: input.type,
488
+ make: input.make,
489
+ model: input.model,
490
+ licensePlate: input.licensePlate,
491
+ },
492
+ }
493
+ );
494
+ const created = mapVehicle(data.createVehicle);
495
+ setVehicles((previous) => [created, ...previous]);
496
+ return created;
497
+ },
498
+ [graphQLOptions]
499
+ );
500
+
501
+ const createDriver = useCallback(
502
+ async (input: CreateDriverInput): Promise<Driver> => {
503
+ const data = await executeGraphQL<{ createDriver: ApiDriver }>(
504
+ graphQLOptions,
505
+ CREATE_DRIVER_MUTATION,
506
+ {
507
+ input: {
508
+ employeeId: input.employeeId,
509
+ firstName: input.firstName,
510
+ lastName: input.lastName,
511
+ email: input.email,
512
+ phone: input.phone,
513
+ licenseNumber: input.licenseNumber,
514
+ },
515
+ }
516
+ );
517
+ const created = mapDriver(data.createDriver);
518
+ setDrivers((previous) => [created, ...previous]);
519
+ return created;
520
+ },
521
+ [graphQLOptions]
522
+ );
523
+
419
524
  return {
420
525
  vehicles,
421
526
  drivers,
@@ -424,6 +529,8 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
424
529
  isLoading,
425
530
  error,
426
531
  refetch,
532
+ createVehicle,
533
+ createDriver,
427
534
  };
428
535
  }
429
536
 
@@ -7,6 +7,29 @@ interface UseInventoryOptions {
7
7
  filters?: InventoryFilter;
8
8
  }
9
9
 
10
+ export interface CreateProductInput {
11
+ name: string;
12
+ sku: string;
13
+ category?: string;
14
+ brand?: string;
15
+ weight?: number;
16
+ }
17
+
18
+ export interface CreateWarehouseInput {
19
+ code: string;
20
+ name: string;
21
+ /** WarehouseType enum value, e.g. `DISTRIBUTION`, `FULFILLMENT`. */
22
+ type: string;
23
+ address: {
24
+ street: string;
25
+ city: string;
26
+ state: string;
27
+ postalCode: string;
28
+ country: string;
29
+ };
30
+ totalCapacity?: number;
31
+ }
32
+
10
33
  interface UseInventoryResult {
11
34
  inventory: Inventory[];
12
35
  products: Product[];
@@ -22,6 +45,8 @@ interface UseInventoryResult {
22
45
  productId: string,
23
46
  quantity: number
24
47
  ) => Promise<void>;
48
+ createProduct: (input: CreateProductInput) => Promise<Product>;
49
+ createWarehouse: (input: CreateWarehouseInput) => Promise<Warehouse>;
25
50
  }
26
51
 
27
52
  interface ApiInventoryItem {
@@ -166,6 +191,42 @@ const INVENTORY_STATS_QUERY = `
166
191
  }
167
192
  `;
168
193
 
194
+ const CREATE_PRODUCT_MUTATION = `
195
+ mutation CreateProduct($input: CreateProductInput!) {
196
+ createProduct(input: $input) {
197
+ id
198
+ sku
199
+ name
200
+ description
201
+ category
202
+ brand
203
+ dimensions
204
+ weight
205
+ value
206
+ hazardous
207
+ fragile
208
+ temperatureControlled
209
+ createdAt
210
+ updatedAt
211
+ }
212
+ }
213
+ `;
214
+
215
+ const CREATE_WAREHOUSE_MUTATION = `
216
+ mutation CreateWarehouse($input: CreateWarehouseInput!) {
217
+ createWarehouse(input: $input) {
218
+ id
219
+ name
220
+ code
221
+ type
222
+ address
223
+ totalCapacity
224
+ usedCapacity
225
+ createdAt
226
+ }
227
+ }
228
+ `;
229
+
169
230
  const ADJUST_INVENTORY_MUTATION = `
170
231
  mutation AdjustInventoryStock($input: AdjustInventoryStockInput!) {
171
232
  adjustInventoryStock(input: $input) {
@@ -412,6 +473,50 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
412
473
  [inventory, updateStock]
413
474
  );
414
475
 
476
+ const createProduct = useCallback(
477
+ async (input: CreateProductInput): Promise<Product> => {
478
+ const data = await executeGraphQL<{ createProduct: ApiProduct }>(
479
+ graphQLOptions,
480
+ CREATE_PRODUCT_MUTATION,
481
+ {
482
+ input: {
483
+ name: input.name,
484
+ sku: input.sku,
485
+ category: input.category,
486
+ brand: input.brand,
487
+ weight: input.weight,
488
+ },
489
+ }
490
+ );
491
+ const created = mapProduct(data.createProduct);
492
+ setProducts((previous) => [created, ...previous]);
493
+ return created;
494
+ },
495
+ [graphQLOptions]
496
+ );
497
+
498
+ const createWarehouse = useCallback(
499
+ async (input: CreateWarehouseInput): Promise<Warehouse> => {
500
+ const data = await executeGraphQL<{ createWarehouse: ApiWarehouse }>(
501
+ graphQLOptions,
502
+ CREATE_WAREHOUSE_MUTATION,
503
+ {
504
+ input: {
505
+ code: input.code,
506
+ name: input.name,
507
+ type: input.type,
508
+ address: input.address,
509
+ totalCapacity: input.totalCapacity,
510
+ },
511
+ }
512
+ );
513
+ const created = mapWarehouse(data.createWarehouse);
514
+ setWarehouses((previous) => [created, ...previous]);
515
+ return created;
516
+ },
517
+ [graphQLOptions]
518
+ );
519
+
415
520
  return {
416
521
  inventory,
417
522
  products,
@@ -422,6 +527,8 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
422
527
  refetch,
423
528
  updateStock,
424
529
  transferStock,
530
+ createProduct,
531
+ createWarehouse,
425
532
  };
426
533
  }
427
534
 
@@ -2,6 +2,7 @@
2
2
  * Customer Details Page
3
3
  */
4
4
 
5
+ import { useState } from 'react';
5
6
  import { useParams } from 'react-router-dom';
6
7
  import {
7
8
  Users,
@@ -13,15 +14,164 @@ import {
13
14
  Star,
14
15
  Building,
15
16
  FileText,
17
+ Save,
16
18
  } from 'lucide-react';
17
19
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
18
20
  import { TypeBadge } from '../components/StatusBadge';
19
- import { Card, CardContent, CardHeader, CardTitle, Button, Badge } from '../components/ui';
21
+ import {
22
+ Card,
23
+ CardContent,
24
+ CardHeader,
25
+ CardTitle,
26
+ Button,
27
+ Badge,
28
+ Input,
29
+ Label,
30
+ Select,
31
+ SelectContent,
32
+ SelectItem,
33
+ SelectTrigger,
34
+ SelectValue,
35
+ } from '../components/ui';
20
36
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
21
- import { useCustomer } from '../hooks/useCustomers';
37
+ import { useCustomer, useCustomers } from '../hooks/useCustomers';
22
38
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
23
39
  import { formatAddress, formatDate } from '../utils/formatters';
24
40
  import { joinPath } from '../utils/navigation';
41
+ import { FormError } from '../components/FormError';
42
+
43
+ function NewCustomerForm() {
44
+ const { basePath, navigate } = useMoveTheWheels();
45
+ const { createCustomer } = useCustomers();
46
+ const [form, setForm] = useState({
47
+ name: '',
48
+ type: 'b2c' as 'b2b' | 'b2c',
49
+ email: '',
50
+ phone: '',
51
+ company: '',
52
+ });
53
+ const [submitting, setSubmitting] = useState(false);
54
+ const [error, setError] = useState<string | null>(null);
55
+
56
+ const goBack = () => navigate?.(joinPath(basePath, 'customers'));
57
+
58
+ const handleSubmit = async (e: React.FormEvent) => {
59
+ e.preventDefault();
60
+ if (!form.name.trim()) {
61
+ setError('Customer name is required.');
62
+ return;
63
+ }
64
+ setSubmitting(true);
65
+ setError(null);
66
+ try {
67
+ const created = await createCustomer({
68
+ name: form.name.trim(),
69
+ type: form.type,
70
+ email: form.email.trim() || undefined,
71
+ phone: form.phone.trim() || undefined,
72
+ company: form.company.trim() || undefined,
73
+ });
74
+ navigate?.(joinPath(basePath, `customers/${created.id}`));
75
+ } catch (err) {
76
+ console.error('Failed to create customer:', err);
77
+ setError(err instanceof Error ? err.message : 'Failed to create customer. Please try again.');
78
+ setSubmitting(false);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <PageLayout
84
+ title="New Customer"
85
+ subtitle="Add a new customer to your account"
86
+ icon={<Users size={24} />}
87
+ headerActions={
88
+ <div className="flex gap-2">
89
+ <Button variant="outline" onClick={goBack} disabled={submitting}>
90
+ <ArrowLeft size={16} /> Cancel
91
+ </Button>
92
+ <Button onClick={handleSubmit} disabled={submitting}>
93
+ <Save size={16} /> {submitting ? 'Saving...' : 'Create Customer'}
94
+ </Button>
95
+ </div>
96
+ }
97
+ >
98
+ <PagePurpose icon={<Users size={18} />}>
99
+ Add a customer once and their contact details auto-fill every future order you book for them.
100
+ Only a name is required — add email, phone and company to make deliveries easier to arrange.
101
+ </PagePurpose>
102
+
103
+ <form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
104
+ <FormError message={error} />
105
+ <EmphasisPanel>
106
+ <h2 className="text-base font-semibold text-text-primary mb-4">Customer Details</h2>
107
+ <div className="space-y-4">
108
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
109
+ <div className="space-y-2">
110
+ <Label htmlFor="name">
111
+ Name <span className="text-status-error-text">*</span>
112
+ </Label>
113
+ <Input
114
+ id="name"
115
+ value={form.name}
116
+ onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
117
+ placeholder="Acme Corp or Jane Doe"
118
+ required
119
+ />
120
+ </div>
121
+ <div className="space-y-2">
122
+ <Label htmlFor="type">Customer Type</Label>
123
+ <Select
124
+ value={form.type}
125
+ onValueChange={(value) =>
126
+ setForm((prev) => ({ ...prev, type: value as 'b2b' | 'b2c' }))
127
+ }
128
+ >
129
+ <SelectTrigger id="type">
130
+ <SelectValue />
131
+ </SelectTrigger>
132
+ <SelectContent>
133
+ <SelectItem value="b2c">B2C (Individual)</SelectItem>
134
+ <SelectItem value="b2b">B2B (Business)</SelectItem>
135
+ </SelectContent>
136
+ </Select>
137
+ </div>
138
+ </div>
139
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
140
+ <div className="space-y-2">
141
+ <Label htmlFor="email">Email</Label>
142
+ <Input
143
+ id="email"
144
+ type="email"
145
+ value={form.email}
146
+ onChange={(e) => setForm((prev) => ({ ...prev, email: e.target.value }))}
147
+ placeholder="customer@example.com"
148
+ />
149
+ </div>
150
+ <div className="space-y-2">
151
+ <Label htmlFor="phone">Phone</Label>
152
+ <Input
153
+ id="phone"
154
+ value={form.phone}
155
+ onChange={(e) => setForm((prev) => ({ ...prev, phone: e.target.value }))}
156
+ placeholder="+1-555-0100"
157
+ />
158
+ </div>
159
+ </div>
160
+ <div className="space-y-2">
161
+ <Label htmlFor="company">Company</Label>
162
+ <Input
163
+ id="company"
164
+ value={form.company}
165
+ onChange={(e) => setForm((prev) => ({ ...prev, company: e.target.value }))}
166
+ placeholder="Company name (for B2B accounts)"
167
+ />
168
+ </div>
169
+ </div>
170
+ </EmphasisPanel>
171
+ </form>
172
+ </PageLayout>
173
+ );
174
+ }
25
175
 
26
176
  export default function CustomerDetailsPage() {
27
177
  const { customerId } = useParams<{ customerId: string }>();
@@ -33,24 +183,7 @@ export default function CustomerDetailsPage() {
33
183
 
34
184
  // Handle new customer form
35
185
  if (!customerId) {
36
- return (
37
- <PageLayout
38
- title="New Customer"
39
- subtitle="Add a new customer"
40
- icon={<Users size={24} />}
41
- headerActions={
42
- <Button variant="outline" onClick={goBack}>
43
- <ArrowLeft size={16} /> Back
44
- </Button>
45
- }
46
- >
47
- <Card>
48
- <CardContent className="p-8 text-center">
49
- <p className="text-text-secondary">Customer creation form would go here</p>
50
- </CardContent>
51
- </Card>
52
- </PageLayout>
53
- );
186
+ return <NewCustomerForm />;
54
187
  }
55
188
 
56
189
  if (isLoading) return <LoadingState message="Loading customer details..." />;
@@ -96,7 +96,7 @@ export default function CustomersPage() {
96
96
  }}
97
97
  actions={[
98
98
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
99
- { label: 'Export', icon: <Download size={16} />, onSelect: () => {} },
99
+ { label: 'Export', icon: <Download size={16} />, onSelect: () => goTo('import-export') },
100
100
  ]}
101
101
  />
102
102
  }
@@ -2,8 +2,19 @@
2
2
  * Driver Details Page
3
3
  */
4
4
 
5
+ import { useState } from 'react';
5
6
  import { useParams } from 'react-router-dom';
6
- import { Users, ArrowLeft, Star, DollarSign, Clock, FileText, Edit, MapPin } from 'lucide-react';
7
+ import {
8
+ Users,
9
+ ArrowLeft,
10
+ Star,
11
+ DollarSign,
12
+ Clock,
13
+ FileText,
14
+ Edit,
15
+ MapPin,
16
+ Save,
17
+ } from 'lucide-react';
7
18
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
8
19
  import { StatusBadge } from '../components/StatusBadge';
9
20
  import {
@@ -14,12 +25,157 @@ import {
14
25
  Button,
15
26
  Badge,
16
27
  Progress,
28
+ Input,
29
+ Label,
17
30
  } from '../components/ui';
18
31
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
19
- import { useDriver } from '../hooks/useFleet';
32
+ import { useDriver, useFleet } from '../hooks/useFleet';
20
33
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
21
34
  import { formatCurrency, formatDate } from '../utils/formatters';
22
35
  import { joinPath } from '../utils/navigation';
36
+ import { FormError } from '../components/FormError';
37
+
38
+ function NewDriverForm() {
39
+ const { basePath, navigate } = useMoveTheWheels();
40
+ const { createDriver } = useFleet();
41
+ const [form, setForm] = useState({
42
+ employeeId: '',
43
+ firstName: '',
44
+ lastName: '',
45
+ email: '',
46
+ phone: '',
47
+ licenseNumber: '',
48
+ });
49
+ const [submitting, setSubmitting] = useState(false);
50
+ const [error, setError] = useState<string | null>(null);
51
+
52
+ const goBack = () => navigate?.(joinPath(basePath, 'fleet/drivers'));
53
+
54
+ const handleSubmit = async (e: React.FormEvent) => {
55
+ e.preventDefault();
56
+ if (!form.employeeId.trim() || !form.firstName.trim() || !form.lastName.trim()) {
57
+ setError('Employee ID, first name and last name are required.');
58
+ return;
59
+ }
60
+ setSubmitting(true);
61
+ setError(null);
62
+ try {
63
+ const created = await createDriver({
64
+ employeeId: form.employeeId.trim(),
65
+ firstName: form.firstName.trim(),
66
+ lastName: form.lastName.trim(),
67
+ email: form.email.trim() || undefined,
68
+ phone: form.phone.trim() || undefined,
69
+ licenseNumber: form.licenseNumber.trim() || undefined,
70
+ });
71
+ navigate?.(joinPath(basePath, `fleet/drivers/${created.id}`));
72
+ } catch (err) {
73
+ console.error('Failed to create driver:', err);
74
+ setError(err instanceof Error ? err.message : 'Failed to create driver. Please try again.');
75
+ setSubmitting(false);
76
+ }
77
+ };
78
+
79
+ return (
80
+ <PageLayout
81
+ title="Add Driver"
82
+ subtitle="Add a driver to your workforce"
83
+ icon={<Users size={24} />}
84
+ headerActions={
85
+ <div className="flex gap-2">
86
+ <Button variant="outline" onClick={goBack} disabled={submitting}>
87
+ <ArrowLeft size={16} /> Cancel
88
+ </Button>
89
+ <Button onClick={handleSubmit} disabled={submitting}>
90
+ <Save size={16} /> {submitting ? 'Saving...' : 'Add Driver'}
91
+ </Button>
92
+ </div>
93
+ }
94
+ >
95
+ <PagePurpose icon={<Users size={18} />}>
96
+ Add a driver so they can be assigned to orders and routes. An employee ID and name get them on
97
+ the roster — add contact details and license number to keep the record complete.
98
+ </PagePurpose>
99
+
100
+ <form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
101
+ <FormError message={error} />
102
+ <EmphasisPanel>
103
+ <h2 className="text-base font-semibold text-text-primary mb-4">Driver Details</h2>
104
+ <div className="space-y-4">
105
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
106
+ <div className="space-y-2">
107
+ <Label htmlFor="employeeId">
108
+ Employee ID <span className="text-status-error-text">*</span>
109
+ </Label>
110
+ <Input
111
+ id="employeeId"
112
+ value={form.employeeId}
113
+ onChange={(e) => setForm((prev) => ({ ...prev, employeeId: e.target.value }))}
114
+ placeholder="EMP-001"
115
+ required
116
+ />
117
+ </div>
118
+ <div className="space-y-2">
119
+ <Label htmlFor="firstName">
120
+ First Name <span className="text-status-error-text">*</span>
121
+ </Label>
122
+ <Input
123
+ id="firstName"
124
+ value={form.firstName}
125
+ onChange={(e) => setForm((prev) => ({ ...prev, firstName: e.target.value }))}
126
+ placeholder="Jane"
127
+ required
128
+ />
129
+ </div>
130
+ <div className="space-y-2">
131
+ <Label htmlFor="lastName">
132
+ Last Name <span className="text-status-error-text">*</span>
133
+ </Label>
134
+ <Input
135
+ id="lastName"
136
+ value={form.lastName}
137
+ onChange={(e) => setForm((prev) => ({ ...prev, lastName: e.target.value }))}
138
+ placeholder="Doe"
139
+ required
140
+ />
141
+ </div>
142
+ </div>
143
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
144
+ <div className="space-y-2">
145
+ <Label htmlFor="email">Email</Label>
146
+ <Input
147
+ id="email"
148
+ type="email"
149
+ value={form.email}
150
+ onChange={(e) => setForm((prev) => ({ ...prev, email: e.target.value }))}
151
+ placeholder="jane.doe@example.com"
152
+ />
153
+ </div>
154
+ <div className="space-y-2">
155
+ <Label htmlFor="phone">Phone</Label>
156
+ <Input
157
+ id="phone"
158
+ value={form.phone}
159
+ onChange={(e) => setForm((prev) => ({ ...prev, phone: e.target.value }))}
160
+ placeholder="+1-555-0100"
161
+ />
162
+ </div>
163
+ <div className="space-y-2">
164
+ <Label htmlFor="licenseNumber">License Number</Label>
165
+ <Input
166
+ id="licenseNumber"
167
+ value={form.licenseNumber}
168
+ onChange={(e) => setForm((prev) => ({ ...prev, licenseNumber: e.target.value }))}
169
+ placeholder="D1234567"
170
+ />
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </EmphasisPanel>
175
+ </form>
176
+ </PageLayout>
177
+ );
178
+ }
23
179
 
24
180
  export default function DriverDetailsPage() {
25
181
  const { driverId } = useParams<{ driverId: string }>();
@@ -29,6 +185,7 @@ export default function DriverDetailsPage() {
29
185
  const goBack = () => navigate?.(joinPath(basePath, 'fleet/drivers'));
30
186
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
31
187
 
188
+ if (!driverId) return <NewDriverForm />;
32
189
  if (isLoading) return <LoadingState message="Loading driver details..." />;
33
190
  if (error || !driver) return <ErrorState title="Driver not found" onRetry={refetch} />;
34
191
 
@@ -92,10 +92,14 @@ export default function DriversPage() {
92
92
  icon={<Users size={24} />}
93
93
  headerActions={
94
94
  <CTAOverflowMenu
95
- primary={{ label: 'Add Driver', icon: <Plus size={16} />, onSelect: () => {} }}
95
+ primary={{
96
+ label: 'Add Driver',
97
+ icon: <Plus size={16} />,
98
+ onSelect: () => goTo('fleet/drivers/new'),
99
+ }}
96
100
  actions={[
97
101
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
98
- { label: 'Export', icon: <Download size={16} />, onSelect: () => {} },
102
+ { label: 'Export', icon: <Download size={16} />, onSelect: () => goTo('import-export') },
99
103
  ]}
100
104
  />
101
105
  }
@@ -42,7 +42,7 @@ export default function InventoryPage() {
42
42
  primary={{
43
43
  label: t('movethewheels.inventory.actions.addProduct', 'Add Product'),
44
44
  icon: <Plus size={16} />,
45
- onSelect: () => {},
45
+ onSelect: () => goTo('inventory/products/new'),
46
46
  }}
47
47
  actions={[
48
48
  {