@burdenoff/microfe-movethewheels 2026.703.8 → 2026.704.1
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/{CreateOrderPage-V8aXfcpN.js → CreateOrderPage-CXbhJLLz.js} +163 -138
- package/dist/{DashboardPage-DiD-cWwD.js → DashboardPage-BoswH3ec.js} +2 -2
- package/dist/DriverDetailsPage-CPO0G8Ki.js +518 -0
- package/dist/{DriversPage-DW-FGPFW.js → DriversPage-6Z61dpDI.js} +1 -1
- package/dist/{FleetPage-P9k4zZqP.js → FleetPage-qWRj7WFO.js} +1 -1
- package/dist/{InventoryPage-BWikjZMW.js → InventoryPage-DbR4Z--d.js} +1 -1
- package/dist/{LiveTrackingPage-UQBWlthJ.js → LiveTrackingPage-5U68u3oA.js} +1 -1
- package/dist/{OrderDetailsPage-2J4ua0wd.js → OrderDetailsPage-DRYifXm9.js} +1 -1
- package/dist/ProductDetailsPage-CWgcRqGR.js +375 -0
- package/dist/{ProductsPage-Bn1U2jTj.js → ProductsPage-CokwDOR2.js} +1 -1
- package/dist/{RouteDetailsPage-CgSY1CTd.js → RouteDetailsPage-BHQbFgI9.js} +242 -210
- package/dist/{SettingsPage-2a05Sq2k.js → SettingsPage-_6SIlcBX.js} +1 -1
- package/dist/{TrackingPage-SnjW6qeD.js → TrackingPage-Cg0tYGJe.js} +1 -1
- package/dist/{VehicleDetailsPage-CvRMJ2zC.js → VehicleDetailsPage-BbZlCFPo.js} +159 -135
- package/dist/{VehiclesPage-qxf-1C1B.js → VehiclesPage-BfBEyXGs.js} +1 -1
- package/dist/WarehouseDetailsPage-DxNhiaoI.js +515 -0
- package/dist/{WarehousesPage-44Wj7AGT.js → WarehousesPage-M3NaBT0z.js} +1 -1
- package/dist/index.js +25 -5
- package/dist/{useFleet-BXSocBD_.js → useFleet-BfA_-VUW.js} +100 -71
- package/dist/{useInventory-DmZficcp.js → useInventory-BO7TSgq_.js} +98 -72
- package/dist/{useSettings-CteNasBE.js → useSettings-BNNzQs_B.js} +1 -1
- package/dist/{useTracking-B7ttexRe.js → useTracking-7nd_XOSL.js} +1 -1
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +5 -0
- package/src/movethewheels/hooks/useFleet.ts +113 -0
- package/src/movethewheels/hooks/useInventory.ts +86 -0
- package/src/movethewheels/hooks/useSettings.ts +8 -1
- package/src/movethewheels/pages/CreateOrderPage.tsx +48 -6
- package/src/movethewheels/pages/DriverDetailsPage.tsx +56 -18
- package/src/movethewheels/pages/ProductDetailsPage.tsx +51 -15
- package/src/movethewheels/pages/RouteDetailsPage.tsx +51 -7
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +51 -15
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +46 -10
- package/src/movethewheels/types/index.ts +3 -0
- package/dist/DriverDetailsPage-DjaXaATQ.js +0 -490
- package/dist/ProductDetailsPage-BSjWLrty.js +0 -350
- package/dist/WarehouseDetailsPage-D0Wyyj1Y.js +0 -493
|
@@ -47,6 +47,8 @@ interface UseInventoryResult {
|
|
|
47
47
|
) => Promise<void>;
|
|
48
48
|
createProduct: (input: CreateProductInput) => Promise<Product>;
|
|
49
49
|
createWarehouse: (input: CreateWarehouseInput) => Promise<Warehouse>;
|
|
50
|
+
updateProduct: (id: string, input: CreateProductInput) => Promise<Product>;
|
|
51
|
+
updateWarehouse: (id: string, input: CreateWarehouseInput) => Promise<Warehouse>;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
interface ApiInventoryItem {
|
|
@@ -212,6 +214,27 @@ const CREATE_PRODUCT_MUTATION = `
|
|
|
212
214
|
}
|
|
213
215
|
`;
|
|
214
216
|
|
|
217
|
+
const UPDATE_PRODUCT_MUTATION = `
|
|
218
|
+
mutation UpdateProduct($id: ID!, $input: UpdateProductInput!) {
|
|
219
|
+
updateProduct(id: $id, input: $input) {
|
|
220
|
+
id
|
|
221
|
+
sku
|
|
222
|
+
name
|
|
223
|
+
description
|
|
224
|
+
category
|
|
225
|
+
brand
|
|
226
|
+
dimensions
|
|
227
|
+
weight
|
|
228
|
+
value
|
|
229
|
+
hazardous
|
|
230
|
+
fragile
|
|
231
|
+
temperatureControlled
|
|
232
|
+
createdAt
|
|
233
|
+
updatedAt
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
237
|
+
|
|
215
238
|
const CREATE_WAREHOUSE_MUTATION = `
|
|
216
239
|
mutation CreateWarehouse($input: CreateWarehouseInput!) {
|
|
217
240
|
createWarehouse(input: $input) {
|
|
@@ -227,6 +250,21 @@ const CREATE_WAREHOUSE_MUTATION = `
|
|
|
227
250
|
}
|
|
228
251
|
`;
|
|
229
252
|
|
|
253
|
+
const UPDATE_WAREHOUSE_MUTATION = `
|
|
254
|
+
mutation UpdateWarehouse($id: ID!, $input: UpdateWarehouseInput!) {
|
|
255
|
+
updateWarehouse(id: $id, input: $input) {
|
|
256
|
+
id
|
|
257
|
+
name
|
|
258
|
+
code
|
|
259
|
+
type
|
|
260
|
+
address
|
|
261
|
+
totalCapacity
|
|
262
|
+
usedCapacity
|
|
263
|
+
createdAt
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
`;
|
|
267
|
+
|
|
230
268
|
const ADJUST_INVENTORY_MUTATION = `
|
|
231
269
|
mutation AdjustInventoryStock($input: AdjustInventoryStockInput!) {
|
|
232
270
|
adjustInventoryStock(input: $input) {
|
|
@@ -519,6 +557,52 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
519
557
|
[graphQLOptions]
|
|
520
558
|
);
|
|
521
559
|
|
|
560
|
+
const updateProduct = useCallback(
|
|
561
|
+
async (id: string, input: CreateProductInput): Promise<Product> => {
|
|
562
|
+
const data = await executeGraphQL<{ updateProduct: ApiProduct }>(
|
|
563
|
+
graphQLOptions,
|
|
564
|
+
UPDATE_PRODUCT_MUTATION,
|
|
565
|
+
{
|
|
566
|
+
id,
|
|
567
|
+
input: {
|
|
568
|
+
name: input.name,
|
|
569
|
+
sku: input.sku,
|
|
570
|
+
category: input.category,
|
|
571
|
+
brand: input.brand,
|
|
572
|
+
weight: input.weight,
|
|
573
|
+
},
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
const updated = mapProduct(data.updateProduct);
|
|
577
|
+
setProducts((previous) => previous.map((item) => (item.id === id ? updated : item)));
|
|
578
|
+
return updated;
|
|
579
|
+
},
|
|
580
|
+
[graphQLOptions]
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const updateWarehouse = useCallback(
|
|
584
|
+
async (id: string, input: CreateWarehouseInput): Promise<Warehouse> => {
|
|
585
|
+
const data = await executeGraphQL<{ updateWarehouse: ApiWarehouse }>(
|
|
586
|
+
graphQLOptions,
|
|
587
|
+
UPDATE_WAREHOUSE_MUTATION,
|
|
588
|
+
{
|
|
589
|
+
id,
|
|
590
|
+
input: {
|
|
591
|
+
code: input.code,
|
|
592
|
+
name: input.name,
|
|
593
|
+
type: input.type,
|
|
594
|
+
address: input.address,
|
|
595
|
+
totalCapacity: input.totalCapacity,
|
|
596
|
+
},
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
const updated = mapWarehouse(data.updateWarehouse);
|
|
600
|
+
setWarehouses((previous) => previous.map((item) => (item.id === id ? updated : item)));
|
|
601
|
+
return updated;
|
|
602
|
+
},
|
|
603
|
+
[graphQLOptions]
|
|
604
|
+
);
|
|
605
|
+
|
|
522
606
|
return {
|
|
523
607
|
inventory,
|
|
524
608
|
products,
|
|
@@ -531,6 +615,8 @@ export function useInventory(options: UseInventoryOptions = {}): UseInventoryRes
|
|
|
531
615
|
transferStock,
|
|
532
616
|
createProduct,
|
|
533
617
|
createWarehouse,
|
|
618
|
+
updateProduct,
|
|
619
|
+
updateWarehouse,
|
|
534
620
|
};
|
|
535
621
|
}
|
|
536
622
|
|
|
@@ -108,9 +108,16 @@ export function useModuleSettings(): UseModuleSettingsResult {
|
|
|
108
108
|
[graphQLOptions]
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
+
// Stabilize the preferences reference. Returning `settings?.preferences ?? {}`
|
|
112
|
+
// inline minted a fresh `{}` on every render while settings was null, which
|
|
113
|
+
// made SettingsPage's `useMemo(toForm, [preferences])` recompute every render
|
|
114
|
+
// and its `useEffect(setForm, [loadedForm])` fire in a loop → "Maximum update
|
|
115
|
+
// depth exceeded" → the page rendered blank behind the shell's error boundary.
|
|
116
|
+
const preferences = useMemo<ModulePreferences>(() => settings?.preferences ?? {}, [settings]);
|
|
117
|
+
|
|
111
118
|
return {
|
|
112
119
|
settings,
|
|
113
|
-
preferences
|
|
120
|
+
preferences,
|
|
114
121
|
isLoading,
|
|
115
122
|
isSaving,
|
|
116
123
|
error,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Form for creating or editing orders.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useState } from 'react';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
8
|
import { useParams } from 'react-router-dom';
|
|
9
9
|
import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin } from 'lucide-react';
|
|
10
10
|
import { PageLayout, EmptyState } from '../components/PageLayout';
|
|
@@ -35,7 +35,7 @@ export default function CreateOrderPage() {
|
|
|
35
35
|
const { orderId } = useParams<{ orderId: string }>();
|
|
36
36
|
const { basePath, navigate } = useMoveTheWheels();
|
|
37
37
|
const { order } = useOrder(orderId);
|
|
38
|
-
const { createOrder } = useOrders();
|
|
38
|
+
const { createOrder, updateOrder } = useOrders();
|
|
39
39
|
const { customers } = useCustomers({ limit: 100 });
|
|
40
40
|
|
|
41
41
|
const isEditing = !!orderId && !!order;
|
|
@@ -65,6 +65,36 @@ export default function CreateOrderPage() {
|
|
|
65
65
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
66
66
|
const [formError, setFormError] = useState<string | null>(null);
|
|
67
67
|
|
|
68
|
+
// Prefill the form from the loaded order when editing. `order` arrives
|
|
69
|
+
// asynchronously after the initial render, so the useState initializers alone
|
|
70
|
+
// leave the fields blank until this syncs them.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!order) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setFormData({
|
|
76
|
+
customerId: order.customerId || '',
|
|
77
|
+
orderType: order.orderType || 'b2c',
|
|
78
|
+
priority: order.priority || 'normal',
|
|
79
|
+
pickupAddress: {
|
|
80
|
+
street: order.pickupAddress.street || '',
|
|
81
|
+
city: order.pickupAddress.city || '',
|
|
82
|
+
state: order.pickupAddress.state || '',
|
|
83
|
+
postalCode: order.pickupAddress.postalCode || '',
|
|
84
|
+
country: order.pickupAddress.country || 'USA',
|
|
85
|
+
},
|
|
86
|
+
deliveryAddress: {
|
|
87
|
+
street: order.deliveryAddress.street || '',
|
|
88
|
+
city: order.deliveryAddress.city || '',
|
|
89
|
+
state: order.deliveryAddress.state || '',
|
|
90
|
+
postalCode: order.deliveryAddress.postalCode || '',
|
|
91
|
+
country: order.deliveryAddress.country || 'USA',
|
|
92
|
+
},
|
|
93
|
+
items: order.items || [],
|
|
94
|
+
specialInstructions: order.specialInstructions || '',
|
|
95
|
+
});
|
|
96
|
+
}, [order]);
|
|
97
|
+
|
|
68
98
|
const goBack = () => {
|
|
69
99
|
if (navigate) {
|
|
70
100
|
navigate(joinPath(basePath, 'orders'));
|
|
@@ -106,7 +136,7 @@ export default function CreateOrderPage() {
|
|
|
106
136
|
|
|
107
137
|
setIsSubmitting(true);
|
|
108
138
|
try {
|
|
109
|
-
|
|
139
|
+
const input = {
|
|
110
140
|
customerId: formData.customerId,
|
|
111
141
|
customerInfo: {
|
|
112
142
|
name: selectedCustomer.name,
|
|
@@ -120,12 +150,24 @@ export default function CreateOrderPage() {
|
|
|
120
150
|
deliveryAddress: formData.deliveryAddress,
|
|
121
151
|
items: formData.items,
|
|
122
152
|
specialInstructions: formData.specialInstructions,
|
|
123
|
-
}
|
|
153
|
+
};
|
|
124
154
|
|
|
125
|
-
|
|
155
|
+
if (isEditing && orderId) {
|
|
156
|
+
await updateOrder(orderId, input);
|
|
157
|
+
if (navigate) {
|
|
158
|
+
navigate(joinPath(basePath, `orders/${orderId}`));
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
await createOrder(input);
|
|
162
|
+
goBack();
|
|
163
|
+
}
|
|
126
164
|
} catch (error) {
|
|
127
165
|
setFormError(
|
|
128
|
-
error instanceof Error
|
|
166
|
+
error instanceof Error
|
|
167
|
+
? error.message
|
|
168
|
+
: isEditing
|
|
169
|
+
? 'Could not update the order. Please try again.'
|
|
170
|
+
: 'Could not create the order. Please try again.'
|
|
129
171
|
);
|
|
130
172
|
} finally {
|
|
131
173
|
setIsSubmitting(false);
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Driver Details Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useState } from 'react';
|
|
6
|
-
import { useParams } from 'react-router-dom';
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import { useLocation, useParams } from 'react-router-dom';
|
|
7
7
|
import {
|
|
8
8
|
Users,
|
|
9
9
|
ArrowLeft,
|
|
@@ -36,9 +36,11 @@ import { joinPath } from '../utils/navigation';
|
|
|
36
36
|
import { FormError } from '../components/FormError';
|
|
37
37
|
import { DocumentsCard } from '../components/DocumentsCard';
|
|
38
38
|
|
|
39
|
-
function NewDriverForm() {
|
|
39
|
+
function NewDriverForm({ editId }: { editId?: string }) {
|
|
40
40
|
const { basePath, navigate } = useMoveTheWheels();
|
|
41
|
-
const { createDriver } = useFleet();
|
|
41
|
+
const { createDriver, updateDriver } = useFleet();
|
|
42
|
+
const { driver } = useDriver(editId);
|
|
43
|
+
const isEditing = !!editId;
|
|
42
44
|
const [form, setForm] = useState({
|
|
43
45
|
employeeId: '',
|
|
44
46
|
firstName: '',
|
|
@@ -50,6 +52,19 @@ function NewDriverForm() {
|
|
|
50
52
|
const [submitting, setSubmitting] = useState(false);
|
|
51
53
|
const [error, setError] = useState<string | null>(null);
|
|
52
54
|
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!editId || !driver) return;
|
|
57
|
+
const [firstName, ...rest] = driver.fullName.trim().split(/\s+/);
|
|
58
|
+
setForm({
|
|
59
|
+
employeeId: driver.employeeId || '',
|
|
60
|
+
firstName: firstName || '',
|
|
61
|
+
lastName: rest.join(' '),
|
|
62
|
+
email: driver.email || '',
|
|
63
|
+
phone: driver.phone || '',
|
|
64
|
+
licenseNumber: driver.licenseNumber || '',
|
|
65
|
+
});
|
|
66
|
+
}, [editId, driver]);
|
|
67
|
+
|
|
53
68
|
const goBack = () => navigate?.(joinPath(basePath, 'fleet/drivers'));
|
|
54
69
|
|
|
55
70
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
@@ -61,25 +76,37 @@ function NewDriverForm() {
|
|
|
61
76
|
setSubmitting(true);
|
|
62
77
|
setError(null);
|
|
63
78
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
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
|
+
});
|
|
88
|
+
navigate?.(joinPath(basePath, `fleet/drivers/${updated.id}`));
|
|
89
|
+
} 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
|
+
});
|
|
98
|
+
navigate?.(joinPath(basePath, `fleet/drivers/${created.id}`));
|
|
99
|
+
}
|
|
73
100
|
} catch (err) {
|
|
74
|
-
console.error('Failed to
|
|
75
|
-
setError(err instanceof Error ? err.message : 'Failed to
|
|
101
|
+
console.error('Failed to save driver:', err);
|
|
102
|
+
setError(err instanceof Error ? err.message : 'Failed to save driver. Please try again.');
|
|
76
103
|
setSubmitting(false);
|
|
77
104
|
}
|
|
78
105
|
};
|
|
79
106
|
|
|
80
107
|
return (
|
|
81
108
|
<PageLayout
|
|
82
|
-
title=
|
|
109
|
+
title={isEditing ? 'Edit Driver' : 'Add Driver'}
|
|
83
110
|
subtitle="Add a driver to your workforce"
|
|
84
111
|
icon={<Users size={24} />}
|
|
85
112
|
headerActions={
|
|
@@ -88,7 +115,14 @@ function NewDriverForm() {
|
|
|
88
115
|
<ArrowLeft size={16} /> Cancel
|
|
89
116
|
</Button>
|
|
90
117
|
<Button onClick={handleSubmit} disabled={submitting}>
|
|
91
|
-
<Save size={16} />
|
|
118
|
+
<Save size={16} />{' '}
|
|
119
|
+
{isEditing
|
|
120
|
+
? submitting
|
|
121
|
+
? 'Saving...'
|
|
122
|
+
: 'Save Changes'
|
|
123
|
+
: submitting
|
|
124
|
+
? 'Saving...'
|
|
125
|
+
: 'Add Driver'}
|
|
92
126
|
</Button>
|
|
93
127
|
</div>
|
|
94
128
|
}
|
|
@@ -180,6 +214,7 @@ function NewDriverForm() {
|
|
|
180
214
|
|
|
181
215
|
export default function DriverDetailsPage() {
|
|
182
216
|
const { driverId } = useParams<{ driverId: string }>();
|
|
217
|
+
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
183
218
|
const { basePath, navigate } = useMoveTheWheels();
|
|
184
219
|
const { driver, isLoading, error, refetch } = useDriver(driverId);
|
|
185
220
|
|
|
@@ -187,6 +222,7 @@ export default function DriverDetailsPage() {
|
|
|
187
222
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
188
223
|
|
|
189
224
|
if (!driverId) return <NewDriverForm />;
|
|
225
|
+
if (isEditRoute) return <NewDriverForm editId={driverId} />;
|
|
190
226
|
if (isLoading) return <LoadingState message="Loading driver details..." />;
|
|
191
227
|
if (error || !driver) return <ErrorState title="Driver not found" onRetry={refetch} />;
|
|
192
228
|
|
|
@@ -200,7 +236,9 @@ export default function DriverDetailsPage() {
|
|
|
200
236
|
<Button variant="outline" onClick={goBack}>
|
|
201
237
|
<ArrowLeft size={16} /> Back
|
|
202
238
|
</Button>
|
|
203
|
-
<Button
|
|
239
|
+
<Button
|
|
240
|
+
onClick={() => navigate?.(joinPath(basePath, 'fleet/drivers/' + driverId + '/edit'))}
|
|
241
|
+
>
|
|
204
242
|
<Edit size={16} /> Edit
|
|
205
243
|
</Button>
|
|
206
244
|
</div>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Product Details Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useState } from 'react';
|
|
6
|
-
import { useParams } from 'react-router-dom';
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import { useLocation, useParams } from 'react-router-dom';
|
|
7
7
|
import { Package, ArrowLeft, Edit, Box, DollarSign, Tag, Save } from 'lucide-react';
|
|
8
8
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
9
9
|
import {
|
|
@@ -23,9 +23,11 @@ import { formatCurrency } from '../utils/formatters';
|
|
|
23
23
|
import { joinPath } from '../utils/navigation';
|
|
24
24
|
import { FormError } from '../components/FormError';
|
|
25
25
|
|
|
26
|
-
function NewProductForm() {
|
|
26
|
+
function NewProductForm({ editId }: { editId?: string }) {
|
|
27
27
|
const { basePath, navigate } = useMoveTheWheels();
|
|
28
|
-
const { createProduct } = useInventory();
|
|
28
|
+
const { createProduct, updateProduct } = useInventory();
|
|
29
|
+
const { product } = useProduct(editId);
|
|
30
|
+
const isEditing = !!editId;
|
|
29
31
|
const [form, setForm] = useState({
|
|
30
32
|
name: '',
|
|
31
33
|
sku: '',
|
|
@@ -36,6 +38,18 @@ function NewProductForm() {
|
|
|
36
38
|
const [submitting, setSubmitting] = useState(false);
|
|
37
39
|
const [error, setError] = useState<string | null>(null);
|
|
38
40
|
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (editId && product) {
|
|
43
|
+
setForm({
|
|
44
|
+
name: product.name ?? '',
|
|
45
|
+
sku: product.sku ?? '',
|
|
46
|
+
category: product.category ?? '',
|
|
47
|
+
brand: product.brand ?? '',
|
|
48
|
+
weight: product.weight != null ? String(product.weight) : '',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}, [editId, product]);
|
|
52
|
+
|
|
39
53
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/products'));
|
|
40
54
|
|
|
41
55
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
@@ -48,14 +62,25 @@ function NewProductForm() {
|
|
|
48
62
|
setError(null);
|
|
49
63
|
try {
|
|
50
64
|
const weight = form.weight.trim() ? Number(form.weight) : undefined;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
if (isEditing && editId) {
|
|
66
|
+
const updated = await updateProduct(editId, {
|
|
67
|
+
name: form.name.trim(),
|
|
68
|
+
sku: form.sku.trim(),
|
|
69
|
+
category: form.category.trim() || undefined,
|
|
70
|
+
brand: form.brand.trim() || undefined,
|
|
71
|
+
weight: weight !== undefined && !Number.isNaN(weight) ? weight : undefined,
|
|
72
|
+
});
|
|
73
|
+
navigate?.(joinPath(basePath, `inventory/products/${updated.id}`));
|
|
74
|
+
} else {
|
|
75
|
+
const created = await createProduct({
|
|
76
|
+
name: form.name.trim(),
|
|
77
|
+
sku: form.sku.trim(),
|
|
78
|
+
category: form.category.trim() || undefined,
|
|
79
|
+
brand: form.brand.trim() || undefined,
|
|
80
|
+
weight: weight !== undefined && !Number.isNaN(weight) ? weight : undefined,
|
|
81
|
+
});
|
|
82
|
+
navigate?.(joinPath(basePath, `inventory/products/${created.id}`));
|
|
83
|
+
}
|
|
59
84
|
} catch (err) {
|
|
60
85
|
console.error('Failed to create product:', err);
|
|
61
86
|
setError(err instanceof Error ? err.message : 'Failed to create product. Please try again.');
|
|
@@ -65,7 +90,7 @@ function NewProductForm() {
|
|
|
65
90
|
|
|
66
91
|
return (
|
|
67
92
|
<PageLayout
|
|
68
|
-
title=
|
|
93
|
+
title={isEditing ? 'Edit Product' : 'Add Product'}
|
|
69
94
|
subtitle="Add an item to your catalog"
|
|
70
95
|
icon={<Package size={24} />}
|
|
71
96
|
headerActions={
|
|
@@ -74,7 +99,14 @@ function NewProductForm() {
|
|
|
74
99
|
<ArrowLeft size={16} /> Cancel
|
|
75
100
|
</Button>
|
|
76
101
|
<Button onClick={handleSubmit} disabled={submitting}>
|
|
77
|
-
<Save size={16} />
|
|
102
|
+
<Save size={16} />{' '}
|
|
103
|
+
{isEditing
|
|
104
|
+
? submitting
|
|
105
|
+
? 'Saving...'
|
|
106
|
+
: 'Save Changes'
|
|
107
|
+
: submitting
|
|
108
|
+
? 'Saving...'
|
|
109
|
+
: 'Add Product'}
|
|
78
110
|
</Button>
|
|
79
111
|
</div>
|
|
80
112
|
}
|
|
@@ -158,11 +190,13 @@ export default function ProductDetailsPage() {
|
|
|
158
190
|
const { productId } = useParams<{ productId: string }>();
|
|
159
191
|
const { basePath, navigate } = useMoveTheWheels();
|
|
160
192
|
const { product, isLoading, error, refetch } = useProduct(productId);
|
|
193
|
+
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
161
194
|
|
|
162
195
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/products'));
|
|
163
196
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
164
197
|
|
|
165
198
|
if (!productId) return <NewProductForm />;
|
|
199
|
+
if (isEditRoute) return <NewProductForm editId={productId} />;
|
|
166
200
|
if (isLoading) return <LoadingState message="Loading product details..." />;
|
|
167
201
|
if (error || !product) return <ErrorState title="Product not found" onRetry={refetch} />;
|
|
168
202
|
|
|
@@ -176,7 +210,9 @@ export default function ProductDetailsPage() {
|
|
|
176
210
|
<Button variant="outline" onClick={goBack}>
|
|
177
211
|
<ArrowLeft size={16} /> Back
|
|
178
212
|
</Button>
|
|
179
|
-
<Button
|
|
213
|
+
<Button
|
|
214
|
+
onClick={() => navigate?.(joinPath(basePath, `inventory/products/${productId}/edit`))}
|
|
215
|
+
>
|
|
180
216
|
<Edit size={16} /> Edit
|
|
181
217
|
</Button>
|
|
182
218
|
</div>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Route Details Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useState } from 'react';
|
|
6
|
-
import { useParams } from 'react-router-dom';
|
|
5
|
+
import { useEffect, useState } from 'react';
|
|
6
|
+
import { useParams, useLocation } from 'react-router-dom';
|
|
7
7
|
import {
|
|
8
8
|
Navigation,
|
|
9
9
|
ArrowLeft,
|
|
@@ -56,10 +56,12 @@ const ROUTE_STATUS_OPTIONS: { value: RouteType['status']; label: string }[] = [
|
|
|
56
56
|
{ value: 'cancelled', label: 'Cancelled' },
|
|
57
57
|
];
|
|
58
58
|
|
|
59
|
-
function NewRouteForm() {
|
|
59
|
+
function NewRouteForm({ editId }: { editId?: string }) {
|
|
60
60
|
const { basePath, navigate } = useMoveTheWheels();
|
|
61
|
-
const { createRoute } = useRoutes();
|
|
61
|
+
const { createRoute, updateRoute } = useRoutes();
|
|
62
|
+
const { route } = useRoute(editId);
|
|
62
63
|
const { vehicles, drivers } = useFleet();
|
|
64
|
+
const isEditing = !!editId;
|
|
63
65
|
const [form, setForm] = useState<{
|
|
64
66
|
name: string;
|
|
65
67
|
status: RouteType['status'];
|
|
@@ -76,6 +78,24 @@ function NewRouteForm() {
|
|
|
76
78
|
const [submitting, setSubmitting] = useState(false);
|
|
77
79
|
const [error, setError] = useState<string | null>(null);
|
|
78
80
|
|
|
81
|
+
// When editing, prefill the form from the loaded route once it resolves.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!editId || !route) return;
|
|
84
|
+
setForm({
|
|
85
|
+
name: route.name || '',
|
|
86
|
+
status: route.status,
|
|
87
|
+
vehicleId: route.vehicleId || '',
|
|
88
|
+
driverId: route.driverId || '',
|
|
89
|
+
stops:
|
|
90
|
+
route.stops.length > 0
|
|
91
|
+
? route.stops.map((stop) => ({
|
|
92
|
+
address: formatAddress(stop.address) || stop.address.street || '',
|
|
93
|
+
type: stop.type,
|
|
94
|
+
}))
|
|
95
|
+
: [{ address: '', type: 'delivery' }],
|
|
96
|
+
});
|
|
97
|
+
}, [editId, route]);
|
|
98
|
+
|
|
79
99
|
const goBack = () => navigate?.(joinPath(basePath, 'routes'));
|
|
80
100
|
|
|
81
101
|
const addStop = () =>
|
|
@@ -135,6 +155,17 @@ function NewRouteForm() {
|
|
|
135
155
|
duration: 0,
|
|
136
156
|
status: 'pending',
|
|
137
157
|
}));
|
|
158
|
+
if (isEditing) {
|
|
159
|
+
const updated = await updateRoute(editId, {
|
|
160
|
+
name: form.name.trim(),
|
|
161
|
+
status: form.status,
|
|
162
|
+
vehicleId: form.vehicleId || undefined,
|
|
163
|
+
driverId: form.driverId || undefined,
|
|
164
|
+
stops,
|
|
165
|
+
});
|
|
166
|
+
navigate?.(joinPath(basePath, `routes/${updated.id}`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
138
169
|
const created = await createRoute({
|
|
139
170
|
name: form.name.trim(),
|
|
140
171
|
status: form.status,
|
|
@@ -152,7 +183,7 @@ function NewRouteForm() {
|
|
|
152
183
|
|
|
153
184
|
return (
|
|
154
185
|
<PageLayout
|
|
155
|
-
title=
|
|
186
|
+
title={isEditing ? 'Edit Route' : 'Create Route'}
|
|
156
187
|
subtitle="Plan a new delivery route"
|
|
157
188
|
icon={<Navigation size={24} />}
|
|
158
189
|
headerActions={
|
|
@@ -161,7 +192,14 @@ function NewRouteForm() {
|
|
|
161
192
|
<ArrowLeft size={16} /> Cancel
|
|
162
193
|
</Button>
|
|
163
194
|
<Button onClick={handleSubmit} disabled={submitting}>
|
|
164
|
-
<Save size={16} />
|
|
195
|
+
<Save size={16} />{' '}
|
|
196
|
+
{isEditing
|
|
197
|
+
? submitting
|
|
198
|
+
? 'Saving...'
|
|
199
|
+
: 'Save Changes'
|
|
200
|
+
: submitting
|
|
201
|
+
? 'Saving...'
|
|
202
|
+
: 'Create Route'}
|
|
165
203
|
</Button>
|
|
166
204
|
</div>
|
|
167
205
|
}
|
|
@@ -333,6 +371,7 @@ function NewRouteForm() {
|
|
|
333
371
|
|
|
334
372
|
export default function RouteDetailsPage() {
|
|
335
373
|
const { routeId } = useParams<{ routeId: string }>();
|
|
374
|
+
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
336
375
|
const { basePath, navigate } = useMoveTheWheels();
|
|
337
376
|
const { route, isLoading, error, refetch } = useRoute(routeId);
|
|
338
377
|
const { optimizeRoute } = useRoutes();
|
|
@@ -363,6 +402,11 @@ export default function RouteDetailsPage() {
|
|
|
363
402
|
return <NewRouteForm />;
|
|
364
403
|
}
|
|
365
404
|
|
|
405
|
+
// Handle edit route form
|
|
406
|
+
if (isEditRoute) {
|
|
407
|
+
return <NewRouteForm editId={routeId} />;
|
|
408
|
+
}
|
|
409
|
+
|
|
366
410
|
if (isLoading) return <LoadingState message="Loading route details..." />;
|
|
367
411
|
if (error || !route) return <ErrorState title="Route not found" onRetry={refetch} />;
|
|
368
412
|
|
|
@@ -397,7 +441,7 @@ export default function RouteDetailsPage() {
|
|
|
397
441
|
<Zap size={16} /> {optimizing ? 'Optimizing…' : 'Optimize'}
|
|
398
442
|
</Button>
|
|
399
443
|
)}
|
|
400
|
-
<Button>
|
|
444
|
+
<Button onClick={() => navigate?.(joinPath(basePath, `routes/${routeId}/edit`))}>
|
|
401
445
|
<Edit size={16} /> Edit
|
|
402
446
|
</Button>
|
|
403
447
|
</div>
|