@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.
Files changed (43) hide show
  1. package/dist/components/StatusBadge.js +34 -31
  2. package/dist/constants/index.js +12 -5
  3. package/dist/hooks/useCustomers.js +0 -3
  4. package/dist/hooks/useFleet.js +148 -135
  5. package/dist/hooks/useInventory.js +146 -146
  6. package/dist/hooks/useRoutes.js +5 -14
  7. package/dist/movethewheels/components/StatusBadge.d.ts +3 -1
  8. package/dist/movethewheels/constants/index.d.ts +1 -0
  9. package/dist/movethewheels/hooks/useFleet.d.ts +17 -4
  10. package/dist/movethewheels/hooks/useInventory.d.ts +11 -0
  11. package/dist/movethewheels/types/index.d.ts +19 -75
  12. package/dist/pages/CustomerDetailsPage.js +666 -344
  13. package/dist/pages/CustomersPage.js +71 -80
  14. package/dist/pages/DriverDetailsPage.js +2 -2
  15. package/dist/pages/DriversPage.js +2 -2
  16. package/dist/pages/FleetPage.js +1 -3
  17. package/dist/pages/InventoryPage.js +5 -5
  18. package/dist/pages/RouteDetailsPage.js +273 -248
  19. package/dist/pages/RoutesPage.js +130 -67
  20. package/dist/pages/VehicleDetailsPage.js +527 -389
  21. package/dist/pages/VehiclesPage.js +120 -71
  22. package/dist/pages/WarehouseDetailsPage.js +411 -368
  23. package/dist/pages/WarehousesPage.js +132 -83
  24. package/package.json +1 -1
  25. package/src/movethewheels/components/StatusBadge.tsx +15 -7
  26. package/src/movethewheels/constants/index.ts +9 -0
  27. package/src/movethewheels/hooks/useCustomers.ts +0 -3
  28. package/src/movethewheels/hooks/useFleet.ts +89 -25
  29. package/src/movethewheels/hooks/useInventory.ts +77 -33
  30. package/src/movethewheels/hooks/useRoutes.ts +17 -12
  31. package/src/movethewheels/pages/CustomerDetailsPage.tsx +376 -109
  32. package/src/movethewheels/pages/CustomersPage.tsx +6 -15
  33. package/src/movethewheels/pages/DriverDetailsPage.tsx +3 -1
  34. package/src/movethewheels/pages/DriversPage.tsx +5 -1
  35. package/src/movethewheels/pages/FleetPage.tsx +2 -1
  36. package/src/movethewheels/pages/InventoryPage.tsx +18 -6
  37. package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -34
  38. package/src/movethewheels/pages/RoutesPage.tsx +66 -2
  39. package/src/movethewheels/pages/VehicleDetailsPage.tsx +185 -23
  40. package/src/movethewheels/pages/VehiclesPage.tsx +72 -5
  41. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +217 -138
  42. package/src/movethewheels/pages/WarehousesPage.tsx +81 -17
  43. 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 { Gauge as t } from "../node_modules/lucide-react/dist/esm/icons/gauge.js";
