@burdenoff/microfe-movethewheels 2026.1030.1 → 2026.1031.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/ExportStatusBanner.js +53 -0
- package/dist/hooks/useEntityExport.js +85 -0
- package/dist/movethewheels/components/ExportStatusBanner.d.ts +7 -0
- package/dist/movethewheels/hooks/useEntityExport.d.ts +32 -0
- package/dist/movethewheels/utils/entityExport.d.ts +64 -0
- package/dist/pages/CustomersPage.js +64 -58
- package/dist/pages/DriversPage.js +113 -107
- package/dist/pages/OrdersListPage.js +85 -79
- package/dist/pages/ProductsPage.js +63 -57
- package/dist/pages/VehiclesPage.js +91 -85
- package/dist/utils/entityExport.js +54 -0
- package/package.json +3 -2
- package/src/movethewheels/components/ExportStatusBanner.tsx +78 -0
- package/src/movethewheels/hooks/useEntityExport.ts +130 -0
- package/src/movethewheels/pages/CustomersPage.tsx +11 -2
- package/src/movethewheels/pages/DriversPage.tsx +11 -2
- package/src/movethewheels/pages/OrdersListPage.tsx +13 -2
- package/src/movethewheels/pages/ProductsPage.tsx +11 -2
- package/src/movethewheels/pages/VehiclesPage.tsx +11 -2
- package/src/movethewheels/utils/entityExport.ts +140 -0
|
@@ -2,145 +2,151 @@ import { CircleCheck as e } from "../node_modules/lucide-react/dist/esm/icons/ci
|
|
|
2
2
|
import { Download as t } from "../node_modules/lucide-react/dist/esm/icons/download.js";
|
|
3
3
|
import { Filter as n } from "../node_modules/lucide-react/dist/esm/icons/filter.js";
|
|
4
4
|
import { Plus as r } from "../node_modules/lucide-react/dist/esm/icons/plus.js";
|
|
5
|
-
import { RefreshCw as
|
|
6
|
-
import { Route as
|
|
7
|
-
import { Truck as
|
|
8
|
-
import { Wrench as
|
|
9
|
-
import { joinPath as
|
|
10
|
-
import { useMoveTheWheels as
|
|
11
|
-
import { useFleetStats as
|
|
12
|
-
import { Button as
|
|
13
|
-
import { PageLayout as
|
|
14
|
-
import { MetricCard as
|
|
15
|
-
import { VEHICLE_STATUS_LABELS as
|
|
16
|
-
import { StatusBadge as
|
|
17
|
-
import { DataTable as
|
|
18
|
-
import { useDebouncedValue as
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
5
|
+
import { RefreshCw as i } from "../node_modules/lucide-react/dist/esm/icons/refresh-cw.js";
|
|
6
|
+
import { Route as a } from "../node_modules/lucide-react/dist/esm/icons/route.js";
|
|
7
|
+
import { Truck as o } from "../node_modules/lucide-react/dist/esm/icons/truck.js";
|
|
8
|
+
import { Wrench as s } from "../node_modules/lucide-react/dist/esm/icons/wrench.js";
|
|
9
|
+
import { joinPath as c } from "../utils/navigation.js";
|
|
10
|
+
import { useMoveTheWheels as l } from "../providers/MoveTheWheelsProvider.js";
|
|
11
|
+
import { useFleetStats as u, useVehicles as d } from "../hooks/useFleet.js";
|
|
12
|
+
import { Button as f, Card as ee, CardContent as p, Progress as m, Select as h, SelectContent as g, SelectItem as te, SelectTrigger as ne, SelectValue as re } from "../components/ui.js";
|
|
13
|
+
import { PageLayout as ie } from "../components/PageLayout.js";
|
|
14
|
+
import { MetricCard as _, MetricGrid as v } from "../components/MetricCard.js";
|
|
15
|
+
import { VEHICLE_STATUS_LABELS as y } from "../constants/index.js";
|
|
16
|
+
import { StatusBadge as b, VehicleTypeBadge as x } from "../components/StatusBadge.js";
|
|
17
|
+
import { DataTable as S } from "../components/DataTable.js";
|
|
18
|
+
import { useDebouncedValue as C } from "../hooks/useDebouncedValue.js";
|
|
19
|
+
import { useEntityExport as w } from "../hooks/useEntityExport.js";
|
|
20
|
+
import { ExportStatusBanner as T } from "../components/ExportStatusBanner.js";
|
|
21
|
+
import { useState as E } from "react";
|
|
22
|
+
import { jsx as D, jsxs as O } from "react/jsx-runtime";
|
|
23
|
+
import { CTAOverflowMenu as k, PagePurpose as A } from "@burdenoff/fe-libs/ui";
|
|
22
24
|
//#region src/movethewheels/pages/VehiclesPage.tsx
|
|
23
|
-
var
|
|
25
|
+
var j = [{
|
|
24
26
|
value: "all",
|
|
25
27
|
label: "All statuses"
|
|
26
|
-
}, ...Object.entries(
|
|
28
|
+
}, ...Object.entries(y).map(([e, t]) => ({
|
|
27
29
|
value: e,
|
|
28
30
|
label: t
|
|
29
31
|
}))];
|
|
30
|
-
function
|
|
31
|
-
let { basePath:
|
|
32
|
-
limit:
|
|
33
|
-
offset:
|
|
34
|
-
search:
|
|
35
|
-
status:
|
|
36
|
-
}),
|
|
37
|
-
|
|
32
|
+
function M() {
|
|
33
|
+
let { basePath: y, navigate: M } = l(), { state: N, isExporting: P, startExport: F, dismiss: ae } = w("vehicles", "vehicle"), [I, L] = E(0), [R, z] = E(25), [B, V] = E(""), [H, U] = E("all"), W = C(B), { vehicles: G, totalVehicles: K, hasMore: q, isLoading: J, error: Y, refetch: X } = d({
|
|
34
|
+
limit: R,
|
|
35
|
+
offset: I * R,
|
|
36
|
+
search: W || void 0,
|
|
37
|
+
status: H === "all" ? void 0 : H
|
|
38
|
+
}), oe = H !== "all" || B.length > 0, { stats: Z } = u(), Q = Z.totalVehicles, $ = (e) => {
|
|
39
|
+
M && M(c(y, e));
|
|
38
40
|
};
|
|
39
|
-
return /* @__PURE__ */
|
|
41
|
+
return /* @__PURE__ */ O(ie, {
|
|
40
42
|
title: "Vehicles",
|
|
41
43
|
subtitle: `${Q} vehicles in fleet`,
|
|
42
|
-
icon: /* @__PURE__ */
|
|
43
|
-
headerActions: /* @__PURE__ */ k
|
|
44
|
+
icon: /* @__PURE__ */ D(o, { size: 24 }),
|
|
45
|
+
headerActions: /* @__PURE__ */ D(k, {
|
|
44
46
|
primary: {
|
|
45
47
|
label: "Add Vehicle",
|
|
46
|
-
icon: /* @__PURE__ */
|
|
48
|
+
icon: /* @__PURE__ */ D(r, { size: 16 }),
|
|
47
49
|
onSelect: () => $("fleet/vehicles/new")
|
|
48
50
|
},
|
|
49
51
|
actions: [{
|
|
50
52
|
label: "Refresh",
|
|
51
|
-
icon: /* @__PURE__ */
|
|
52
|
-
onSelect: () =>
|
|
53
|
+
icon: /* @__PURE__ */ D(i, { size: 16 }),
|
|
54
|
+
onSelect: () => X()
|
|
53
55
|
}, {
|
|
54
|
-
label: "Export",
|
|
55
|
-
icon: /* @__PURE__ */
|
|
56
|
-
onSelect:
|
|
56
|
+
label: P ? "Exporting…" : "Export",
|
|
57
|
+
icon: /* @__PURE__ */ D(t, { size: 16 }),
|
|
58
|
+
onSelect: F
|
|
57
59
|
}]
|
|
58
60
|
}),
|
|
59
61
|
children: [
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
icon: /* @__PURE__ */
|
|
62
|
+
/* @__PURE__ */ D(A, {
|
|
63
|
+
icon: /* @__PURE__ */ D(o, { size: 18 }),
|
|
62
64
|
children: "Every truck, van and bike in your fleet. Monitor capacity, fuel level, mileage and maintenance status here so you always know which vehicles are road-ready before dispatching an order."
|
|
63
65
|
}),
|
|
64
|
-
/* @__PURE__ */
|
|
66
|
+
/* @__PURE__ */ D(T, {
|
|
67
|
+
state: N,
|
|
68
|
+
onDismiss: ae
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ O("div", { children: [/* @__PURE__ */ D("p", {
|
|
65
71
|
className: "mb-3 text-sm font-medium text-text-secondary",
|
|
66
72
|
children: "Fleet at a glance"
|
|
67
|
-
}), /* @__PURE__ */
|
|
73
|
+
}), /* @__PURE__ */ O(v, {
|
|
68
74
|
columns: 4,
|
|
69
75
|
children: [
|
|
70
|
-
/* @__PURE__ */
|
|
76
|
+
/* @__PURE__ */ D(_, {
|
|
71
77
|
label: "Total vehicles",
|
|
72
78
|
value: Q,
|
|
73
|
-
icon: /* @__PURE__ */
|
|
79
|
+
icon: /* @__PURE__ */ D(o, { size: 20 }),
|
|
74
80
|
accent: "brand"
|
|
75
81
|
}),
|
|
76
|
-
/* @__PURE__ */
|
|
82
|
+
/* @__PURE__ */ D(_, {
|
|
77
83
|
label: "Available now",
|
|
78
84
|
value: Z.vehiclesByStatus.available,
|
|
79
|
-
icon: /* @__PURE__ */
|
|
85
|
+
icon: /* @__PURE__ */ D(e, { size: 20 }),
|
|
80
86
|
accent: "success",
|
|
81
87
|
subtitle: "Ready to dispatch"
|
|
82
88
|
}),
|
|
83
|
-
/* @__PURE__ */
|
|
89
|
+
/* @__PURE__ */ D(_, {
|
|
84
90
|
label: "On assignment",
|
|
85
91
|
value: Z.vehiclesByStatus.assigned,
|
|
86
|
-
icon: /* @__PURE__ */
|
|
92
|
+
icon: /* @__PURE__ */ D(a, { size: 20 }),
|
|
87
93
|
accent: "blue"
|
|
88
94
|
}),
|
|
89
|
-
/* @__PURE__ */
|
|
95
|
+
/* @__PURE__ */ D(_, {
|
|
90
96
|
label: "In maintenance",
|
|
91
97
|
value: Z.vehiclesByStatus.maintenance,
|
|
92
|
-
icon: /* @__PURE__ */
|
|
98
|
+
icon: /* @__PURE__ */ D(s, { size: 20 }),
|
|
93
99
|
accent: "warning"
|
|
94
100
|
})
|
|
95
101
|
]
|
|
96
102
|
})] }),
|
|
97
|
-
/* @__PURE__ */
|
|
103
|
+
/* @__PURE__ */ D(ee, { children: /* @__PURE__ */ D(p, {
|
|
98
104
|
className: "p-4",
|
|
99
|
-
children: /* @__PURE__ */
|
|
105
|
+
children: /* @__PURE__ */ O("div", {
|
|
100
106
|
className: "flex flex-wrap items-center gap-4",
|
|
101
107
|
children: [
|
|
102
|
-
/* @__PURE__ */
|
|
108
|
+
/* @__PURE__ */ O("div", {
|
|
103
109
|
className: "flex items-center gap-2",
|
|
104
|
-
children: [/* @__PURE__ */
|
|
110
|
+
children: [/* @__PURE__ */ D(n, {
|
|
105
111
|
size: 16,
|
|
106
112
|
className: "text-text-secondary"
|
|
107
|
-
}), /* @__PURE__ */
|
|
113
|
+
}), /* @__PURE__ */ D("span", {
|
|
108
114
|
className: "text-sm font-medium",
|
|
109
115
|
children: "Filters:"
|
|
110
116
|
})]
|
|
111
117
|
}),
|
|
112
|
-
/* @__PURE__ */
|
|
113
|
-
value:
|
|
118
|
+
/* @__PURE__ */ O(h, {
|
|
119
|
+
value: H,
|
|
114
120
|
onValueChange: (e) => {
|
|
115
|
-
|
|
121
|
+
U(e), L(0);
|
|
116
122
|
},
|
|
117
|
-
children: [/* @__PURE__ */
|
|
123
|
+
children: [/* @__PURE__ */ D(ne, {
|
|
118
124
|
className: "w-[180px]",
|
|
119
|
-
children: /* @__PURE__ */
|
|
120
|
-
}), /* @__PURE__ */
|
|
125
|
+
children: /* @__PURE__ */ D(re, { placeholder: "Status" })
|
|
126
|
+
}), /* @__PURE__ */ D(g, { children: j.map((e) => /* @__PURE__ */ D(te, {
|
|
121
127
|
value: e.value,
|
|
122
128
|
children: e.label
|
|
123
129
|
}, e.value)) })]
|
|
124
130
|
}),
|
|
125
|
-
|
|
131
|
+
oe && /* @__PURE__ */ D(f, {
|
|
126
132
|
variant: "ghost",
|
|
127
133
|
size: "sm",
|
|
128
134
|
onClick: () => {
|
|
129
|
-
|
|
135
|
+
U("all"), V(""), L(0);
|
|
130
136
|
},
|
|
131
137
|
children: "Clear filters"
|
|
132
138
|
})
|
|
133
139
|
]
|
|
134
140
|
})
|
|
135
141
|
}) }),
|
|
136
|
-
/* @__PURE__ */
|
|
142
|
+
/* @__PURE__ */ D(S, {
|
|
137
143
|
columns: [
|
|
138
144
|
{
|
|
139
145
|
id: "vehicleNumber",
|
|
140
146
|
header: "Vehicle #",
|
|
141
147
|
accessorKey: "vehicleNumber",
|
|
142
148
|
sortable: !0,
|
|
143
|
-
cell: (e) => /* @__PURE__ */
|
|
149
|
+
cell: (e) => /* @__PURE__ */ D("span", {
|
|
144
150
|
className: "font-medium",
|
|
145
151
|
children: e.vehicleNumber
|
|
146
152
|
})
|
|
@@ -149,19 +155,19 @@ function P() {
|
|
|
149
155
|
id: "type",
|
|
150
156
|
header: "Type",
|
|
151
157
|
accessorKey: "type",
|
|
152
|
-
cell: (e) => /* @__PURE__ */
|
|
158
|
+
cell: (e) => /* @__PURE__ */ D(x, { type: e.type })
|
|
153
159
|
},
|
|
154
160
|
{
|
|
155
161
|
id: "details",
|
|
156
162
|
header: "Vehicle",
|
|
157
|
-
cell: (e) => /* @__PURE__ */
|
|
163
|
+
cell: (e) => /* @__PURE__ */ O("div", { children: [/* @__PURE__ */ O("p", {
|
|
158
164
|
className: "font-medium",
|
|
159
165
|
children: [
|
|
160
166
|
e.make,
|
|
161
167
|
" ",
|
|
162
168
|
e.model
|
|
163
169
|
]
|
|
164
|
-
}), /* @__PURE__ */
|
|
170
|
+
}), /* @__PURE__ */ D("p", {
|
|
165
171
|
className: "text-sm text-text-secondary",
|
|
166
172
|
children: e.year ?? "—"
|
|
167
173
|
})] })
|
|
@@ -171,7 +177,7 @@ function P() {
|
|
|
171
177
|
header: "Status",
|
|
172
178
|
accessorKey: "status",
|
|
173
179
|
sortable: !0,
|
|
174
|
-
cell: (e) => /* @__PURE__ */
|
|
180
|
+
cell: (e) => /* @__PURE__ */ D(b, {
|
|
175
181
|
status: e.status,
|
|
176
182
|
type: "vehicle"
|
|
177
183
|
})
|
|
@@ -179,9 +185,9 @@ function P() {
|
|
|
179
185
|
{
|
|
180
186
|
id: "capacity",
|
|
181
187
|
header: "Capacity",
|
|
182
|
-
cell: (e) => /* @__PURE__ */
|
|
188
|
+
cell: (e) => /* @__PURE__ */ O("div", {
|
|
183
189
|
className: "text-sm",
|
|
184
|
-
children: [/* @__PURE__ */
|
|
190
|
+
children: [/* @__PURE__ */ D("p", { children: e.capacity.weight === void 0 ? "—" : `${e.capacity.weight} kg` }), /* @__PURE__ */ D("p", {
|
|
185
191
|
className: "text-text-secondary",
|
|
186
192
|
children: e.capacity.volume === void 0 ? "—" : `${e.capacity.volume} m³`
|
|
187
193
|
})]
|
|
@@ -190,12 +196,12 @@ function P() {
|
|
|
190
196
|
{
|
|
191
197
|
id: "fuel",
|
|
192
198
|
header: "Fuel Level",
|
|
193
|
-
cell: (e) => e.fuelLevel === void 0 ? "-" : /* @__PURE__ */
|
|
199
|
+
cell: (e) => e.fuelLevel === void 0 ? "-" : /* @__PURE__ */ O("div", {
|
|
194
200
|
className: "flex items-center gap-2",
|
|
195
|
-
children: [/* @__PURE__ */
|
|
201
|
+
children: [/* @__PURE__ */ D(m, {
|
|
196
202
|
value: e.fuelLevel,
|
|
197
203
|
className: "w-16 h-2"
|
|
198
|
-
}), /* @__PURE__ */
|
|
204
|
+
}), /* @__PURE__ */ O("span", {
|
|
199
205
|
className: "text-sm",
|
|
200
206
|
children: [e.fuelLevel, "%"]
|
|
201
207
|
})]
|
|
@@ -209,10 +215,10 @@ function P() {
|
|
|
209
215
|
cell: (e) => e.mileage ? `${e.mileage.toLocaleString()} mi` : "-"
|
|
210
216
|
}
|
|
211
217
|
],
|
|
212
|
-
data:
|
|
213
|
-
isLoading:
|
|
214
|
-
error:
|
|
215
|
-
onRetry:
|
|
218
|
+
data: G,
|
|
219
|
+
isLoading: J,
|
|
220
|
+
error: Y,
|
|
221
|
+
onRetry: X,
|
|
216
222
|
searchable: !0,
|
|
217
223
|
searchPlaceholder: "Search vehicles...",
|
|
218
224
|
searchFields: [
|
|
@@ -220,20 +226,20 @@ function P() {
|
|
|
220
226
|
"make",
|
|
221
227
|
"model"
|
|
222
228
|
],
|
|
223
|
-
searchQuery:
|
|
229
|
+
searchQuery: B,
|
|
224
230
|
onSearchQueryChange: (e) => {
|
|
225
|
-
|
|
231
|
+
V(e), L(0);
|
|
226
232
|
},
|
|
227
233
|
onRowClick: (e) => $(`fleet/vehicles/${e.id}`),
|
|
228
234
|
getRowId: (e) => e.id,
|
|
229
|
-
pageIndex:
|
|
230
|
-
pageSize:
|
|
231
|
-
onPageChange:
|
|
235
|
+
pageIndex: I,
|
|
236
|
+
pageSize: R,
|
|
237
|
+
onPageChange: L,
|
|
232
238
|
onPageSizeChange: (e) => {
|
|
233
|
-
|
|
239
|
+
z(e), L(0);
|
|
234
240
|
},
|
|
235
|
-
totalItems:
|
|
236
|
-
hasNextPage:
|
|
241
|
+
totalItems: K,
|
|
242
|
+
hasNextPage: q,
|
|
237
243
|
emptyIllustration: "empty-data",
|
|
238
244
|
emptyTitle: "No vehicles yet",
|
|
239
245
|
emptyDescription: "Add vehicles to your fleet to start assigning them to deliveries."
|
|
@@ -242,4 +248,4 @@ function P() {
|
|
|
242
248
|
});
|
|
243
249
|
}
|
|
244
250
|
//#endregion
|
|
245
|
-
export {
|
|
251
|
+
export { M as default };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/movethewheels/utils/entityExport.ts
|
|
2
|
+
var e = {
|
|
3
|
+
customers: "movethewheels_customers",
|
|
4
|
+
drivers: "movethewheels_drivers",
|
|
5
|
+
orders: "movethewheels_delivery_jobs",
|
|
6
|
+
products: "movethewheels_cargo_items",
|
|
7
|
+
vehicles: "movethewheels_vehicles"
|
|
8
|
+
}, t = "\n mutation StartMovethewheelsEntityExport($input: ExportMtwEntitiesInput!) {\n exportMtwEntities(input: $input) {\n id\n status\n downloadUrl\n }\n }\n", n = "\n query GetMovethewheelsExportJob($id: ID!) {\n getExportJob(id: $id) {\n id\n status\n progress\n downloadUrl\n errorMessage\n }\n }\n", r = new Set([
|
|
9
|
+
"FAILED",
|
|
10
|
+
"CANCELLED",
|
|
11
|
+
"EXPIRED"
|
|
12
|
+
]);
|
|
13
|
+
function i(e) {
|
|
14
|
+
return e.status === "COMPLETED" ? e.downloadUrl ? {
|
|
15
|
+
kind: "ready",
|
|
16
|
+
jobId: e.id,
|
|
17
|
+
downloadUrl: e.downloadUrl
|
|
18
|
+
} : {
|
|
19
|
+
kind: "failed",
|
|
20
|
+
jobId: e.id,
|
|
21
|
+
message: "The export finished without a download link."
|
|
22
|
+
} : r.has(e.status) ? {
|
|
23
|
+
kind: "failed",
|
|
24
|
+
jobId: e.id,
|
|
25
|
+
message: e.errorMessage?.trim() || `The export was ${e.status.toLowerCase()}.`
|
|
26
|
+
} : null;
|
|
27
|
+
}
|
|
28
|
+
var a = (e) => new Promise((t) => setTimeout(t, e));
|
|
29
|
+
async function o(e, t, { intervalMs: n = 2e3, timeoutMs: r = 9e4, sleep: o = a, now: s = Date.now, signal: c } = {}) {
|
|
30
|
+
let l = i(e);
|
|
31
|
+
if (l) return l;
|
|
32
|
+
let u = s() + r;
|
|
33
|
+
for (; s() < u && !(c?.aborted || (await o(n), c?.aborted));) {
|
|
34
|
+
let n = await t(e.id);
|
|
35
|
+
if (!n) return {
|
|
36
|
+
kind: "failed",
|
|
37
|
+
jobId: e.id,
|
|
38
|
+
message: "The export job could not be found."
|
|
39
|
+
};
|
|
40
|
+
let r = i(n);
|
|
41
|
+
if (r) return r;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
kind: "pending",
|
|
45
|
+
jobId: e.id
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function s(e) {
|
|
49
|
+
if (typeof document > "u") return;
|
|
50
|
+
let t = document.createElement("a");
|
|
51
|
+
t.href = e, t.rel = "noopener", document.body.appendChild(t), t.click(), t.remove();
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { n as GET_MTW_EXPORT_JOB_QUERY, e as MTW_EXPORT_ENTITY_TYPES, t as START_MTW_EXPORT_MUTATION, s as triggerUrlDownload, o as waitForExportJob };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-movethewheels",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1031.1",
|
|
4
4
|
"description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
38
38
|
"type:check": "bash scripts/build-limits.sh tsc --noEmit",
|
|
39
39
|
"validate:schema-drift": "graphql-schema-drift",
|
|
40
|
-
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:pick-review-ack && bun run verify:driver-license-expiry && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:freight-commercial && bun run verify:freight-invoice && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:free-time-permissions && bun run verify:core-codegen && bun run verify:order-vehicle-hold && bun run verify:pallet-preview && bun run verify:consumed-fuel-preview && bun run build && bun run build-storybook'",
|
|
40
|
+
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:pick-review-ack && bun run verify:driver-license-expiry && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:freight-commercial && bun run verify:freight-invoice && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:free-time-permissions && bun run verify:core-codegen && bun run verify:order-vehicle-hold && bun run verify:pallet-preview && bun run verify:entity-export && bun run verify:consumed-fuel-preview && bun run build && bun run build-storybook'",
|
|
41
41
|
"storybook": "storybook dev -p 6006",
|
|
42
42
|
"build-storybook": "storybook build",
|
|
43
43
|
"test:storybook": "vitest run --project=storybook",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"verify:freight-invoice": "bun test scripts/verify-invoice-comparison.test.ts && bun scripts/verify-invoice-recovery-hook.mjs",
|
|
72
72
|
"verify:driver-license-expiry": "bun test scripts/check-driver-license-expiry.test.ts",
|
|
73
73
|
"verify:core-codegen": "bun test scripts/codegen.test.ts && node --test scripts/check-package-types.test.mjs",
|
|
74
|
+
"verify:entity-export": "bun test scripts/check-entity-export.test.ts",
|
|
74
75
|
"verify:pallet-preview": "bun test scripts/check-pallet-preview.test.ts && bun scripts/verify-pallet-preview-hook.mjs",
|
|
75
76
|
"verify:consumed-fuel-preview": "bun test scripts/check-consumed-fuel-preview.test.ts && bun scripts/verify-consumed-fuel-preview-hook.mjs && bun scripts/verify-consumed-fuel-preview-component.mjs"
|
|
76
77
|
},
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExportStatusBanner — inline feedback for a list page's "Export" action.
|
|
3
|
+
*
|
|
4
|
+
* The MFE does not mount its own toaster, so export progress, success and
|
|
5
|
+
* failure are shown inline. A job that outlives the wait budget links to the
|
|
6
|
+
* host shell's shared Exports page, where it can be downloaded once ready.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AlertCircle, CheckCircle, Info, Loader2, X } from 'lucide-react';
|
|
10
|
+
import type { EntityExportState } from '../hooks/useEntityExport';
|
|
11
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
12
|
+
|
|
13
|
+
interface ExportStatusBannerProps {
|
|
14
|
+
state: EntityExportState;
|
|
15
|
+
onDismiss: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Host-shell route of the shared export module (microfe-export). */
|
|
19
|
+
const EXPORTS_PATH = '/export';
|
|
20
|
+
|
|
21
|
+
export function ExportStatusBanner({ state, onDismiss }: ExportStatusBannerProps) {
|
|
22
|
+
const { navigate } = useMoveTheWheels();
|
|
23
|
+
if (state.phase === 'idle') return null;
|
|
24
|
+
|
|
25
|
+
const tone =
|
|
26
|
+
state.phase === 'error'
|
|
27
|
+
? 'border-status-error-border bg-status-error-bg-subtle text-status-error-text'
|
|
28
|
+
: state.phase === 'ready'
|
|
29
|
+
? 'border-status-success-border bg-status-success-bg-subtle text-status-success-text'
|
|
30
|
+
: 'border-status-info-border bg-status-info-bg-subtle text-status-info-text';
|
|
31
|
+
|
|
32
|
+
const icon =
|
|
33
|
+
state.phase === 'error' ? (
|
|
34
|
+
<AlertCircle size={18} className="mt-0.5 shrink-0" />
|
|
35
|
+
) : state.phase === 'ready' ? (
|
|
36
|
+
<CheckCircle size={18} className="mt-0.5 shrink-0" />
|
|
37
|
+
) : state.phase === 'running' ? (
|
|
38
|
+
<Loader2 size={18} className="mt-0.5 shrink-0 animate-spin" />
|
|
39
|
+
) : (
|
|
40
|
+
<Info size={18} className="mt-0.5 shrink-0" />
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
role={state.phase === 'error' ? 'alert' : 'status'}
|
|
46
|
+
data-testid="mtw-export-status"
|
|
47
|
+
data-phase={state.phase}
|
|
48
|
+
className={`flex items-start gap-2 rounded-lg border px-4 py-3 text-sm ${tone}`}
|
|
49
|
+
>
|
|
50
|
+
{icon}
|
|
51
|
+
<span className="flex-1">
|
|
52
|
+
{state.message}
|
|
53
|
+
{state.phase === 'pending' ? (
|
|
54
|
+
<>
|
|
55
|
+
{' '}
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
className="font-medium underline underline-offset-2"
|
|
59
|
+
onClick={() => navigate(`${EXPORTS_PATH}/${state.jobId}`)}
|
|
60
|
+
>
|
|
61
|
+
Open Exports
|
|
62
|
+
</button>
|
|
63
|
+
</>
|
|
64
|
+
) : null}
|
|
65
|
+
</span>
|
|
66
|
+
{state.phase !== 'running' ? (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
aria-label="Dismiss export status"
|
|
70
|
+
className="shrink-0 opacity-70 hover:opacity-100"
|
|
71
|
+
onClick={onDismiss}
|
|
72
|
+
>
|
|
73
|
+
<X size={16} />
|
|
74
|
+
</button>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
3
|
+
import { executeGraphQL } from '../utils/graphqlClient';
|
|
4
|
+
import {
|
|
5
|
+
GET_MTW_EXPORT_JOB_QUERY,
|
|
6
|
+
MTW_EXPORT_ENTITY_TYPES,
|
|
7
|
+
START_MTW_EXPORT_MUTATION,
|
|
8
|
+
triggerUrlDownload,
|
|
9
|
+
waitForExportJob,
|
|
10
|
+
type ExportJobSnapshot,
|
|
11
|
+
type MtwExportEntity,
|
|
12
|
+
} from '../utils/entityExport';
|
|
13
|
+
|
|
14
|
+
export type EntityExportState =
|
|
15
|
+
| { phase: 'idle' }
|
|
16
|
+
| { phase: 'running'; message: string }
|
|
17
|
+
| { phase: 'ready'; message: string }
|
|
18
|
+
| { phase: 'pending'; message: string; jobId: string }
|
|
19
|
+
| { phase: 'error'; message: string };
|
|
20
|
+
|
|
21
|
+
export interface UseEntityExportResult {
|
|
22
|
+
state: EntityExportState;
|
|
23
|
+
isExporting: boolean;
|
|
24
|
+
/** Start a CSV export of every record of this entity in the workspace. */
|
|
25
|
+
startExport: () => void;
|
|
26
|
+
dismiss: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drives the list-page "Export" action: starts an `exportMtwEntities` job for
|
|
31
|
+
* the page's entity type, waits for wspace-export-svc to build the archive and
|
|
32
|
+
* starts the download. A job that takes longer than the wait budget keeps
|
|
33
|
+
* running and is reported as pending, with its id, so the user can collect it
|
|
34
|
+
* from the shared Exports page.
|
|
35
|
+
*/
|
|
36
|
+
export function useEntityExport(entity: MtwExportEntity, label: string): UseEntityExportResult {
|
|
37
|
+
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
38
|
+
useMoveTheWheels();
|
|
39
|
+
const [state, setState] = useState<EntityExportState>({ phase: 'idle' });
|
|
40
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
41
|
+
const runningRef = useRef(false);
|
|
42
|
+
|
|
43
|
+
const graphQLOptions = useMemo(
|
|
44
|
+
() => ({
|
|
45
|
+
apiGatewayUrl,
|
|
46
|
+
authToken,
|
|
47
|
+
workspaceToken,
|
|
48
|
+
workspaceId,
|
|
49
|
+
tenantId,
|
|
50
|
+
actorId: currentUser?.id,
|
|
51
|
+
}),
|
|
52
|
+
[apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
useEffect(() => () => abortRef.current?.abort(), []);
|
|
56
|
+
|
|
57
|
+
const startExport = useCallback(() => {
|
|
58
|
+
// Starting a second job while one is being prepared would only duplicate work.
|
|
59
|
+
if (runningRef.current) return;
|
|
60
|
+
if (!workspaceId) {
|
|
61
|
+
setState({ phase: 'error', message: 'Select a workspace before exporting.' });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
abortRef.current?.abort();
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
abortRef.current = controller;
|
|
67
|
+
setState({ phase: 'running', message: `Preparing your ${label} export…` });
|
|
68
|
+
|
|
69
|
+
const run = async () => {
|
|
70
|
+
runningRef.current = true;
|
|
71
|
+
try {
|
|
72
|
+
const started = await executeGraphQL<{ exportMtwEntities: ExportJobSnapshot }>(
|
|
73
|
+
graphQLOptions,
|
|
74
|
+
START_MTW_EXPORT_MUTATION,
|
|
75
|
+
{
|
|
76
|
+
input: {
|
|
77
|
+
workspaceId,
|
|
78
|
+
entityTypes: [MTW_EXPORT_ENTITY_TYPES[entity]],
|
|
79
|
+
format: 'CSV',
|
|
80
|
+
includeMetadata: true,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
// Starting a job is not idempotent: never replay it on a transient error.
|
|
84
|
+
'SINGLE_ATTEMPT'
|
|
85
|
+
);
|
|
86
|
+
const outcome = await waitForExportJob(
|
|
87
|
+
started.exportMtwEntities,
|
|
88
|
+
async (id) => {
|
|
89
|
+
const data = await executeGraphQL<{ getExportJob: ExportJobSnapshot | null }>(
|
|
90
|
+
{ ...graphQLOptions, signal: controller.signal },
|
|
91
|
+
GET_MTW_EXPORT_JOB_QUERY,
|
|
92
|
+
{ id }
|
|
93
|
+
);
|
|
94
|
+
return data.getExportJob;
|
|
95
|
+
},
|
|
96
|
+
{ signal: controller.signal }
|
|
97
|
+
);
|
|
98
|
+
if (controller.signal.aborted) return;
|
|
99
|
+
if (outcome.kind === 'ready') {
|
|
100
|
+
triggerUrlDownload(outcome.downloadUrl);
|
|
101
|
+
setState({ phase: 'ready', message: `Your ${label} export is downloading.` });
|
|
102
|
+
} else if (outcome.kind === 'pending') {
|
|
103
|
+
setState({
|
|
104
|
+
phase: 'pending',
|
|
105
|
+
jobId: outcome.jobId,
|
|
106
|
+
message: `Your ${label} export is still being prepared. It will be available on the Exports page when it finishes.`,
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
setState({ phase: 'error', message: `Export failed: ${outcome.message}` });
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (controller.signal.aborted) return;
|
|
113
|
+
setState({
|
|
114
|
+
phase: 'error',
|
|
115
|
+
message:
|
|
116
|
+
error instanceof Error && error.message
|
|
117
|
+
? `Export failed: ${error.message}`
|
|
118
|
+
: 'Export failed. Please try again.',
|
|
119
|
+
});
|
|
120
|
+
} finally {
|
|
121
|
+
runningRef.current = false;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
void run();
|
|
125
|
+
}, [entity, graphQLOptions, label, workspaceId]);
|
|
126
|
+
|
|
127
|
+
const dismiss = useCallback(() => setState({ phase: 'idle' }), []);
|
|
128
|
+
|
|
129
|
+
return { state, isExporting: state.phase === 'running', startExport, dismiss };
|
|
130
|
+
}
|
|
@@ -15,10 +15,18 @@ import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
|
15
15
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
16
16
|
import { formatDate } from '../utils/formatters';
|
|
17
17
|
import { joinPath } from '../utils/navigation';
|
|
18
|
+
import { useEntityExport } from '../hooks/useEntityExport';
|
|
19
|
+
import { ExportStatusBanner } from '../components/ExportStatusBanner';
|
|
18
20
|
import type { Customer } from '../types';
|
|
19
21
|
|
|
20
22
|
export default function CustomersPage() {
|
|
21
23
|
const { basePath, navigate } = useMoveTheWheels();
|
|
24
|
+
const {
|
|
25
|
+
state: exportState,
|
|
26
|
+
isExporting,
|
|
27
|
+
startExport,
|
|
28
|
+
dismiss: dismissExport,
|
|
29
|
+
} = useEntityExport('customers', 'customer');
|
|
22
30
|
const [page, setPage] = useState(0);
|
|
23
31
|
const [pageSize, setPageSize] = useState(25);
|
|
24
32
|
const [search, setSearch] = useState('');
|
|
@@ -104,9 +112,9 @@ export default function CustomersPage() {
|
|
|
104
112
|
actions={[
|
|
105
113
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
106
114
|
{
|
|
107
|
-
label: 'Export',
|
|
115
|
+
label: isExporting ? 'Exporting…' : 'Export',
|
|
108
116
|
icon: <Download size={16} />,
|
|
109
|
-
onSelect:
|
|
117
|
+
onSelect: startExport,
|
|
110
118
|
},
|
|
111
119
|
]}
|
|
112
120
|
/>
|
|
@@ -117,6 +125,7 @@ export default function CustomersPage() {
|
|
|
117
125
|
satisfaction ratings here, and open a customer to see their order history before booking the
|
|
118
126
|
next shipment.
|
|
119
127
|
</PagePurpose>
|
|
128
|
+
<ExportStatusBanner state={exportState} onDismiss={dismissExport} />
|
|
120
129
|
|
|
121
130
|
{/* Customer base at a glance — premium stat cards */}
|
|
122
131
|
<div>
|