@burdenoff/microfe-movethewheels 2026.703.7 → 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/{AIAssistantPage-B7FGmmKT.js → AIAssistantPage-MWJLAMN_.js} +5 -5
- package/dist/{AnalyticsPage-DXduj5KB.js → AnalyticsPage-900jpV2T.js} +9 -9
- package/dist/{CreateOrderPage-DuJJQ16b.js → CreateOrderPage-CXbhJLLz.js} +164 -139
- package/dist/{CustomerDetailsPage-C98SKw7Z.js → CustomerDetailsPage-CHzg75Q5.js} +16 -16
- package/dist/{CustomersPage-s8hK55JL.js → CustomersPage-CwtVspuq.js} +11 -11
- package/dist/{DashboardPage-BsEgcKEN.js → DashboardPage-BoswH3ec.js} +17 -17
- package/dist/{DataTable-Da7yHjM1.js → DataTable-CmdZd6iV.js} +7 -7
- package/dist/{DocumentsCard-CziaTJhP.js → DocumentsCard-y2ybnAlu.js} +4 -4
- package/dist/DriverDetailsPage-CPO0G8Ki.js +518 -0
- package/dist/{DriversPage-Duy_ITG6.js → DriversPage-6Z61dpDI.js} +11 -11
- package/dist/{FinancePage-BZtPTpmT.js → FinancePage-DYadOUzC.js} +9 -9
- package/dist/{FleetPage-DJfzoHbs.js → FleetPage-qWRj7WFO.js} +10 -10
- package/dist/{FormError-CWjGwOW8.js → FormError-D4T5FWZA.js} +1 -1
- package/dist/{ImportExportPage-DJ77GK_7.js → ImportExportPage-BhWGfbH3.js} +6 -6
- package/dist/{InventoryPage-Dpnwk_N3.js → InventoryPage-DbR4Z--d.js} +10 -10
- package/dist/{LiveTrackingPage-CLlvsP1B.js → LiveTrackingPage-5U68u3oA.js} +10 -10
- package/dist/{MarketplacePage-d99r-v6p.js → MarketplacePage-DMH0HNDH.js} +9 -9
- package/dist/{MetricCard-DA5ZqAVl.js → MetricCard-CqU5WsyW.js} +2 -2
- package/dist/{Notice-kkjV2rLR.js → Notice-BrXJ85dA.js} +2 -2
- package/dist/{OrderDetailsPage-BePtkyN0.js → OrderDetailsPage-DRYifXm9.js} +28 -24
- package/dist/{OrdersListPage-Csy_KScx.js → OrdersListPage-DvICveRv.js} +14 -14
- package/dist/{PageLayout-h_q9l8af.js → PageLayout-j9Zw3wBN.js} +579 -561
- package/dist/ProductDetailsPage-CWgcRqGR.js +375 -0
- package/dist/{ProductsPage-B4jWJ5bv.js → ProductsPage-CokwDOR2.js} +9 -9
- package/dist/{ReportsPage-ALRYfXtz.js → ReportsPage-MU8F9DQC.js} +9 -9
- package/dist/RouteDetailsPage-BHQbFgI9.js +642 -0
- package/dist/{RoutesPage-CQHCDInT.js → RoutesPage-Dqd5Don6.js} +7 -7
- package/dist/{SettingsPage-BYBRrlGe.js → SettingsPage-_6SIlcBX.js} +4 -4
- package/dist/{Sparkline-Q-6bJ24J.js → Sparkline-DTGdlxUH.js} +1 -1
- package/dist/{StatusBadge-1W2p8fEi.js → StatusBadge-BfiF-nNI.js} +1 -1
- package/dist/{TrackingPage-CFndTIyk.js → TrackingPage-Cg0tYGJe.js} +11 -11
- package/dist/VehicleDetailsPage-BbZlCFPo.js +417 -0
- package/dist/{VehiclesPage-ClkfFZxL.js → VehiclesPage-BfBEyXGs.js} +12 -12
- package/dist/WarehouseDetailsPage-DxNhiaoI.js +515 -0
- package/dist/{WarehousesPage-kNba6cr3.js → WarehousesPage-M3NaBT0z.js} +8 -8
- package/dist/{arrow-left-XOzFvBFD.js → arrow-left-1dVWv2dg.js} +1 -1
- package/dist/{arrow-right-CRrwlvFe.js → arrow-right-PUCNATnk.js} +1 -1
- package/dist/{arrow-up-ClFaLPly.js → arrow-up-VQuGMABN.js} +1 -1
- package/dist/{box-BX4WyCLv.js → box-DW1KNP8j.js} +1 -1
- package/dist/{boxes-R3nq5f0_.js → boxes-D6I00XqA.js} +1 -1
- package/dist/{chart-column-BgF8J9bx.js → chart-column-BwQ5H9ol.js} +1 -1
- package/dist/{charts-BS_OnWc6.js → charts-Dv_zxP7L.js} +2 -2
- package/dist/{chevron-left-D3hcLQ1g.js → chevron-left-DzNxoaBG.js} +1 -1
- package/dist/{chevron-right-BpfSrzQF.js → chevron-right-CKm3ZC1T.js} +1 -1
- package/dist/{circle-alert-CLqT8iCQ.js → circle-alert-CtBm6oIb.js} +1 -1
- package/dist/{circle-check-CWLAr_8f.js → circle-check-B_Y7FDyR.js} +1 -1
- package/dist/{circle-check-big-Dsuz8dJ2.js → circle-check-big-H5pLiMBW.js} +1 -1
- package/dist/{clock-DrZOQD8h.js → clock-B4-qjGuB.js} +1 -1
- package/dist/{dollar-sign-CJfKfYul.js → dollar-sign-CYGHy2oS.js} +1 -1
- package/dist/{download-C-yNvUiL.js → download-BJonq2ty.js} +1 -1
- package/dist/{file-text-m2SyQgBo.js → file-text-C-cZx58r.js} +1 -1
- package/dist/{filter-uA6govE9.js → filter-B_S6WOqr.js} +1 -1
- package/dist/{fuel-BroSIBo1.js → fuel-Cjt6IxKr.js} +1 -1
- package/dist/index.js +53 -33
- package/dist/{map-BzwQ__kD.js → map-CO0aWUae.js} +1 -1
- package/dist/{map-pin-BGbmEGn3.js → map-pin-BISLzLwb.js} +1 -1
- package/dist/{maps-DpIuSSmg.js → maps-CtfuCAmh.js} +1 -1
- package/dist/{navigation-CjKGLL9T.js → navigation-BFki5UJz.js} +1 -1
- package/dist/{package-D36cKFvu.js → package-UyaWiALC.js} +1 -1
- package/dist/{phone-mMZFHd1Z.js → phone-Bav_V7rR.js} +1 -1
- package/dist/{plus-DY5R-c1j.js → plus-Bk_G-lFC.js} +1 -1
- package/dist/{refresh-cw-DyOLYuDU.js → refresh-cw-C4-jecjy.js} +1 -1
- package/dist/{route-CuaZ1wU3.js → route-WpCP6F1x.js} +1 -1
- package/dist/{save-h9dzHwJE.js → save-CcfoIn2J.js} +1 -1
- package/dist/{search-stTGSec-.js → search-C3YRxW9e.js} +1 -1
- package/dist/{square-pen-DT5srCPD.js → square-pen-Dr1X-P2s.js} +1 -1
- package/dist/{star-gp4yO3PF.js → star-1Ixfuzai.js} +1 -1
- package/dist/{store-DJZDAvCY.js → store-BwU935v5.js} +1 -1
- package/dist/{trash-2-DwcjTxYE.js → trash-2-CC4KugEJ.js} +1 -1
- package/dist/{trending-up-2q2sPVsb.js → trending-up-1brglj1E.js} +1 -1
- package/dist/{triangle-alert-CU60xaOi.js → triangle-alert-Dh56Yu3-.js} +1 -1
- package/dist/{truck-ByZxqZhI.js → truck-DxhtLfkG.js} +1 -1
- package/dist/{upload-CWor1uj3.js → upload-CLf0JLXW.js} +1 -1
- package/dist/{useAssistant-uoc8wjr0.js → useAssistant-B9qBtRKm.js} +1 -1
- package/dist/{useCustomers-BPPhQXSH.js → useCustomers-BQ4AIEAT.js} +1 -1
- package/dist/{useFleet-CT_iW2V9.js → useFleet-BfA_-VUW.js} +101 -72
- package/dist/{useImportExport-BG0Cg_D9.js → useImportExport-7-O8noRJ.js} +1 -1
- package/dist/{useInventory-BtBGnOC2.js → useInventory-BO7TSgq_.js} +99 -73
- package/dist/{useOrders-BC-1mCc0.js → useOrders-B5FV0kcz.js} +1 -1
- package/dist/{useReports-Cx8blThR.js → useReports-hUXb8ryJ.js} +1 -1
- package/dist/{useRoutes-8fw8oO6y.js → useRoutes-C_ZCYJ_h.js} +1 -1
- package/dist/{useSettings-Do5WSUaI.js → useSettings-BNNzQs_B.js} +2 -2
- package/dist/{useTracking-Dogilo5C.js → useTracking-7nd_XOSL.js} +3 -3
- package/dist/{user-Dr_JS2FF.js → user-B1IjWJtn.js} +1 -1
- package/dist/{user-check-CBSiMH6I.js → user-check-Cx6fHHll.js} +1 -1
- package/dist/{users-BRV9-2nV.js → users-BKmsb79k.js} +1 -1
- package/dist/{warehouse-CmIAl0_w.js → warehouse-DE1irFyJ.js} +1 -1
- package/dist/{wrench-6U-BTSbg.js → wrench-DTJGFs17.js} +1 -1
- package/dist/{x-DX8sZrkd.js → x-CvsUGJy1.js} +1 -1
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +5 -0
- package/src/movethewheels/components/GlassPanel.tsx +1 -3
- package/src/movethewheels/components/MetricCard.tsx +16 -3
- package/src/movethewheels/components/charts/StatusDonut.tsx +2 -6
- package/src/movethewheels/components/charts/TrendChart.tsx +2 -6
- package/src/movethewheels/components/maps/LogisticsMap.tsx +1 -4
- package/src/movethewheels/components/maps/RouteMap.tsx +3 -5
- package/src/movethewheels/components/maps/TrackingMap.tsx +11 -2
- 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/CustomerDetailsPage.tsx +3 -2
- package/src/movethewheels/pages/DriverDetailsPage.tsx +58 -20
- package/src/movethewheels/pages/OrderDetailsPage.tsx +2 -1
- package/src/movethewheels/pages/ProductDetailsPage.tsx +51 -15
- package/src/movethewheels/pages/RouteDetailsPage.tsx +51 -7
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +53 -17
- package/src/movethewheels/pages/VehiclesPage.tsx +9 -1
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +49 -11
- package/src/movethewheels/types/index.ts +3 -0
- package/src/movethewheels/utils/graphqlClient.ts +51 -1
- package/dist/DriverDetailsPage-CBzAmM6s.js +0 -490
- package/dist/ProductDetailsPage-BMlRoR9E.js +0 -350
- package/dist/RouteDetailsPage-Btb1MSIS.js +0 -610
- package/dist/VehicleDetailsPage-DNLrMljV.js +0 -393
- package/dist/WarehouseDetailsPage-CAQFz7l_.js +0 -493
|
@@ -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>
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Vehicle 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 { Truck, ArrowLeft, MapPin, Fuel, Wrench, Edit, Save } from 'lucide-react';
|
|
8
8
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
9
9
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
@@ -41,9 +41,11 @@ const VEHICLE_TYPE_OPTIONS = [
|
|
|
41
41
|
{ value: 'DRONE', label: 'Drone' },
|
|
42
42
|
];
|
|
43
43
|
|
|
44
|
-
function NewVehicleForm() {
|
|
44
|
+
function NewVehicleForm({ editId }: { editId?: string }) {
|
|
45
45
|
const { basePath, navigate } = useMoveTheWheels();
|
|
46
|
-
const { createVehicle } = useFleet();
|
|
46
|
+
const { createVehicle, updateVehicle } = useFleet();
|
|
47
|
+
const { vehicle } = useVehicle(editId);
|
|
48
|
+
const isEditing = !!editId;
|
|
47
49
|
const [form, setForm] = useState({
|
|
48
50
|
vehicleNumber: '',
|
|
49
51
|
type: 'VAN',
|
|
@@ -54,6 +56,18 @@ function NewVehicleForm() {
|
|
|
54
56
|
const [submitting, setSubmitting] = useState(false);
|
|
55
57
|
const [error, setError] = useState<string | null>(null);
|
|
56
58
|
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (editId && vehicle) {
|
|
61
|
+
setForm({
|
|
62
|
+
vehicleNumber: vehicle.vehicleNumber ?? '',
|
|
63
|
+
type: vehicle.type ? vehicle.type.toUpperCase() : 'VAN',
|
|
64
|
+
make: vehicle.make ?? '',
|
|
65
|
+
model: vehicle.model ?? '',
|
|
66
|
+
licensePlate: vehicle.licensePlate ?? '',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}, [editId, vehicle]);
|
|
70
|
+
|
|
57
71
|
const goBack = () => navigate?.(joinPath(basePath, 'fleet/vehicles'));
|
|
58
72
|
|
|
59
73
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
@@ -65,14 +79,25 @@ function NewVehicleForm() {
|
|
|
65
79
|
setSubmitting(true);
|
|
66
80
|
setError(null);
|
|
67
81
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
if (editId) {
|
|
83
|
+
const updated = await updateVehicle(editId, {
|
|
84
|
+
vehicleNumber: form.vehicleNumber.trim(),
|
|
85
|
+
type: form.type,
|
|
86
|
+
make: form.make.trim() || undefined,
|
|
87
|
+
model: form.model.trim() || undefined,
|
|
88
|
+
licensePlate: form.licensePlate.trim() || undefined,
|
|
89
|
+
});
|
|
90
|
+
navigate?.(joinPath(basePath, `fleet/vehicles/${updated.id}`));
|
|
91
|
+
} else {
|
|
92
|
+
const created = await createVehicle({
|
|
93
|
+
vehicleNumber: form.vehicleNumber.trim(),
|
|
94
|
+
type: form.type,
|
|
95
|
+
make: form.make.trim() || undefined,
|
|
96
|
+
model: form.model.trim() || undefined,
|
|
97
|
+
licensePlate: form.licensePlate.trim() || undefined,
|
|
98
|
+
});
|
|
99
|
+
navigate?.(joinPath(basePath, `fleet/vehicles/${created.id}`));
|
|
100
|
+
}
|
|
76
101
|
} catch (err) {
|
|
77
102
|
console.error('Failed to create vehicle:', err);
|
|
78
103
|
setError(err instanceof Error ? err.message : 'Failed to create vehicle. Please try again.');
|
|
@@ -82,7 +107,7 @@ function NewVehicleForm() {
|
|
|
82
107
|
|
|
83
108
|
return (
|
|
84
109
|
<PageLayout
|
|
85
|
-
title=
|
|
110
|
+
title={isEditing ? 'Edit Vehicle' : 'Add Vehicle'}
|
|
86
111
|
subtitle="Register a new vehicle in your fleet"
|
|
87
112
|
icon={<Truck size={24} />}
|
|
88
113
|
headerActions={
|
|
@@ -91,14 +116,21 @@ function NewVehicleForm() {
|
|
|
91
116
|
<ArrowLeft size={16} /> Cancel
|
|
92
117
|
</Button>
|
|
93
118
|
<Button onClick={handleSubmit} disabled={submitting}>
|
|
94
|
-
<Save size={16} />
|
|
119
|
+
<Save size={16} />{' '}
|
|
120
|
+
{isEditing
|
|
121
|
+
? submitting
|
|
122
|
+
? 'Saving...'
|
|
123
|
+
: 'Save Changes'
|
|
124
|
+
: submitting
|
|
125
|
+
? 'Saving...'
|
|
126
|
+
: 'Add Vehicle'}
|
|
95
127
|
</Button>
|
|
96
128
|
</div>
|
|
97
129
|
}
|
|
98
130
|
>
|
|
99
131
|
<PagePurpose icon={<Truck size={18} />}>
|
|
100
|
-
Add a vehicle so it can be assigned to deliveries and routes. Give it a fleet number and
|
|
101
|
-
to start — make, model and plate help identify it on the road.
|
|
132
|
+
Add a vehicle so it can be assigned to deliveries and routes. Give it a fleet number and
|
|
133
|
+
type to start — make, model and plate help identify it on the road.
|
|
102
134
|
</PagePurpose>
|
|
103
135
|
|
|
104
136
|
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
|
@@ -180,11 +212,13 @@ export default function VehicleDetailsPage() {
|
|
|
180
212
|
const { vehicleId } = useParams<{ vehicleId: string }>();
|
|
181
213
|
const { basePath, navigate } = useMoveTheWheels();
|
|
182
214
|
const { vehicle, isLoading, error, refetch } = useVehicle(vehicleId);
|
|
215
|
+
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
183
216
|
|
|
184
217
|
const goBack = () => navigate?.(joinPath(basePath, 'fleet/vehicles'));
|
|
185
218
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
186
219
|
|
|
187
220
|
if (!vehicleId) return <NewVehicleForm />;
|
|
221
|
+
if (isEditRoute) return <NewVehicleForm editId={vehicleId} />;
|
|
188
222
|
if (isLoading) return <LoadingState message="Loading vehicle details..." />;
|
|
189
223
|
if (error || !vehicle) return <ErrorState title="Vehicle not found" onRetry={refetch} />;
|
|
190
224
|
|
|
@@ -198,7 +232,9 @@ export default function VehicleDetailsPage() {
|
|
|
198
232
|
<Button variant="outline" onClick={goBack}>
|
|
199
233
|
<ArrowLeft size={16} /> Back
|
|
200
234
|
</Button>
|
|
201
|
-
<Button
|
|
235
|
+
<Button
|
|
236
|
+
onClick={() => navigate?.(joinPath(basePath, 'fleet/vehicles/' + vehicleId + '/edit'))}
|
|
237
|
+
>
|
|
202
238
|
<Edit size={16} /> Edit
|
|
203
239
|
</Button>
|
|
204
240
|
</div>
|
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useState } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Truck,
|
|
8
|
+
Plus,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Download,
|
|
11
|
+
CircleCheck,
|
|
12
|
+
Route as RouteIcon,
|
|
13
|
+
Wrench,
|
|
14
|
+
} from 'lucide-react';
|
|
7
15
|
import { PageLayout } from '../components/PageLayout';
|
|
8
16
|
import { MetricCard, MetricGrid } from '../components/MetricCard';
|
|
9
17
|
import { DataTable, type Column } from '../components/DataTable';
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Warehouse 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 { Warehouse, ArrowLeft, Edit, MapPin, Clock, Users, Box, Save } from 'lucide-react';
|
|
8
8
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
9
9
|
import {
|
|
@@ -37,9 +37,11 @@ const WAREHOUSE_TYPE_OPTIONS = [
|
|
|
37
37
|
{ value: 'RETAIL', label: 'Retail' },
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
-
function NewWarehouseForm() {
|
|
40
|
+
function NewWarehouseForm({ editId }: { editId?: string }) {
|
|
41
41
|
const { basePath, navigate } = useMoveTheWheels();
|
|
42
|
-
const { createWarehouse } = useInventory();
|
|
42
|
+
const { createWarehouse, updateWarehouse } = useInventory();
|
|
43
|
+
const { warehouse } = useWarehouse(editId);
|
|
44
|
+
const isEditing = !!editId;
|
|
43
45
|
const [form, setForm] = useState({
|
|
44
46
|
code: '',
|
|
45
47
|
name: '',
|
|
@@ -54,6 +56,21 @@ function NewWarehouseForm() {
|
|
|
54
56
|
const [submitting, setSubmitting] = useState(false);
|
|
55
57
|
const [error, setError] = useState<string | null>(null);
|
|
56
58
|
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!editId || !warehouse) return;
|
|
61
|
+
setForm({
|
|
62
|
+
code: warehouse.code || '',
|
|
63
|
+
name: warehouse.name || '',
|
|
64
|
+
type: warehouse.type ? warehouse.type.toUpperCase() : 'DISTRIBUTION',
|
|
65
|
+
street: warehouse.address.street || '',
|
|
66
|
+
city: warehouse.address.city || '',
|
|
67
|
+
state: warehouse.address.state || '',
|
|
68
|
+
postalCode: warehouse.address.postalCode || '',
|
|
69
|
+
country: warehouse.address.country || 'USA',
|
|
70
|
+
totalCapacity: warehouse.capacity.totalSpace ? String(warehouse.capacity.totalSpace) : '',
|
|
71
|
+
});
|
|
72
|
+
}, [editId, warehouse]);
|
|
73
|
+
|
|
57
74
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/warehouses'));
|
|
58
75
|
|
|
59
76
|
const update = (field: keyof typeof form, value: string) =>
|
|
@@ -73,7 +90,7 @@ function NewWarehouseForm() {
|
|
|
73
90
|
setError(null);
|
|
74
91
|
try {
|
|
75
92
|
const totalCapacity = form.totalCapacity.trim() ? Number(form.totalCapacity) : undefined;
|
|
76
|
-
const
|
|
93
|
+
const warehouseInput = {
|
|
77
94
|
code: form.code.trim(),
|
|
78
95
|
name: form.name.trim(),
|
|
79
96
|
type: form.type,
|
|
@@ -86,18 +103,26 @@ function NewWarehouseForm() {
|
|
|
86
103
|
},
|
|
87
104
|
totalCapacity:
|
|
88
105
|
totalCapacity !== undefined && !Number.isNaN(totalCapacity) ? totalCapacity : undefined,
|
|
89
|
-
}
|
|
90
|
-
|
|
106
|
+
};
|
|
107
|
+
if (editId) {
|
|
108
|
+
await updateWarehouse(editId, warehouseInput);
|
|
109
|
+
navigate?.(joinPath(basePath, `inventory/warehouses/${editId}`));
|
|
110
|
+
} else {
|
|
111
|
+
const created = await createWarehouse(warehouseInput);
|
|
112
|
+
navigate?.(joinPath(basePath, `inventory/warehouses/${created.id}`));
|
|
113
|
+
}
|
|
91
114
|
} catch (err) {
|
|
92
115
|
console.error('Failed to create warehouse:', err);
|
|
93
|
-
setError(
|
|
116
|
+
setError(
|
|
117
|
+
err instanceof Error ? err.message : 'Failed to create warehouse. Please try again.'
|
|
118
|
+
);
|
|
94
119
|
setSubmitting(false);
|
|
95
120
|
}
|
|
96
121
|
};
|
|
97
122
|
|
|
98
123
|
return (
|
|
99
124
|
<PageLayout
|
|
100
|
-
title=
|
|
125
|
+
title={isEditing ? 'Edit Warehouse' : 'Add Warehouse'}
|
|
101
126
|
subtitle="Register a storage and fulfilment site"
|
|
102
127
|
icon={<Warehouse size={24} />}
|
|
103
128
|
headerActions={
|
|
@@ -106,7 +131,14 @@ function NewWarehouseForm() {
|
|
|
106
131
|
<ArrowLeft size={16} /> Cancel
|
|
107
132
|
</Button>
|
|
108
133
|
<Button onClick={handleSubmit} disabled={submitting}>
|
|
109
|
-
<Save size={16} />
|
|
134
|
+
<Save size={16} />{' '}
|
|
135
|
+
{isEditing
|
|
136
|
+
? submitting
|
|
137
|
+
? 'Saving...'
|
|
138
|
+
: 'Save Changes'
|
|
139
|
+
: submitting
|
|
140
|
+
? 'Saving...'
|
|
141
|
+
: 'Add Warehouse'}
|
|
110
142
|
</Button>
|
|
111
143
|
</div>
|
|
112
144
|
}
|
|
@@ -244,12 +276,14 @@ function NewWarehouseForm() {
|
|
|
244
276
|
export default function WarehouseDetailsPage() {
|
|
245
277
|
const { warehouseId } = useParams<{ warehouseId: string }>();
|
|
246
278
|
const { basePath, navigate } = useMoveTheWheels();
|
|
279
|
+
const isEditRoute = useLocation().pathname.endsWith('/edit');
|
|
247
280
|
const { warehouse, isLoading, error, refetch } = useWarehouse(warehouseId);
|
|
248
281
|
|
|
249
282
|
const goBack = () => navigate?.(joinPath(basePath, 'inventory/warehouses'));
|
|
250
283
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
251
284
|
|
|
252
285
|
if (!warehouseId) return <NewWarehouseForm />;
|
|
286
|
+
if (isEditRoute) return <NewWarehouseForm editId={warehouseId} />;
|
|
253
287
|
if (isLoading) return <LoadingState message="Loading warehouse details..." />;
|
|
254
288
|
if (error || !warehouse) return <ErrorState title="Warehouse not found" onRetry={refetch} />;
|
|
255
289
|
|
|
@@ -265,7 +299,11 @@ export default function WarehouseDetailsPage() {
|
|
|
265
299
|
<Button variant="outline" onClick={goBack}>
|
|
266
300
|
<ArrowLeft size={16} /> Back
|
|
267
301
|
</Button>
|
|
268
|
-
<Button
|
|
302
|
+
<Button
|
|
303
|
+
onClick={() =>
|
|
304
|
+
navigate?.(joinPath(basePath, `inventory/warehouses/${warehouseId}/edit`))
|
|
305
|
+
}
|
|
306
|
+
>
|
|
269
307
|
<Edit size={16} /> Edit
|
|
270
308
|
</Button>
|
|
271
309
|
</div>
|
|
@@ -307,6 +307,7 @@ export interface Vehicle {
|
|
|
307
307
|
type: VehicleType;
|
|
308
308
|
make: string;
|
|
309
309
|
model: string;
|
|
310
|
+
licensePlate: string;
|
|
310
311
|
year: number;
|
|
311
312
|
capacity: {
|
|
312
313
|
weight: number;
|
|
@@ -339,6 +340,8 @@ export interface Driver {
|
|
|
339
340
|
id: string;
|
|
340
341
|
userId: string;
|
|
341
342
|
fullName: string;
|
|
343
|
+
email: string;
|
|
344
|
+
phone: string;
|
|
342
345
|
employeeId: string;
|
|
343
346
|
licenseNumber: string;
|
|
344
347
|
licenseType: string;
|
|
@@ -69,6 +69,40 @@ function isAbortError(error: unknown): boolean {
|
|
|
69
69
|
|
|
70
70
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Global in-flight cap across every GraphQL call this MFE makes. The workspace
|
|
74
|
+
* service runs a single lean replica (no PgBouncer), and a page like the
|
|
75
|
+
* dashboard fans out ~7 queries at once; multiplied by the retry loop below a
|
|
76
|
+
* transient blip could balloon into dozens of simultaneous requests and tip the
|
|
77
|
+
* replica over. A small semaphore keeps the fan-out polite — requests beyond the
|
|
78
|
+
* limit queue for a moment instead of stampeding the service. 4 is high enough
|
|
79
|
+
* that normal navigation feels instant, low enough that a burst never floods.
|
|
80
|
+
*/
|
|
81
|
+
const MAX_CONCURRENT = 4;
|
|
82
|
+
let activeRequests = 0;
|
|
83
|
+
const pendingAcquirers: Array<() => void> = [];
|
|
84
|
+
|
|
85
|
+
function acquireSlot(): Promise<void> {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
if (activeRequests < MAX_CONCURRENT) {
|
|
88
|
+
activeRequests += 1;
|
|
89
|
+
resolve();
|
|
90
|
+
} else {
|
|
91
|
+
pendingAcquirers.push(resolve);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function releaseSlot(): void {
|
|
97
|
+
const next = pendingAcquirers.shift();
|
|
98
|
+
if (next) {
|
|
99
|
+
// Hand the slot straight to the next waiter — activeRequests stays put.
|
|
100
|
+
next();
|
|
101
|
+
} else {
|
|
102
|
+
activeRequests -= 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
72
106
|
export async function executeGraphQL<TData>(
|
|
73
107
|
options: GraphQLRequestOptions,
|
|
74
108
|
query: string,
|
|
@@ -88,12 +122,28 @@ export async function executeGraphQL<TData>(
|
|
|
88
122
|
};
|
|
89
123
|
const body = JSON.stringify({ query, variables });
|
|
90
124
|
|
|
125
|
+
// Serialize past the global concurrency cap before doing any network work,
|
|
126
|
+
// and always release the slot when this call settles (success or failure).
|
|
127
|
+
await acquireSlot();
|
|
128
|
+
try {
|
|
129
|
+
return await runWithRetries<TData>(url, headers, body);
|
|
130
|
+
} finally {
|
|
131
|
+
releaseSlot();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runWithRetries<TData>(
|
|
136
|
+
url: string,
|
|
137
|
+
headers: Record<string, string>,
|
|
138
|
+
body: string
|
|
139
|
+
): Promise<TData> {
|
|
91
140
|
let lastError: Error = new Error('GraphQL request failed');
|
|
92
141
|
|
|
93
142
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
94
143
|
const canRetry = attempt < MAX_RETRIES;
|
|
95
144
|
// Exponential backoff with jitter before each retry (not the first attempt).
|
|
96
|
-
if (attempt > 0)
|
|
145
|
+
if (attempt > 0)
|
|
146
|
+
await sleep(Math.min(250 * 2 ** (attempt - 1), 2000) + Math.floor(Math.random() * 150));
|
|
97
147
|
|
|
98
148
|
const controller = new AbortController();
|
|
99
149
|
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|