3
- import { Plus as n } from "../node_modules/lucide-react/dist/esm/icons/plus.js";
4
- import { RefreshCw as r } from "../node_modules/lucide-react/dist/esm/icons/refresh-cw.js";
5
- import { Warehouse as i } from "../node_modules/lucide-react/dist/esm/icons/warehouse.js";
6
- import { joinPath as a } from "../utils/navigation.js";
7
- import { useMoveTheWheels as o } from "../providers/MoveTheWheelsProvider.js";
8
- import { useWarehouseStats as s, useWarehouses as c } from "../hooks/useInventory.js";
9
- import { Badge as l, Progress as u } from "../components/ui.js";
10
- import { PageLayout as d } from "../components/PageLayout.js";
11
- import { MetricCard as f, MetricGrid as p } from "../components/MetricCard.js";
12
- import { DataTable as m } from "../components/DataTable.js";
13
- import { useDebouncedValue as h } from "../hooks/useDebouncedValue.js";
14
- import { useState as g } from "react";
15
- import { jsx as _, jsxs as v } from "react/jsx-runtime";
16
- import { CTAOverflowMenu as y, PagePurpose as b } from "@burdenoff/fe-libs/ui";
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
- function x() {
19
- let { basePath: x, navigate: S } = o(), [C, w] = g(0), [T, E] = g(25), [D, O] = g(""), k = h(D), { warehouses: A, totalWarehouses: j, hasMore: M, isLoading: N, error: P, refetch: F } = c({
20
- limit: T,
21
- offset: C * T,
22
- search: k
23
- }), { stats: I } = s(), L = (e) => S?.(a(x, e));
24
- return /* @__PURE__ */ v(d, {
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: `${I.total} warehouses`,
27
- icon: /* @__PURE__ */ _(i, { size: 24 }),
28
- headerActions: /* @__PURE__ */ _(y, {
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__ */ _(n, { size: 16 }),
32
- onSelect: () => L("inventory/warehouses/new")
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__ */ _(r, { size: 16 }),
37
- onSelect: () => F()
52
+ icon: /* @__PURE__ */ E(i, { size: 16 }),
53
+ onSelect: () => q()
38
54
  }]
39
55
  }),
40
56
  children: [
41
- /* @__PURE__ */ _(b, {
42
- icon: /* @__PURE__ */ _(i, { size: 18 }),
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__ */ v("div", { children: [/* @__PURE__ */ _("p", {
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__ */ v(p, {
64
+ }), /* @__PURE__ */ D(S, {
49
65
  columns: 3,
50
66
  children: [
51
- /* @__PURE__ */ _(f, {
67
+ /* @__PURE__ */ E(x, {
52
68
  label: "Total sites",
53
- value: I.total,
54
- icon: /* @__PURE__ */ _(i, { size: 20 }),
69
+ value: Y.total,
70
+ icon: /* @__PURE__ */ E(a, { size: 20 }),
55
71
  accent: "brand"
56
72
  }),
57
- /* @__PURE__ */ _(f, {
73
+ /* @__PURE__ */ E(x, {
58
74
  label: "Active",
59
- value: I.active,
60
- icon: /* @__PURE__ */ _(e, { size: 20 }),
75
+ value: Y.active,
76
+ icon: /* @__PURE__ */ E(e, { size: 20 }),
61
77
  accent: "success",
62
78
  subtitle: "Fulfilling orders"
63
79
  }),
64
- /* @__PURE__ */ _(f, {
80
+ /* @__PURE__ */ E(x, {
65
81
  label: "Avg utilization",
66
- value: I.total > 0 ? `${Math.round(I.avgUtilization * 100)}%` : "—",
67
- icon: /* @__PURE__ */ _(t, { size: 20 }),
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__ */ _(m, {
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__ */ v("div", { children: [/* @__PURE__ */ _("p", {
134
+ cell: (e) => /* @__PURE__ */ D("div", { children: [/* @__PURE__ */ E("p", {
80
135
  className: "font-medium",
81
136
  children: e.name
82
- }), /* @__PURE__ */ _("p", {
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__ */ v("div", { children: [/* @__PURE__ */ v("p", { children: [
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__ */ _("p", {
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__ */ _(l, {
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__ */ v("div", {
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__ */ _(u, {
171
+ children: [/* @__PURE__ */ E(m, {
117
172
  value: t,
118
173
  className: "w-16 h-2"
119
- }), /* @__PURE__ */ v("span", {
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__ */ v("div", {
184
+ cell: (e) => /* @__PURE__ */ D("p", {
130
185
  className: "text-sm",
131
- children: [/* @__PURE__ */ v("p", { children: [
186
+ children: [
132
187
  e.capacity.usedSpace.toLocaleString(),
133
188
  " / ",
134
189
  e.capacity.totalSpace.toLocaleString(),
135
- " ",
136
- "sq ft"
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) => /* @__PURE__ */ _(l, {
152
- variant: e.isActive ? "default" : "secondary",
153
- children: e.isActive ? "Active" : "Inactive"
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: A,
158
- isLoading: N,
159
- error: P,
160
- onRetry: F,
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: D,
213
+ searchQuery: L,
165
214
  onSearchQueryChange: (e) => {
166
- O(e), w(0);
215
+ R(e), P(0);
167
216
  },
168
- onRowClick: (e) => L(`inventory/warehouses/${e.id}`),
217
+ onRowClick: (e) => X(`inventory/warehouses/${e.id}`),
169
218
  getRowId: (e) => e.id,
170
- pageIndex: C,
171
- pageSize: T,
172
- onPageChange: w,
219
+ pageIndex: N,
220
+ pageSize: F,
221
+ onPageChange: P,
173
222
  onPageSizeChange: (e) => {
174
- E(e), w(0);
223
+ I(e), P(0);
175
224
  },
176
- totalItems: j,
177
- hasNextPage: M,
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 { x as default };
235
+ export { j as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-movethewheels",
3
- "version": "2026.925.2",
3
+ "version": "2026.929.1",
4
4
  "description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -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: 'van' | 'truck' | 'motorcycle' | 'bicycle' | 'car';
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
- car: '🚗',
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 { Driver, DriverStatus, FleetFilter, Vehicle, VehicleStatus } from '../types';
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
- fuelType?: string;
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?: string | null;
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
- /** Exported so the vehicle edit form's status field can round-trip (BOFF-7909). */
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 ?? new Date().getFullYear(),
575
+ year: vehicle.year ?? undefined,
523
576
  capacity: {
524
- weight: 0,
525
- volume: 0,
526
- ...(vehicle.capacity ?? {}),
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) : new Date(),
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