@burdenoff/microfe-movethewheels 2026.925.2 → 2026.929.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/components/StatusBadge.js +34 -31
- package/dist/constants/index.js +12 -5
- package/dist/hooks/useCustomers.js +0 -3
- package/dist/hooks/useFleet.js +148 -135
- package/dist/hooks/useInventory.js +146 -146
- package/dist/hooks/useRoutes.js +5 -14
- package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
- package/dist/movethewheels/constants/index.d.ts +1 -0
- package/dist/movethewheels/hooks/useFleet.d.ts +17 -4
- package/dist/movethewheels/hooks/useInventory.d.ts +11 -0
- package/dist/movethewheels/types/index.d.ts +19 -75
- package/dist/pages/CustomerDetailsPage.js +666 -344
- package/dist/pages/CustomersPage.js +71 -80
- package/dist/pages/DriverDetailsPage.js +2 -2
- package/dist/pages/DriversPage.js +2 -2
- package/dist/pages/FleetPage.js +1 -3
- package/dist/pages/InventoryPage.js +5 -5
- package/dist/pages/RouteDetailsPage.js +273 -248
- package/dist/pages/RoutesPage.js +130 -67
- package/dist/pages/VehicleDetailsPage.js +527 -389
- package/dist/pages/VehiclesPage.js +120 -71
- package/dist/pages/WarehouseDetailsPage.js +411 -368
- package/dist/pages/WarehousesPage.js +132 -83
- package/package.json +1 -1
- package/src/movethewheels/components/StatusBadge.tsx +15 -7
- package/src/movethewheels/constants/index.ts +9 -0
- package/src/movethewheels/hooks/useCustomers.ts +0 -3
- package/src/movethewheels/hooks/useFleet.ts +89 -25
- package/src/movethewheels/hooks/useInventory.ts +77 -33
- 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/DriverDetailsPage.tsx +3 -1
- package/src/movethewheels/pages/DriversPage.tsx +5 -1
- package/src/movethewheels/pages/FleetPage.tsx +2 -1
- package/src/movethewheels/pages/InventoryPage.tsx +18 -6
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -34
- package/src/movethewheels/pages/RoutesPage.tsx +66 -2
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +185 -23
- package/src/movethewheels/pages/VehiclesPage.tsx +72 -5
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +217 -138
- package/src/movethewheels/pages/WarehousesPage.tsx +81 -17
- package/src/movethewheels/types/index.ts +28 -75
|
@@ -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("_", " ")
|
|
@@ -110,13 +165,13 @@ function x() {
|
|
|
110
165
|
id: "utilization",
|
|
111
166
|
header: "Utilization",
|
|
112
167
|
cell: (e) => {
|
|
113
|
-
let t = e.capacity.usedSpace / e.capacity.totalSpace * 100;
|
|
114
|
-
return /* @__PURE__ */
|
|
168
|
+
let t = e.capacity.totalSpace > 0 ? e.capacity.usedSpace / e.capacity.totalSpace * 100 : 0;
|
|
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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { cn } from '../utils/cn';
|
|
2
2
|
import { Badge } from './ui';
|
|
3
|
-
import type { OrderStatus, VehicleStatus, DriverStatus } from '../types';
|
|
3
|
+
import type { OrderStatus, VehicleStatus, VehicleType, 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
|
|
@@ -207,16 +212,19 @@ export function VehicleTypeBadge({
|
|
|
207
212
|
size = 'md',
|
|
208
213
|
className,
|
|
209
214
|
}: {
|
|
210
|
-
type:
|
|
215
|
+
type: VehicleType;
|
|
211
216
|
size?: 'sm' | 'md' | 'lg';
|
|
212
217
|
className?: string;
|
|
213
218
|
}) {
|
|
214
|
-
const typeIcons = {
|
|
219
|
+
const typeIcons: Record<VehicleType, string> = {
|
|
215
220
|
van: '🚐',
|
|
216
221
|
truck: '🚚',
|
|
217
222
|
motorcycle: '🏍️',
|
|
218
223
|
bicycle: '🚲',
|
|
219
|
-
|
|
224
|
+
flatbed: '🚛',
|
|
225
|
+
refrigerated: '🧊',
|
|
226
|
+
semi_trailer: '🚛',
|
|
227
|
+
drone: '🚁',
|
|
220
228
|
};
|
|
221
229
|
|
|
222
230
|
return (
|
|
@@ -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,14 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Driver,
|
|
5
|
+
DriverStatus,
|
|
6
|
+
FleetFilter,
|
|
7
|
+
Location,
|
|
8
|
+
Vehicle,
|
|
9
|
+
VehicleFuelType,
|
|
10
|
+
VehicleStatus,
|
|
11
|
+
} from '../types';
|
|
4
12
|
import { executeGraphQL } from '../utils/graphqlClient';
|
|
5
13
|
|
|
6
14
|
interface UseFleetOptions {
|
|
@@ -20,8 +28,10 @@ export interface CreateVehicleInput {
|
|
|
20
28
|
year?: number;
|
|
21
29
|
/** VehicleStatus enum value, e.g. `ACTIVE`, `MAINTENANCE`. */
|
|
22
30
|
status?: string;
|
|
23
|
-
|
|
31
|
+
/** FuelType enum value, e.g. `DIESEL`, `ELECTRIC`. */
|
|
32
|
+
fuelType?: VehicleFuelType;
|
|
24
33
|
fuelEfficiencyLPer100Km?: number;
|
|
34
|
+
capacity?: { weight?: number; volume?: number; pallets?: number };
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export interface CreateDriverInput {
|
|
@@ -63,6 +73,7 @@ interface UseFleetResult {
|
|
|
63
73
|
createDriver: (input: CreateDriverInput) => Promise<Driver>;
|
|
64
74
|
updateVehicle: (id: string, input: CreateVehicleInput) => Promise<Vehicle>;
|
|
65
75
|
updateDriver: (id: string, input: UpdateDriverInput) => Promise<Driver>;
|
|
76
|
+
deleteVehicle: (id: string) => Promise<void>;
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
export interface ApiVehicle {
|
|
@@ -77,7 +88,7 @@ export interface ApiVehicle {
|
|
|
77
88
|
capacity?: Record<string, unknown> | null;
|
|
78
89
|
status: string;
|
|
79
90
|
currentLocation?: Record<string, unknown> | null;
|
|
80
|
-
fuelType?:
|
|
91
|
+
fuelType?: VehicleFuelType | null;
|
|
81
92
|
fuelEfficiencyLPer100Km?: number | null;
|
|
82
93
|
fuelLevel?: number | null;
|
|
83
94
|
mileage?: number | null;
|
|
@@ -230,6 +241,12 @@ const GET_DRIVER_QUERY = `
|
|
|
230
241
|
}
|
|
231
242
|
`;
|
|
232
243
|
|
|
244
|
+
const DELETE_VEHICLE_MUTATION = `
|
|
245
|
+
mutation DeleteVehicle($id: ID!) {
|
|
246
|
+
deleteVehicle(id: $id)
|
|
247
|
+
}
|
|
248
|
+
`;
|
|
249
|
+
|
|
233
250
|
const UPDATE_VEHICLE_STATUS_MUTATION = `
|
|
234
251
|
mutation UpdateVehicleStatus($id: ID!, $status: VehicleStatus!) {
|
|
235
252
|
updateVehicleStatus(id: $id, status: $status) {
|
|
@@ -460,12 +477,19 @@ function mapVehicleStatus(status: string): VehicleStatus {
|
|
|
460
477
|
return 'assigned';
|
|
461
478
|
case 'MAINTENANCE':
|
|
462
479
|
return 'maintenance';
|
|
480
|
+
case 'DECOMMISSIONED':
|
|
481
|
+
return 'decommissioned';
|
|
463
482
|
default:
|
|
464
483
|
return 'out_of_service';
|
|
465
484
|
}
|
|
466
485
|
}
|
|
467
486
|
|
|
468
|
-
/**
|
|
487
|
+
/**
|
|
488
|
+
* Exported so the vehicle edit form's status field can round-trip (BOFF-7909).
|
|
489
|
+
* `decommissioned` must stay distinct from `out_of_service` — collapsing them
|
|
490
|
+
* used to silently rewrite a DECOMMISSIONED vehicle to INACTIVE on any save
|
|
491
|
+
* that touched status, even without the user changing it.
|
|
492
|
+
*/
|
|
469
493
|
export function toApiVehicleStatus(status: VehicleStatus): string {
|
|
470
494
|
switch (status) {
|
|
471
495
|
case 'available':
|
|
@@ -474,6 +498,8 @@ export function toApiVehicleStatus(status: VehicleStatus): string {
|
|
|
474
498
|
return 'IN_TRANSIT';
|
|
475
499
|
case 'maintenance':
|
|
476
500
|
return 'MAINTENANCE';
|
|
501
|
+
case 'decommissioned':
|
|
502
|
+
return 'DECOMMISSIONED';
|
|
477
503
|
default:
|
|
478
504
|
return 'INACTIVE';
|
|
479
505
|
}
|
|
@@ -510,6 +536,33 @@ export function toApiDriverStatus(status: DriverStatus): string {
|
|
|
510
536
|
}
|
|
511
537
|
}
|
|
512
538
|
|
|
539
|
+
/**
|
|
540
|
+
* `currentLocation` is an untyped JSON blob; every real writer (the IoT
|
|
541
|
+
* telemetry consumer — `toVehicleLocation()` in
|
|
542
|
+
* wspace-movethewheels-svc/src/events/iot-location-consumer.ts — and the
|
|
543
|
+
* `updateVehicleLocation`/`updateDriverLocation`-style manual mutations)
|
|
544
|
+
* stores `lat`/`lng`/`recordedAt`, never `latitude`/`longitude`. Reading the
|
|
545
|
+
* latter (as this used to) silently produced (0, 0) for every real fix and
|
|
546
|
+
* fabricated the "last updated" timestamp as "now" on every render. Parse
|
|
547
|
+
* the actual shape instead.
|
|
548
|
+
*/
|
|
549
|
+
function parseCurrentLocation(
|
|
550
|
+
raw: Record<string, unknown> | null | undefined
|
|
551
|
+
): Location | undefined {
|
|
552
|
+
if (!raw) return undefined;
|
|
553
|
+
const lat = raw.lat as number | undefined;
|
|
554
|
+
const lng = raw.lng as number | undefined;
|
|
555
|
+
if (typeof lat !== 'number' || typeof lng !== 'number') return undefined;
|
|
556
|
+
const recordedAt = raw.recordedAt as string | undefined;
|
|
557
|
+
return {
|
|
558
|
+
latitude: lat,
|
|
559
|
+
longitude: lng,
|
|
560
|
+
address: (raw.address as string | undefined) ?? undefined,
|
|
561
|
+
accuracy: typeof raw.accuracy === 'number' ? raw.accuracy : undefined,
|
|
562
|
+
timestamp: recordedAt ? new Date(recordedAt) : undefined,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
513
566
|
export function mapVehicle(vehicle: ApiVehicle): Vehicle {
|
|
514
567
|
return {
|
|
515
568
|
id: vehicle.id,
|
|
@@ -519,22 +572,15 @@ export function mapVehicle(vehicle: ApiVehicle): Vehicle {
|
|
|
519
572
|
model: vehicle.model ?? '',
|
|
520
573
|
licensePlate: vehicle.licensePlate ?? '',
|
|
521
574
|
ownershipType: vehicle.ownershipType ?? undefined,
|
|
522
|
-
year: vehicle.year ??
|
|
575
|
+
year: vehicle.year ?? undefined,
|
|
523
576
|
capacity: {
|
|
524
|
-
weight:
|
|
525
|
-
volume:
|
|
526
|
-
|
|
577
|
+
weight: typeof vehicle.capacity?.weight === 'number' ? vehicle.capacity.weight : undefined,
|
|
578
|
+
volume: typeof vehicle.capacity?.volume === 'number' ? vehicle.capacity.volume : undefined,
|
|
579
|
+
pallets: typeof vehicle.capacity?.pallets === 'number' ? vehicle.capacity.pallets : undefined,
|
|
527
580
|
},
|
|
528
581
|
status: mapVehicleStatus(vehicle.status),
|
|
529
582
|
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,
|
|
583
|
+
currentLocation: parseCurrentLocation(vehicle.currentLocation),
|
|
538
584
|
lastMaintenance: vehicle.lastServiceDate ? new Date(vehicle.lastServiceDate) : undefined,
|
|
539
585
|
nextMaintenance: vehicle.nextServiceDueDate ? new Date(vehicle.nextServiceDueDate) : undefined,
|
|
540
586
|
nextMaintenanceMileage: vehicle.nextServiceDueMileage ?? undefined,
|
|
@@ -557,18 +603,11 @@ export function mapDriver(driver: ApiDriver): Driver {
|
|
|
557
603
|
employeeId: driver.employeeId,
|
|
558
604
|
licenseNumber: driver.licenseNumber ?? '',
|
|
559
605
|
licenseType: driver.licenseClass ?? 'standard',
|
|
560
|
-
licenseExpiry: driver.licenseExpiry ? new Date(driver.licenseExpiry) :
|
|
606
|
+
licenseExpiry: driver.licenseExpiry ? new Date(driver.licenseExpiry) : undefined,
|
|
561
607
|
vehicleIds: driver.vehicleId ? [driver.vehicleId] : [],
|
|
562
608
|
assignedVehicle: driver.vehicleId ?? undefined,
|
|
563
609
|
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,
|
|
610
|
+
currentLocation: parseCurrentLocation(driver.currentLocation),
|
|
572
611
|
rating: driver.rating ?? 0,
|
|
573
612
|
notes: driver.notes ?? undefined,
|
|
574
613
|
hireDate: driver.hireDate ? new Date(driver.hireDate) : undefined,
|
|
@@ -677,6 +716,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
677
716
|
status: input.status,
|
|
678
717
|
fuelType: input.fuelType,
|
|
679
718
|
fuelEfficiencyLPer100Km: input.fuelEfficiencyLPer100Km,
|
|
719
|
+
capacity: input.capacity,
|
|
680
720
|
},
|
|
681
721
|
}
|
|
682
722
|
);
|
|
@@ -738,6 +778,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
738
778
|
status: input.status,
|
|
739
779
|
fuelType: input.fuelType,
|
|
740
780
|
fuelEfficiencyLPer100Km: input.fuelEfficiencyLPer100Km,
|
|
781
|
+
capacity: input.capacity,
|
|
741
782
|
},
|
|
742
783
|
}
|
|
743
784
|
);
|
|
@@ -784,6 +825,16 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
784
825
|
[graphQLOptions]
|
|
785
826
|
);
|
|
786
827
|
|
|
828
|
+
const deleteVehicle = useCallback(
|
|
829
|
+
async (id: string): Promise<void> => {
|
|
830
|
+
await executeGraphQL<{ deleteVehicle: boolean }>(graphQLOptions, DELETE_VEHICLE_MUTATION, {
|
|
831
|
+
id,
|
|
832
|
+
});
|
|
833
|
+
setVehicles((previous) => previous.filter((item) => item.id !== id));
|
|
834
|
+
},
|
|
835
|
+
[graphQLOptions]
|
|
836
|
+
);
|
|
837
|
+
|
|
787
838
|
return {
|
|
788
839
|
vehicles,
|
|
789
840
|
drivers,
|
|
@@ -796,6 +847,7 @@ export function useFleet(options: UseFleetOptions = {}): UseFleetResult {
|
|
|
796
847
|
createDriver,
|
|
797
848
|
updateVehicle,
|
|
798
849
|
updateDriver,
|
|
850
|
+
deleteVehicle,
|
|
799
851
|
};
|
|
800
852
|
}
|
|
801
853
|
|
|
@@ -824,6 +876,7 @@ interface UseVehiclesResult {
|
|
|
824
876
|
refetch: () => void;
|
|
825
877
|
updateVehicleStatus: (id: string, status: VehicleStatus) => Promise<Vehicle>;
|
|
826
878
|
assignDriver: (vehicleId: string, driverId: string) => Promise<Vehicle>;
|
|
879
|
+
deleteVehicle: (id: string) => Promise<void>;
|
|
827
880
|
}
|
|
828
881
|
|
|
829
882
|
export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult {
|
|
@@ -918,6 +971,16 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
|
|
|
918
971
|
[graphQLOptions, vehicles]
|
|
919
972
|
);
|
|
920
973
|
|
|
974
|
+
const deleteVehicle = useCallback(
|
|
975
|
+
async (id: string): Promise<void> => {
|
|
976
|
+
await executeGraphQL<{ deleteVehicle: boolean }>(graphQLOptions, DELETE_VEHICLE_MUTATION, {
|
|
977
|
+
id,
|
|
978
|
+
});
|
|
979
|
+
setVehicles((previous) => previous.filter((item) => item.id !== id));
|
|
980
|
+
},
|
|
981
|
+
[graphQLOptions]
|
|
982
|
+
);
|
|
983
|
+
|
|
921
984
|
return {
|
|
922
985
|
vehicles,
|
|
923
986
|
totalVehicles,
|
|
@@ -927,6 +990,7 @@ export function useVehicles(options: UseVehiclesOptions = {}): UseVehiclesResult
|
|
|
927
990
|
refetch,
|
|
928
991
|
updateVehicleStatus,
|
|
929
992
|
assignDriver,
|
|
993
|
+
deleteVehicle,
|
|
930
994
|
};
|
|
931
995
|
}
|
|
932
996
|
|