@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.
- package/dist/{AIAssistantPage-C3ovZ0Fh.js → AIAssistantPage-C3cPsWt-.js} +1 -1
- package/dist/{AnalyticsPage-O8SpY-V4.js → AnalyticsPage-Dz6g2MvK.js} +6 -6
- package/dist/{CreateOrderPage-CB1_-_mN.js → CreateOrderPage-C5iwnjp4.js} +29 -58
- package/dist/CustomerDetailsPage-B6nwFyLO.js +406 -0
- package/dist/{CustomersPage-DRaLuHMC.js → CustomersPage-BKxMS1AV.js} +5 -5
- package/dist/{DashboardPage-DPjhNfKB.js → DashboardPage-BVjQi0EB.js} +11 -11
- package/dist/DriverDetailsPage-CN3WQ0B9.js +507 -0
- package/dist/{DriversPage-C2Bk0w4Q.js → DriversPage-CH9rjQC-.js} +6 -6
- package/dist/{FinancePage-lmp0Sdsw.js → FinancePage-DEDykSOP.js} +5 -5
- package/dist/{FleetPage-DZn5pIqd.js → FleetPage-DqkFi3s3.js} +5 -5
- package/dist/FormError-BlOjnXOq.js +15 -0
- package/dist/{ImportExportPage-BIUJZc7O.js → ImportExportPage-DKJRCezl.js} +1 -1
- package/dist/{InventoryPage-Cxh1zYJd.js → InventoryPage-e0ze63iX.js} +6 -6
- package/dist/{LiveTrackingPage-BY1yAkIt.js → LiveTrackingPage-DCX-K-LR.js} +5 -5
- package/dist/{MarketplacePage-RMuL17EA.js → MarketplacePage-CIQ97_zp.js} +1 -1
- package/dist/{OrderDetailsPage-C2Mf4KSy.js → OrderDetailsPage-DYiZ-9UK.js} +5 -5
- package/dist/{OrdersListPage-BrPBtnhp.js → OrdersListPage-Cn90P1Xx.js} +4 -4
- package/dist/ProductDetailsPage-C9uOiYLp.js +350 -0
- package/dist/{ProductsPage-I5DilIoY.js → ProductsPage-QJfHHpqB.js} +4 -4
- package/dist/{ReportsPage-BDXNmUng.js → ReportsPage-Zp53q3_8.js} +2 -2
- package/dist/RouteDetailsPage-DUStXSf9.js +531 -0
- package/dist/{RoutesPage-lqX9ylHP.js → RoutesPage-DMfoicM1.js} +3 -3
- package/dist/{SettingsPage-CQVIOaNs.js → SettingsPage-Bn-v0mbQ.js} +1 -1
- package/dist/{TrackingPage-BXrjz7V8.js → TrackingPage-W-jtdM3Y.js} +4 -4
- package/dist/VehicleDetailsPage-Bceo5oz3.js +410 -0
- package/dist/{VehiclesPage-6W39XT5W.js → VehiclesPage-ApKThYeG.js} +5 -5
- package/dist/WarehouseDetailsPage-L_6sWigM.js +493 -0
- package/dist/{WarehousesPage-CHvBqNaQ.js → WarehousesPage-DCbXXXWE.js} +3 -3
- package/dist/index.js +154 -138
- package/dist/trash-2-BKhar6Hs.js +32 -0
- package/dist/{useFleet-DiAC-IiH.js → useFleet-DKqXZ16m.js} +77 -56
- package/dist/{useInventory-DKoks37x.js → useInventory-DkyiVY_A.js} +73 -53
- package/dist/{useTracking-GJify4HN.js → useTracking-B-va1kO3.js} +2 -2
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
- package/src/movethewheels/components/FormError.tsx +25 -0
- package/src/movethewheels/hooks/useFleet.ts +107 -0
- package/src/movethewheels/hooks/useInventory.ts +107 -0
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +153 -20
- package/src/movethewheels/pages/CustomersPage.tsx +1 -1
- package/src/movethewheels/pages/DriverDetailsPage.tsx +159 -2
- package/src/movethewheels/pages/DriversPage.tsx +6 -2
- package/src/movethewheels/pages/InventoryPage.tsx +1 -1
- package/src/movethewheels/pages/OrdersListPage.tsx +1 -1
- package/src/movethewheels/pages/ProductDetailsPage.tsx +146 -3
- package/src/movethewheels/pages/ProductsPage.tsx +6 -2
- package/src/movethewheels/pages/RouteDetailsPage.tsx +285 -25
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +158 -2
- package/src/movethewheels/pages/VehiclesPage.tsx +6 -2
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +224 -2
- package/src/movethewheels/pages/WarehousesPage.tsx +5 -1
- package/dist/CustomerDetailsPage-BaeKEI1D.js +0 -263
- package/dist/DriverDetailsPage-CSSQTOzn.js +0 -328
- package/dist/ProductDetailsPage-CezzzZcj.js +0 -193
- package/dist/RouteDetailsPage-BcomL20M.js +0 -263
- package/dist/VehicleDetailsPage-DZjLcJgr.js +0 -218
- package/dist/WarehouseDetailsPage-C10pgRGW.js +0 -236
- /package/dist/{MetricCard-7aYgzZHK.js → MetricCard-BY0QrMD4.js} +0 -0
- /package/dist/{StatusBadge-NpTeHVlw.js → StatusBadge-BZurYHMy.js} +0 -0
- /package/dist/{formatters-DhKZ-eA1.js → formatters-CF2S2l1m.js} +0 -0
- /package/dist/{trending-up-B7W-GL6P.js → trending-up-Dohgd4t_.js} +0 -0
- /package/dist/{triangle-alert-BonHFFNz.js → triangle-alert-i-XQL9Rd.js} +0 -0
- /package/dist/{truck-DrlfSpRC.js → truck-C0v_hbLR.js} +0 -0
- /package/dist/{useAnalytics-CgdzUfY_.js → useAnalytics-DS8n0xgg.js} +0 -0
- /package/dist/{useCustomers-QrlozybF.js → useCustomers-BmTNGxiI.js} +0 -0
- /package/dist/{useImportExport-BLMnAKCC.js → useImportExport-DHr-SWSw.js} +0 -0
- /package/dist/{useOrders-D3UZVy8Y.js → useOrders-Cfk1GLiq.js} +0 -0
- /package/dist/{useReports-Puq4DCH9.js → useReports-CTqZr-AB.js} +0 -0
- /package/dist/{useRoutes-Do57LvRb.js → useRoutes-BQWO607N.js} +0 -0
- /package/dist/{user-CjSoudeu.js → user-jyFU3lXk.js} +0 -0
- /package/dist/{users-BAM4gHTn.js → users-DZ-ilS6B.js} +0 -0
- /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 {
|
|
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 {
|
|
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={{
|
|
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
|
}
|