@burdenoff/microfe-movethewheels 2026.925.2 → 2026.925.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/components/StatusBadge.js +30 -30
- package/dist/constants/index.js +12 -5
- package/dist/hooks/useCustomers.js +0 -3
- package/dist/hooks/useFleet.js +144 -131
- package/dist/hooks/useInventory.js +144 -145
- package/dist/hooks/useRoutes.js +5 -14
- package/dist/movethewheels/constants/index.d.ts +1 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +13 -1
- package/dist/movethewheels/hooks/useInventory.d.ts +10 -0
- package/dist/movethewheels/types/index.d.ts +13 -70
- package/dist/pages/CustomerDetailsPage.js +666 -344
- package/dist/pages/CustomersPage.js +71 -80
- package/dist/pages/FleetPage.js +1 -3
- package/dist/pages/InventoryPage.js +4 -4
- package/dist/pages/RouteDetailsPage.js +273 -248
- package/dist/pages/RoutesPage.js +130 -67
- package/dist/pages/VehicleDetailsPage.js +506 -391
- package/dist/pages/VehiclesPage.js +119 -70
- package/dist/pages/WarehouseDetailsPage.js +410 -370
- package/dist/pages/WarehousesPage.js +131 -82
- package/package.json +1 -1
- package/src/movethewheels/components/StatusBadge.tsx +8 -3
- package/src/movethewheels/constants/index.ts +9 -0
- package/src/movethewheels/hooks/useCustomers.ts +0 -3
- package/src/movethewheels/hooks/useFleet.ts +74 -19
- package/src/movethewheels/hooks/useInventory.ts +76 -32
- package/src/movethewheels/hooks/useRoutes.ts +17 -12
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +376 -109
- package/src/movethewheels/pages/CustomersPage.tsx +6 -15
- package/src/movethewheels/pages/FleetPage.tsx +2 -1
- package/src/movethewheels/pages/InventoryPage.tsx +14 -5
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -34
- package/src/movethewheels/pages/RoutesPage.tsx +66 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +151 -11
- package/src/movethewheels/pages/VehiclesPage.tsx +68 -3
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +213 -135
- package/src/movethewheels/pages/WarehousesPage.tsx +77 -16
- package/src/movethewheels/types/index.ts +14 -70
|
@@ -1,85 +1,140 @@
|
|
|
1
1
|
import { CircleCheck as e } from "../node_modules/lucide-react/dist/esm/icons/circle-check.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
2
|
+
import { Filter as t } from "../node_modules/lucide-react/dist/esm/icons/filter.js";
|
|
3
|
+
import { Gauge as n } from "../node_modules/lucide-react/dist/esm/icons/gauge.js";
|
|
4
|
+
import { Plus as r } from "../node_modules/lucide-react/dist/esm/icons/plus.js";
|
|
5
|
+
import { RefreshCw as i } from "../node_modules/lucide-react/dist/esm/icons/refresh-cw.js";
|
|
6
|
+
import { Warehouse as a } from "../node_modules/lucide-react/dist/esm/icons/warehouse.js";
|
|
7
|
+
import { joinPath as o } from "../utils/navigation.js";
|
|
8
|
+
import { useMoveTheWheels as s } from "../providers/MoveTheWheelsProvider.js";
|
|
9
|
+
import { useWarehouseStats as c, useWarehouses as l } from "../hooks/useInventory.js";
|
|
10
|
+
import { Badge as u, Button as d, Card as f, CardContent as p, Progress as m, Select as h, SelectContent as g, SelectItem as _, SelectTrigger as v, SelectValue as y } from "../components/ui.js";
|
|
11
|
+
import { PageLayout as b } from "../components/PageLayout.js";
|
|
12
|
+
import { MetricCard as x, MetricGrid as S } from "../components/MetricCard.js";
|
|
13
|
+
import { DataTable as C } from "../components/DataTable.js";
|
|
14
|
+
import { useDebouncedValue as w } from "../hooks/useDebouncedValue.js";
|
|
15
|
+
import { useState as T } from "react";
|
|
16
|
+
import { jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
17
|
+
import { CTAOverflowMenu as O, PagePurpose as k } from "@burdenoff/fe-libs/ui";
|
|
17
18
|
//#region src/movethewheels/pages/WarehousesPage.tsx
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
var A = [
|
|
20
|
+
{
|
|
21
|
+
value: "all",
|
|
22
|
+
label: "All statuses"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: "ACTIVE",
|
|
26
|
+
label: "Active"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: "INACTIVE",
|
|
30
|
+
label: "Inactive"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
function j() {
|
|
34
|
+
let { basePath: j, navigate: M } = s(), [N, P] = T(0), [F, I] = T(25), [L, R] = T(""), [z, B] = T("all"), V = w(L), { warehouses: H, totalWarehouses: U, hasMore: W, isLoading: G, error: K, refetch: q } = l({
|
|
35
|
+
limit: F,
|
|
36
|
+
offset: N * F,
|
|
37
|
+
search: V,
|
|
38
|
+
status: z === "all" ? void 0 : z
|
|
39
|
+
}), J = z !== "all" || L.length > 0, { stats: Y } = c(), X = (e) => M?.(o(j, e));
|
|
40
|
+
return /* @__PURE__ */ D(b, {
|
|
25
41
|
title: "Warehouses",
|
|
26
|
-
subtitle: `${
|
|
27
|
-
icon: /* @__PURE__ */
|
|
28
|
-
headerActions: /* @__PURE__ */
|
|
42
|
+
subtitle: `${Y.total} warehouses`,
|
|
43
|
+
icon: /* @__PURE__ */ E(a, { size: 24 }),
|
|
44
|
+
headerActions: /* @__PURE__ */ E(O, {
|
|
29
45
|
primary: {
|
|
30
46
|
label: "Add Warehouse",
|
|
31
|
-
icon: /* @__PURE__ */
|
|
32
|
-
onSelect: () =>
|
|
47
|
+
icon: /* @__PURE__ */ E(r, { size: 16 }),
|
|
48
|
+
onSelect: () => X("inventory/warehouses/new")
|
|
33
49
|
},
|
|
34
50
|
actions: [{
|
|
35
51
|
label: "Refresh",
|
|
36
|
-
icon: /* @__PURE__ */
|
|
37
|
-
onSelect: () =>
|
|
52
|
+
icon: /* @__PURE__ */ E(i, { size: 16 }),
|
|
53
|
+
onSelect: () => q()
|
|
38
54
|
}]
|
|
39
55
|
}),
|
|
40
56
|
children: [
|
|
41
|
-
/* @__PURE__ */
|
|
42
|
-
icon: /* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ E(k, {
|
|
58
|
+
icon: /* @__PURE__ */ E(a, { size: 18 }),
|
|
43
59
|
children: "Your storage and fulfilment sites. Watch space and pallet utilization across each warehouse to balance stock, spot capacity crunches, and decide where to fulfil an order from."
|
|
44
60
|
}),
|
|
45
|
-
/* @__PURE__ */
|
|
61
|
+
/* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("p", {
|
|
46
62
|
className: "mb-3 text-sm font-medium text-text-secondary",
|
|
47
63
|
children: "Network at a glance"
|
|
48
|
-
}), /* @__PURE__ */
|
|
64
|
+
}), /* @__PURE__ */ D(S, {
|
|
49
65
|
columns: 3,
|
|
50
66
|
children: [
|
|
51
|
-
/* @__PURE__ */
|
|
67
|
+
/* @__PURE__ */ E(x, {
|
|
52
68
|
label: "Total sites",
|
|
53
|
-
value:
|
|
54
|
-
icon: /* @__PURE__ */
|
|
69
|
+
value: Y.total,
|
|
70
|
+
icon: /* @__PURE__ */ E(a, { size: 20 }),
|
|
55
71
|
accent: "brand"
|
|
56
72
|
}),
|
|
57
|
-
/* @__PURE__ */
|
|
73
|
+
/* @__PURE__ */ E(x, {
|
|
58
74
|
label: "Active",
|
|
59
|
-
value:
|
|
60
|
-
icon: /* @__PURE__ */
|
|
75
|
+
value: Y.active,
|
|
76
|
+
icon: /* @__PURE__ */ E(e, { size: 20 }),
|
|
61
77
|
accent: "success",
|
|
62
78
|
subtitle: "Fulfilling orders"
|
|
63
79
|
}),
|
|
64
|
-
/* @__PURE__ */
|
|
80
|
+
/* @__PURE__ */ E(x, {
|
|
65
81
|
label: "Avg utilization",
|
|
66
|
-
value:
|
|
67
|
-
icon: /* @__PURE__ */
|
|
82
|
+
value: Y.total > 0 ? `${Math.round(Y.avgUtilization * 100)}%` : "—",
|
|
83
|
+
icon: /* @__PURE__ */ E(n, { size: 20 }),
|
|
68
84
|
accent: "blue"
|
|
69
85
|
})
|
|
70
86
|
]
|
|
71
87
|
})] }),
|
|
72
|
-
/* @__PURE__ */
|
|
88
|
+
/* @__PURE__ */ E(f, { children: /* @__PURE__ */ E(p, {
|
|
89
|
+
className: "p-4",
|
|
90
|
+
children: /* @__PURE__ */ D("div", {
|
|
91
|
+
className: "flex flex-wrap items-center gap-4",
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ D("div", {
|
|
94
|
+
className: "flex items-center gap-2",
|
|
95
|
+
children: [/* @__PURE__ */ E(t, {
|
|
96
|
+
size: 16,
|
|
97
|
+
className: "text-text-secondary"
|
|
98
|
+
}), /* @__PURE__ */ E("span", {
|
|
99
|
+
className: "text-sm font-medium",
|
|
100
|
+
children: "Filters:"
|
|
101
|
+
})]
|
|
102
|
+
}),
|
|
103
|
+
/* @__PURE__ */ D(h, {
|
|
104
|
+
value: z,
|
|
105
|
+
onValueChange: (e) => {
|
|
106
|
+
B(e), P(0);
|
|
107
|
+
},
|
|
108
|
+
children: [/* @__PURE__ */ E(v, {
|
|
109
|
+
className: "w-[180px]",
|
|
110
|
+
children: /* @__PURE__ */ E(y, { placeholder: "Status" })
|
|
111
|
+
}), /* @__PURE__ */ E(g, { children: A.map((e) => /* @__PURE__ */ E(_, {
|
|
112
|
+
value: e.value,
|
|
113
|
+
children: e.label
|
|
114
|
+
}, e.value)) })]
|
|
115
|
+
}),
|
|
116
|
+
J && /* @__PURE__ */ E(d, {
|
|
117
|
+
variant: "ghost",
|
|
118
|
+
size: "sm",
|
|
119
|
+
onClick: () => {
|
|
120
|
+
B("all"), R(""), P(0);
|
|
121
|
+
},
|
|
122
|
+
children: "Clear filters"
|
|
123
|
+
})
|
|
124
|
+
]
|
|
125
|
+
})
|
|
126
|
+
}) }),
|
|
127
|
+
/* @__PURE__ */ E(C, {
|
|
73
128
|
columns: [
|
|
74
129
|
{
|
|
75
130
|
id: "name",
|
|
76
131
|
header: "Warehouse",
|
|
77
132
|
accessorKey: "name",
|
|
78
133
|
sortable: !0,
|
|
79
|
-
cell: (e) => /* @__PURE__ */
|
|
134
|
+
cell: (e) => /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("p", {
|
|
80
135
|
className: "font-medium",
|
|
81
136
|
children: e.name
|
|
82
|
-
}), /* @__PURE__ */
|
|
137
|
+
}), /* @__PURE__ */ E("p", {
|
|
83
138
|
className: "text-sm text-text-secondary",
|
|
84
139
|
children: e.code
|
|
85
140
|
})] })
|
|
@@ -87,11 +142,11 @@ function x() {
|
|
|
87
142
|
{
|
|
88
143
|
id: "location",
|
|
89
144
|
header: "Location",
|
|
90
|
-
cell: (e) => /* @__PURE__ */
|
|
145
|
+
cell: (e) => /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ D("p", { children: [
|
|
91
146
|
e.address.city,
|
|
92
147
|
", ",
|
|
93
148
|
e.address.state
|
|
94
|
-
] }), /* @__PURE__ */
|
|
149
|
+
] }), /* @__PURE__ */ E("p", {
|
|
95
150
|
className: "text-sm text-text-secondary",
|
|
96
151
|
children: e.address.country
|
|
97
152
|
})] })
|
|
@@ -100,7 +155,7 @@ function x() {
|
|
|
100
155
|
id: "type",
|
|
101
156
|
header: "Type",
|
|
102
157
|
accessorKey: "type",
|
|
103
|
-
cell: (e) => /* @__PURE__ */
|
|
158
|
+
cell: (e) => /* @__PURE__ */ E(u, {
|
|
104
159
|
variant: "outline",
|
|
105
160
|
className: "capitalize",
|
|
106
161
|
children: e.type.replace("_", " ")
|
|
@@ -111,12 +166,12 @@ function x() {
|
|
|
111
166
|
header: "Utilization",
|
|
112
167
|
cell: (e) => {
|
|
113
168
|
let t = e.capacity.usedSpace / e.capacity.totalSpace * 100;
|
|
114
|
-
return /* @__PURE__ */
|
|
169
|
+
return /* @__PURE__ */ D("div", {
|
|
115
170
|
className: "flex items-center gap-2",
|
|
116
|
-
children: [/* @__PURE__ */
|
|
171
|
+
children: [/* @__PURE__ */ E(m, {
|
|
117
172
|
value: t,
|
|
118
173
|
className: "w-16 h-2"
|
|
119
|
-
}), /* @__PURE__ */
|
|
174
|
+
}), /* @__PURE__ */ D("span", {
|
|
120
175
|
className: "text-sm",
|
|
121
176
|
children: [t.toFixed(0), "%"]
|
|
122
177
|
})]
|
|
@@ -126,55 +181,49 @@ function x() {
|
|
|
126
181
|
{
|
|
127
182
|
id: "capacity",
|
|
128
183
|
header: "Capacity",
|
|
129
|
-
cell: (e) => /* @__PURE__ */
|
|
184
|
+
cell: (e) => /* @__PURE__ */ D("p", {
|
|
130
185
|
className: "text-sm",
|
|
131
|
-
children: [
|
|
186
|
+
children: [
|
|
132
187
|
e.capacity.usedSpace.toLocaleString(),
|
|
133
188
|
" / ",
|
|
134
189
|
e.capacity.totalSpace.toLocaleString(),
|
|
135
|
-
" "
|
|
136
|
-
|
|
137
|
-
] }), /* @__PURE__ */ v("p", {
|
|
138
|
-
className: "text-text-secondary",
|
|
139
|
-
children: [
|
|
140
|
-
e.capacity.usedPallets,
|
|
141
|
-
" / ",
|
|
142
|
-
e.capacity.totalPallets,
|
|
143
|
-
" pallets"
|
|
144
|
-
]
|
|
145
|
-
})]
|
|
190
|
+
" sq ft"
|
|
191
|
+
]
|
|
146
192
|
})
|
|
147
193
|
},
|
|
148
194
|
{
|
|
149
195
|
id: "status",
|
|
150
196
|
header: "Status",
|
|
151
|
-
cell: (e) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
197
|
+
cell: (e) => {
|
|
198
|
+
let t = e.status.toUpperCase() === "ACTIVE";
|
|
199
|
+
return /* @__PURE__ */ E(u, {
|
|
200
|
+
variant: t ? "default" : "secondary",
|
|
201
|
+
children: t ? "Active" : "Inactive"
|
|
202
|
+
});
|
|
203
|
+
}
|
|
155
204
|
}
|
|
156
205
|
],
|
|
157
|
-
data:
|
|
158
|
-
isLoading:
|
|
159
|
-
error:
|
|
160
|
-
onRetry:
|
|
206
|
+
data: H,
|
|
207
|
+
isLoading: G,
|
|
208
|
+
error: K,
|
|
209
|
+
onRetry: q,
|
|
161
210
|
searchable: !0,
|
|
162
211
|
searchPlaceholder: "Search warehouses...",
|
|
163
212
|
searchFields: ["name", "code"],
|
|
164
|
-
searchQuery:
|
|
213
|
+
searchQuery: L,
|
|
165
214
|
onSearchQueryChange: (e) => {
|
|
166
|
-
|
|
215
|
+
R(e), P(0);
|
|
167
216
|
},
|
|
168
|
-
onRowClick: (e) =>
|
|
217
|
+
onRowClick: (e) => X(`inventory/warehouses/${e.id}`),
|
|
169
218
|
getRowId: (e) => e.id,
|
|
170
|
-
pageIndex:
|
|
171
|
-
pageSize:
|
|
172
|
-
onPageChange:
|
|
219
|
+
pageIndex: N,
|
|
220
|
+
pageSize: F,
|
|
221
|
+
onPageChange: P,
|
|
173
222
|
onPageSizeChange: (e) => {
|
|
174
|
-
|
|
223
|
+
I(e), P(0);
|
|
175
224
|
},
|
|
176
|
-
totalItems:
|
|
177
|
-
hasNextPage:
|
|
225
|
+
totalItems: U,
|
|
226
|
+
hasNextPage: W,
|
|
178
227
|
emptyIllustration: "empty-data",
|
|
179
228
|
emptyTitle: "No warehouses yet",
|
|
180
229
|
emptyDescription: "Add a warehouse to store inventory and fulfil orders from it."
|
|
@@ -183,4 +232,4 @@ function x() {
|
|
|
183
232
|
});
|
|
184
233
|
}
|
|
185
234
|
//#endregion
|
|
186
|
-
export {
|
|
235
|
+
export { j as default };
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ import type { OrderStatus, VehicleStatus, DriverStatus } from '../types';
|
|
|
4
4
|
import {
|
|
5
5
|
ORDER_STATUS_COLORS,
|
|
6
6
|
VEHICLE_STATUS_COLORS,
|
|
7
|
+
VEHICLE_STATUS_LABELS,
|
|
7
8
|
DRIVER_STATUS_COLORS,
|
|
8
9
|
DRIVER_STATUS_LABELS,
|
|
9
10
|
PRIORITY_COLORS,
|
|
@@ -66,10 +67,14 @@ export function StatusBadge({
|
|
|
66
67
|
// Format status for display — an explicit label always wins; driver status
|
|
67
68
|
// otherwise renders from the same shared label map the filters use, so the
|
|
68
69
|
// table and the filter dropdown can never drift apart again (BOFF-7909).
|
|
70
|
+
const sharedLabel =
|
|
71
|
+
type === 'driver'
|
|
72
|
+
? DRIVER_STATUS_LABELS[status]
|
|
73
|
+
: type === 'vehicle'
|
|
74
|
+
? VEHICLE_STATUS_LABELS[status]
|
|
75
|
+
: undefined;
|
|
69
76
|
const displayStatus =
|
|
70
|
-
label ??
|
|
71
|
-
(type === 'driver' ? DRIVER_STATUS_LABELS[status] : undefined) ??
|
|
72
|
-
status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
77
|
+
label ?? sharedLabel ?? status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
73
78
|
|
|
74
79
|
return (
|
|
75
80
|
<Badge
|
|
@@ -57,6 +57,15 @@ export const VEHICLE_STATUS_COLORS: Record<string, StatusColor> = {
|
|
|
57
57
|
assigned: INFO,
|
|
58
58
|
maintenance: WARNING,
|
|
59
59
|
out_of_service: ERROR,
|
|
60
|
+
decommissioned: NEUTRAL,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const VEHICLE_STATUS_LABELS: Record<string, string> = {
|
|
64
|
+
available: 'Available',
|
|
65
|
+
assigned: 'In Transit',
|
|
66
|
+
maintenance: 'Maintenance',
|
|
67
|
+
out_of_service: 'Out of Service',
|
|
68
|
+
decommissioned: 'Decommissioned',
|
|
60
69
|
};
|
|
61
70
|
|
|
62
71
|
// Driver status colors
|
|
@@ -142,9 +142,6 @@ function mapCustomer(customer: ApiCustomer): Customer {
|
|
|
142
142
|
},
|
|
143
143
|
businessLicense: customer.businessLicense ?? undefined,
|
|
144
144
|
taxId: customer.taxId ?? undefined,
|
|
145
|
-
preferredContactMethod: 'email',
|
|
146
|
-
marketingOptIn: false,
|
|
147
|
-
isActive: !customer.deletedAt,
|
|
148
145
|
rating: customer.rating ?? 0,
|
|
149
146
|
createdAt: new Date(customer.createdAt),
|
|
150
147
|
updatedAt: new Date(customer.updatedAt),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
3
|
-
import type { Driver, DriverStatus, FleetFilter, Vehicle, VehicleStatus } from '../types';
|
|
3
|
+
import type { Driver, DriverStatus, FleetFilter, Location, Vehicle, VehicleStatus } from '../types';
|
|
4
4
|
import { executeGraphQL } from '../utils/graphqlClient';
|
|
5
5
|
|
|
6
6
|
interface UseFleetOptions {
|
|
@@ -22,6 +22,7 @@ export interface CreateVehicleInput {
|
|
|
22
22
|
status?: string;
|
|
23
23
|
fuelType?: string;
|
|
24
24
|
fuelEfficiencyLPer100Km?: number;
|
|
25
|
+
capacity?: { weight?: number; volume?: number; pallets?: number };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface CreateDriverInput {
|
|
@@ -63,6 +64,7 @@ interface UseFleetResult {
|
|
|
63
64
|
createDriver: (input: CreateDriverInput) => Promise<Driver>;
|
|
64
65
|
updateVehicle: (id: string, input: CreateVehicleInput) => Promise<Vehicle>;
|
|
65
66
|
updateDriver: (id: string, input: UpdateDriverInput) => Promise<Driver>;
|
|
67
|
+
deleteVehicle: (id: string) => Promise<void>;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export interface ApiVehicle {
|
|
@@ -230,6 +232,12 @@ const GET_DRIVER_QUERY = `
|
|
|
230
232
|
}
|
|
231
233
|
`;
|
|
232
234
|
|
|
235
|
+
const DELETE_VEHICLE_MUTATION = `
|
|
236
|
+
mutation DeleteVehicle($id: ID!) {
|
|
237
|
+
deleteVehicle(id: $id)
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
|
|
233
241
|
const UPDATE_VEHICLE_STATUS_MUTATION = `
|
|
234
242
|
mutation UpdateVehicleStatus($id: ID!, $status: VehicleStatus!) {
|
|
235
243
|
updateVehicleStatus(id: $id, status: $status) {
|
|
@@ -460,12 +468,19 @@ function mapVehicleStatus(status: string): VehicleStatus {
|
|
|
460
468
|
return 'assigned';
|
|
461
469
|
case 'MAINTENANCE':
|
|
462
470
|
return 'maintenance';
|
|
471
|
+
case 'DECOMMISSIONED':
|
|
472
|
+
return 'decommissioned';
|
|
463
473
|
default:
|
|
464
474
|
return 'out_of_service';
|
|
465
475
|
}
|
|
466
476
|
}
|
|
467
477
|
|
|
468
|
-
/**
|
|
478
|
+
/**
|
|
479
|
+
* Exported so the vehicle edit form's status field can round-trip (BOFF-7909).
|
|
480
|
+
* `decommissioned` must stay distinct from `out_of_service` — collapsing them
|
|
481
|
+
* used to silently rewrite a DECOMMISSIONED vehicle to INACTIVE on any save
|
|
482
|
+
* that touched status, even without the user changing it.
|
|
483
|
+
*/
|
|
469
484
|
export function toApiVehicleStatus(status: VehicleStatus): string {
|
|
470
485
|
switch (status) {
|
|
471
486
|
case 'available':
|
|
@@ -474,6 +489,8 @@ export function toApiVehicleStatus(status: VehicleStatus): string {
|
|
|
474
489
|
return 'IN_TRANSIT';
|
|
475
490
|
case 'maintenance':
|
|
476
491
|
return 'MAINTENANCE';
|
|
492
|
+
case 'decommissioned':
|
|
493
|
+
return 'DECOMMISSIONED';
|
|
477
494
|
default:
|
|
478
495
|
return 'INACTIVE';
|
|
479
496
|
}
|
|
@@ -510,6 +527,33 @@ export function toApiDriverStatus(status: DriverStatus): string {
|
|
|
510
527
|
}
|
|
511
528
|
}
|
|
512
529
|
|
|
530
|
+
/**
|
|
531
|
+
* `currentLocation` is an untyped JSON blob; every real writer (the IoT
|
|
532
|
+
* telemetry consumer — `toVehicleLocation()` in
|
|
533
|
+
* wspace-movethewheels-svc/src/events/iot-location-consumer.ts — and the
|
|
534
|
+
* `updateVehicleLocation`/`updateDriverLocation`-style manual mutations)
|
|
535
|
+
* stores `lat`/`lng`/`recordedAt`, never `latitude`/`longitude`. Reading the
|
|
536
|
+
* latter (as this used to) silently produced (0, 0) for every real fix and
|
|
537
|
+
* fabricated the "last updated" timestamp as "now" on every render. Parse
|
|
538
|
+
* the actual shape instead.
|
|
539
|
+
*/
|
|
540
|
+
function parseCurrentLocation(
|
|
541
|
+
raw: Record<string, unknown> | null | undefined
|
|
542
|
+
): Location | undefined {
|
|
543
|
+
if (!raw) return undefined;
|
|
544
|
+
const lat = raw.lat as number | undefined;
|
|
545
|
+
const lng = raw.lng as number | undefined;
|
|
546
|
+
if (typeof lat !== 'number' || typeof lng !== 'number') return undefined;
|
|
547
|
+
const recordedAt = raw.recordedAt as string | undefined;
|
|
548
|
+
return {
|
|
549
|
+
latitude: lat,
|
|
550
|
+
longitude: lng,
|
|
551
|
+
address: (raw.address as string | undefined) ?? undefined,
|
|
552
|
+
accuracy: typeof raw.accuracy === 'number' ? raw.accuracy : undefined,
|
|
553
|
+
timestamp: recordedAt ? new Date(recordedAt) : undefined,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
513
557
|
export function mapVehicle(vehicle: ApiVehicle): Vehicle {
|
|
514
558
|
return {
|
|
515
559
|
id: vehicle.id,
|
|
@@ -519,7 +563,7 @@ export function mapVehicle(vehicle: ApiVehicle): Vehicle {
|
|
|
519
563
|
model: vehicle.model ?? '',
|
|
520
564
|
licensePlate: vehicle.licensePlate ?? '',
|
|
521
565
|
ownershipType: vehicle.ownershipType ?? undefined,
|
|
522
|
-
year: vehicle.year ??
|
|
566
|
+
year: vehicle.year ?? undefined,
|
|
523
567
|
capacity: {
|
|
524
568
|
weight: 0,
|
|
525
569
|
volume: 0,
|
|
@@ -527,14 +571,7 @@ export function mapVehicle(vehicle: ApiVehicle): Vehicle {
|
|
|
527
571
|
},
|
|
528
572
|
status: mapVehicleStatus(vehicle.status),
|
|
529
573
|
driverId: undefined,
|
|
530
|
-
currentLocation: vehicle.currentLocation
|
|
531
|
-
? {
|
|
532
|
-
latitude: Number((vehicle.currentLocation.latitude as number | undefined) ?? 0),
|
|
533
|
-
longitude: Number((vehicle.currentLocation.longitude as number | undefined) ?? 0),
|
|
534
|
-
address: (vehicle.currentLocation.address as string | undefined) ?? undefined,
|
|
535
|
-
timestamp: new Date(),
|
|
536
|
-
}
|
|
537
|
-
: undefined,
|
|
574
|
+
currentLocation: parseCurrentLocation(vehicle.currentLocation),
|
|
538
575
|
lastMaintenance: vehicle.lastServiceDate ? new Date(vehicle.lastServiceDate) : undefined,
|
|
539
576
|
nextMaintenance: vehicle.nextServiceDueDate ? new Date(vehicle.nextServiceDueDate) : undefined,
|
|
540
577
|
nextMaintenanceMileage: vehicle.nextServiceDueMileage ?? undefined,
|
|
@@ -561,14 +598,7 @@ export function mapDriver(driver: ApiDriver): Driver {
|
|
|
561
598
|
vehicleIds: driver.vehicleId ? [driver.vehicleId] : [],
|
|
562
599
|
assignedVehicle: driver.vehicleId ?? undefined,
|
|
563
600
|
status: mapDriverStatus(driver.status),
|
|
564
|
-
currentLocation: driver.currentLocation
|
|
565
|
-
? {
|
|
566
|
-
latitude: Number((driver.currentLocation.latitude as number | undefined) ?? 0),
|
|
567
|
-
longitude: Number((driver.currentLocation.longitude as number | undefined) ?? 0),
|
|
568
|
-
address: (driver.currentLocation.address as string | undefined) ?? undefined,
|
|
569
|
-
timestamp: new Date(),
|
|
570
|
-
}
|
|
571
|
-
: undefined,
|
|
601
|
+
currentLocation: parseCurrentLocation(driver.currentLocation),
|
|
572
602
|
rating: driver.rating ?? 0,
|
|
573
603
|
notes: driver.notes ?? undefined,
|
|
574
604
|
hireDate: driver.hireDate ? new Date(driver.hireDate) : undefined,
|
|
@@ -677,6 +707,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
677
707
|
status: input.status,
|
|
678
708
|
fuelType: input.fuelType,
|
|
679
709
|
fuelEfficiencyLPer100Km: input.fuelEfficiencyLPer100Km,
|
|
710
|
+
capacity: input.capacity,
|
|
680
711
|
},
|
|
681
712
|
}
|
|
682
713
|
);
|
|
@@ -738,6 +769,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
738
769
|
status: input.status,
|
|
739
770
|
fuelType: input.fuelType,
|
|
740
771
|
fuelEfficiencyLPer100Km: input.fuelEfficiencyLPer100Km,
|
|
772
|
+
capacity: input.capacity,
|
|
741
773
|
},
|
|
742
774
|
}
|
|
743
775
|
);
|
|
@@ -784,6 +816,16 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
784
816
|
[graphQLOptions]
|
|
785
817
|
);
|
|
786
818
|
|
|
819
|
+
const deleteVehicle = useCallback(
|
|
820
|
+
async (id: string): Promise<void> => {
|
|
821
|
+
await executeGraphQL<{ deleteVehicle: boolean }>(graphQLOptions, DELETE_VEHICLE_MUTATION, {
|
|
822
|
+
id,
|
|
823
|
+
});
|
|
824
|
+
setVehicles((previous) => previous.filter((item) => item.id !== id));
|
|
825
|
+
},
|
|
826
|
+
[graphQLOptions]
|
|
827
|
+
);
|
|
828
|
+
|
|
787
829
|
return {
|
|
788
830
|
vehicles,
|
|
789
831
|
drivers,
|
|
@@ -796,6 +838,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
796
838
|
createDriver,
|
|
797
839
|
updateVehicle,
|
|
798
840
|
updateDriver,
|
|
841
|
+
deleteVehicle,
|
|
799
842
|
};
|
|
800
843
|
}
|
|
801
844
|
|
|
@@ -824,6 +867,7 @@ interface UseVehiclesResult {
|
|
|
824
867
|
refetch: () => void;
|
|
825
868
|
updateVehicleStatus: (id: string, status: VehicleStatus) => Promise<Vehicle>;
|
|
826
869
|
assignDriver: (vehicleId: string, driverId: string) => Promise<Vehicle>;
|
|
870
|
+
deleteVehicle: (id: string) => Promise<void>;
|
|
827
871
|
}
|
|
828
872
|
|
|
829
873
|
export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult {
|
|
@@ -918,6 +962,16 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
|
|
|
918
962
|
[graphQLOptions, vehicles]
|
|
919
963
|
);
|
|
920
964
|
|
|
965
|
+
const deleteVehicle = useCallback(
|
|
966
|
+
async (id: string): Promise<void> => {
|
|
967
|
+
await executeGraphQL<{ deleteVehicle: boolean }>(graphQLOptions, DELETE_VEHICLE_MUTATION, {
|
|
968
|
+
id,
|
|
969
|
+
});
|
|
970
|
+
setVehicles((previous) => previous.filter((item) => item.id !== id));
|
|
971
|
+
},
|
|
972
|
+
[graphQLOptions]
|
|
973
|
+
);
|
|
974
|
+
|
|
921
975
|
return {
|
|
922
976
|
vehicles,
|
|
923
977
|
totalVehicles,
|
|
@@ -927,6 +981,7 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
|
|
|
927
981
|
refetch,
|
|
928
982
|
updateVehicleStatus,
|
|
929
983
|
assignDriver,
|
|
984
|
+
deleteVehicle,
|
|
930
985
|
};
|
|
931
986
|
}
|
|
932
987
|
|