@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
|
@@ -37,6 +37,8 @@ import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
|
37
37
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
38
38
|
import { formatDate } from '../utils/formatters';
|
|
39
39
|
import { joinPath } from '../utils/navigation';
|
|
40
|
+
import { useEntityExport } from '../hooks/useEntityExport';
|
|
41
|
+
import { ExportStatusBanner } from '../components/ExportStatusBanner';
|
|
40
42
|
import { DRIVER_STATUS_LABELS } from '../constants';
|
|
41
43
|
import type { Driver, DriverStatus } from '../types';
|
|
42
44
|
|
|
@@ -52,6 +54,12 @@ const STATUS_OPTIONS: Array<{ value: DriverStatus | 'all'; label: string }> = [
|
|
|
52
54
|
|
|
53
55
|
export default function DriversPage() {
|
|
54
56
|
const { basePath, navigate } = useMoveTheWheels();
|
|
57
|
+
const {
|
|
58
|
+
state: exportState,
|
|
59
|
+
isExporting,
|
|
60
|
+
startExport,
|
|
61
|
+
dismiss: dismissExport,
|
|
62
|
+
} = useEntityExport('drivers', 'driver');
|
|
55
63
|
const [page, setPage] = useState(0);
|
|
56
64
|
const [pageSize, setPageSize] = useState(25);
|
|
57
65
|
const [search, setSearch] = useState('');
|
|
@@ -224,9 +232,9 @@ export default function DriversPage() {
|
|
|
224
232
|
actions={[
|
|
225
233
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
226
234
|
{
|
|
227
|
-
label: 'Export',
|
|
235
|
+
label: isExporting ? 'Exporting…' : 'Export',
|
|
228
236
|
icon: <Download size={16} />,
|
|
229
|
-
onSelect:
|
|
237
|
+
onSelect: startExport,
|
|
230
238
|
},
|
|
231
239
|
]}
|
|
232
240
|
/>
|
|
@@ -237,6 +245,7 @@ export default function DriversPage() {
|
|
|
237
245
|
contact number and the vehicle they are on — open a driver to review their record before
|
|
238
246
|
assigning them to an order or route.
|
|
239
247
|
</PagePurpose>
|
|
248
|
+
<ExportStatusBanner state={exportState} onDismiss={dismissExport} />
|
|
240
249
|
|
|
241
250
|
{/* Workforce at a glance — premium stat cards */}
|
|
242
251
|
<div>
|
|
@@ -39,6 +39,8 @@ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
|
39
39
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
40
40
|
import { formatCurrency, formatDate } from '../utils/formatters';
|
|
41
41
|
import { joinPath } from '../utils/navigation';
|
|
42
|
+
import { useEntityExport } from '../hooks/useEntityExport';
|
|
43
|
+
import { ExportStatusBanner } from '../components/ExportStatusBanner';
|
|
42
44
|
import type { Order, OrderStatus } from '../types';
|
|
43
45
|
|
|
44
46
|
export default function OrdersListPage() {
|
|
@@ -48,6 +50,12 @@ export default function OrdersListPage() {
|
|
|
48
50
|
return value === key ? fallback : value;
|
|
49
51
|
};
|
|
50
52
|
const { basePath, navigate } = useMoveTheWheels();
|
|
53
|
+
const {
|
|
54
|
+
state: exportState,
|
|
55
|
+
isExporting,
|
|
56
|
+
startExport,
|
|
57
|
+
dismiss: dismissExport,
|
|
58
|
+
} = useEntityExport('orders', 'order');
|
|
51
59
|
const [statusFilter, setStatusFilter] = useState<OrderStatus | 'all'>('all');
|
|
52
60
|
const [unassignedOnly, setUnassignedOnly] = useState(false);
|
|
53
61
|
const [page, setPage] = useState(0);
|
|
@@ -219,9 +227,11 @@ export default function OrdersListPage() {
|
|
|
219
227
|
onSelect: () => refetch(),
|
|
220
228
|
},
|
|
221
229
|
{
|
|
222
|
-
label:
|
|
230
|
+
label: isExporting
|
|
231
|
+
? t('movethewheels.orders.actions.exporting', 'Exporting…')
|
|
232
|
+
: t('movethewheels.orders.actions.export', 'Export'),
|
|
223
233
|
icon: <Download size={16} />,
|
|
224
|
-
onSelect:
|
|
234
|
+
onSelect: startExport,
|
|
225
235
|
},
|
|
226
236
|
]}
|
|
227
237
|
/>
|
|
@@ -233,6 +243,7 @@ export default function OrdersListPage() {
|
|
|
233
243
|
'Every delivery job lives here — their status, priority and value. Use this list to find an order, filter by status, and open one to assign a driver, track it, or update fulfilment.'
|
|
234
244
|
)}
|
|
235
245
|
</PagePurpose>
|
|
246
|
+
<ExportStatusBanner state={exportState} onDismiss={dismissExport} />
|
|
236
247
|
|
|
237
248
|
{/* Pipeline at a glance — premium stat cards */}
|
|
238
249
|
<div>
|
|
@@ -14,10 +14,18 @@ import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
|
14
14
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
15
15
|
import { formatCurrency } from '../utils/formatters';
|
|
16
16
|
import { joinPath } from '../utils/navigation';
|
|
17
|
+
import { useEntityExport } from '../hooks/useEntityExport';
|
|
18
|
+
import { ExportStatusBanner } from '../components/ExportStatusBanner';
|
|
17
19
|
import type { Product } from '../types';
|
|
18
20
|
|
|
19
21
|
export default function ProductsPage() {
|
|
20
22
|
const { basePath, navigate } = useMoveTheWheels();
|
|
23
|
+
const {
|
|
24
|
+
state: exportState,
|
|
25
|
+
isExporting,
|
|
26
|
+
startExport,
|
|
27
|
+
dismiss: dismissExport,
|
|
28
|
+
} = useEntityExport('products', 'product');
|
|
21
29
|
const [page, setPage] = useState(0);
|
|
22
30
|
const [pageSize, setPageSize] = useState(25);
|
|
23
31
|
const [search, setSearch] = useState('');
|
|
@@ -127,9 +135,9 @@ export default function ProductsPage() {
|
|
|
127
135
|
actions={[
|
|
128
136
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
129
137
|
{
|
|
130
|
-
label: 'Export',
|
|
138
|
+
label: isExporting ? 'Exporting…' : 'Export',
|
|
131
139
|
icon: <Download size={16} />,
|
|
132
|
-
onSelect:
|
|
140
|
+
onSelect: startExport,
|
|
133
141
|
},
|
|
134
142
|
]}
|
|
135
143
|
/>
|
|
@@ -140,6 +148,7 @@ export default function ProductsPage() {
|
|
|
140
148
|
flags (fragile, hazardous, temperature-controlled) drive how it's packed, priced and
|
|
141
149
|
routed — keep them accurate so shipments are quoted and handled correctly.
|
|
142
150
|
</PagePurpose>
|
|
151
|
+
<ExportStatusBanner state={exportState} onDismiss={dismissExport} />
|
|
143
152
|
|
|
144
153
|
{/* Catalog at a glance — premium stat cards */}
|
|
145
154
|
<div>
|
|
@@ -33,6 +33,8 @@ import { useVehicles, useFleetStats } from '../hooks/useFleet';
|
|
|
33
33
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
34
34
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
35
35
|
import { joinPath } from '../utils/navigation';
|
|
36
|
+
import { useEntityExport } from '../hooks/useEntityExport';
|
|
37
|
+
import { ExportStatusBanner } from '../components/ExportStatusBanner';
|
|
36
38
|
import { VEHICLE_STATUS_LABELS } from '../constants';
|
|
37
39
|
import type { Vehicle, VehicleStatus } from '../types';
|
|
38
40
|
|
|
@@ -45,6 +47,12 @@ const STATUS_OPTIONS: Array<{ value: VehicleStatus | 'all'; label: string }> = [
|
|
|
45
47
|
|
|
46
48
|
export default function VehiclesPage() {
|
|
47
49
|
const { basePath, navigate } = useMoveTheWheels();
|
|
50
|
+
const {
|
|
51
|
+
state: exportState,
|
|
52
|
+
isExporting,
|
|
53
|
+
startExport,
|
|
54
|
+
dismiss: dismissExport,
|
|
55
|
+
} = useEntityExport('vehicles', 'vehicle');
|
|
48
56
|
const [page, setPage] = useState(0);
|
|
49
57
|
const [pageSize, setPageSize] = useState(25);
|
|
50
58
|
const [search, setSearch] = useState('');
|
|
@@ -156,9 +164,9 @@ export default function VehiclesPage() {
|
|
|
156
164
|
actions={[
|
|
157
165
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
158
166
|
{
|
|
159
|
-
label: 'Export',
|
|
167
|
+
label: isExporting ? 'Exporting…' : 'Export',
|
|
160
168
|
icon: <Download size={16} />,
|
|
161
|
-
onSelect:
|
|
169
|
+
onSelect: startExport,
|
|
162
170
|
},
|
|
163
171
|
]}
|
|
164
172
|
/>
|
|
@@ -169,6 +177,7 @@ export default function VehiclesPage() {
|
|
|
169
177
|
maintenance status here so you always know which vehicles are road-ready before dispatching
|
|
170
178
|
an order.
|
|
171
179
|
</PagePurpose>
|
|
180
|
+
<ExportStatusBanner state={exportState} onDismiss={dismissExport} />
|
|
172
181
|
|
|
173
182
|
{/* Fleet at a glance — premium stat cards */}
|
|
174
183
|
<div>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity export — starts a MoveTheWheels export job and waits for its archive.
|
|
3
|
+
*
|
|
4
|
+
* `exportMtwEntities` hands the request to wspace-export-svc, which builds a
|
|
5
|
+
* ZIP of the workspace's records in the background. The job is read back with
|
|
6
|
+
* `getExportJob` until it reports COMPLETED (with a presigned `downloadUrl`),
|
|
7
|
+
* FAILED/CANCELLED/EXPIRED, or the wait budget runs out. A job that is still
|
|
8
|
+
* running when the budget is spent keeps going server side and stays
|
|
9
|
+
* downloadable from the shared Exports page.
|
|
10
|
+
*
|
|
11
|
+
* Kept free of React so the polling contract can be unit-tested.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** List pages that offer an export, mapped to wspace-export-svc entity types. */
|
|
15
|
+
export const MTW_EXPORT_ENTITY_TYPES = {
|
|
16
|
+
customers: 'movethewheels_customers',
|
|
17
|
+
drivers: 'movethewheels_drivers',
|
|
18
|
+
orders: 'movethewheels_delivery_jobs',
|
|
19
|
+
products: 'movethewheels_cargo_items',
|
|
20
|
+
vehicles: 'movethewheels_vehicles',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type MtwExportEntity = keyof typeof MTW_EXPORT_ENTITY_TYPES;
|
|
24
|
+
|
|
25
|
+
export const START_MTW_EXPORT_MUTATION = `
|
|
26
|
+
mutation StartMovethewheelsEntityExport($input: ExportMtwEntitiesInput!) {
|
|
27
|
+
exportMtwEntities(input: $input) {
|
|
28
|
+
id
|
|
29
|
+
status
|
|
30
|
+
downloadUrl
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export const GET_MTW_EXPORT_JOB_QUERY = `
|
|
36
|
+
query GetMovethewheelsExportJob($id: ID!) {
|
|
37
|
+
getExportJob(id: $id) {
|
|
38
|
+
id
|
|
39
|
+
status
|
|
40
|
+
progress
|
|
41
|
+
downloadUrl
|
|
42
|
+
errorMessage
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export interface ExportJobSnapshot {
|
|
48
|
+
id: string;
|
|
49
|
+
status: string;
|
|
50
|
+
progress?: number | null;
|
|
51
|
+
downloadUrl?: string | null;
|
|
52
|
+
errorMessage?: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ExportJobOutcome =
|
|
56
|
+
| { kind: 'ready'; jobId: string; downloadUrl: string }
|
|
57
|
+
| { kind: 'failed'; jobId: string; message: string }
|
|
58
|
+
| { kind: 'pending'; jobId: string };
|
|
59
|
+
|
|
60
|
+
const TERMINAL_FAILURES = new Set(['FAILED', 'CANCELLED', 'EXPIRED']);
|
|
61
|
+
|
|
62
|
+
/** Classify one job snapshot; `null` means keep waiting. */
|
|
63
|
+
export function classifyExportJob(job: ExportJobSnapshot): ExportJobOutcome | null {
|
|
64
|
+
if (job.status === 'COMPLETED') {
|
|
65
|
+
if (job.downloadUrl) return { kind: 'ready', jobId: job.id, downloadUrl: job.downloadUrl };
|
|
66
|
+
return {
|
|
67
|
+
kind: 'failed',
|
|
68
|
+
jobId: job.id,
|
|
69
|
+
message: 'The export finished without a download link.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (TERMINAL_FAILURES.has(job.status)) {
|
|
73
|
+
return {
|
|
74
|
+
kind: 'failed',
|
|
75
|
+
jobId: job.id,
|
|
76
|
+
message: job.errorMessage?.trim() || `The export was ${job.status.toLowerCase()}.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface WaitForExportOptions {
|
|
83
|
+
/** Delay between reads of the job. */
|
|
84
|
+
intervalMs?: number;
|
|
85
|
+
/** Total time to wait before reporting the job as still pending. */
|
|
86
|
+
timeoutMs?: number;
|
|
87
|
+
/** Injectable for tests. */
|
|
88
|
+
sleep?: (ms: number) => Promise<void>;
|
|
89
|
+
/** Injectable for tests. */
|
|
90
|
+
now?: () => number;
|
|
91
|
+
/** Stops waiting (e.g. the page unmounted); resolves as pending. */
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read the job until it settles or the wait budget is spent. `initial` is the
|
|
99
|
+
* snapshot returned by the start mutation, so an already-finished job resolves
|
|
100
|
+
* without another round trip.
|
|
101
|
+
*/
|
|
102
|
+
export async function waitForExportJob(
|
|
103
|
+
initial: ExportJobSnapshot,
|
|
104
|
+
fetchJob: (id: string) => Promise<ExportJobSnapshot | null>,
|
|
105
|
+
{
|
|
106
|
+
intervalMs = 2000,
|
|
107
|
+
timeoutMs = 90000,
|
|
108
|
+
sleep = defaultSleep,
|
|
109
|
+
now = Date.now,
|
|
110
|
+
signal,
|
|
111
|
+
}: WaitForExportOptions = {}
|
|
112
|
+
): Promise<ExportJobOutcome> {
|
|
113
|
+
const settled = classifyExportJob(initial);
|
|
114
|
+
if (settled) return settled;
|
|
115
|
+
|
|
116
|
+
const deadline = now() + timeoutMs;
|
|
117
|
+
while (now() < deadline) {
|
|
118
|
+
if (signal?.aborted) break;
|
|
119
|
+
await sleep(intervalMs);
|
|
120
|
+
if (signal?.aborted) break;
|
|
121
|
+
const job = await fetchJob(initial.id);
|
|
122
|
+
if (!job) {
|
|
123
|
+
return { kind: 'failed', jobId: initial.id, message: 'The export job could not be found.' };
|
|
124
|
+
}
|
|
125
|
+
const outcome = classifyExportJob(job);
|
|
126
|
+
if (outcome) return outcome;
|
|
127
|
+
}
|
|
128
|
+
return { kind: 'pending', jobId: initial.id };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Start the archive download in the current tab without navigating away. */
|
|
132
|
+
export function triggerUrlDownload(url: string): void {
|
|
133
|
+
if (typeof document === 'undefined') return;
|
|
134
|
+
const anchor = document.createElement('a');
|
|
135
|
+
anchor.href = url;
|
|
136
|
+
anchor.rel = 'noopener';
|
|
137
|
+
document.body.appendChild(anchor);
|
|
138
|
+
anchor.click();
|
|
139
|
+
anchor.remove();
|
|
140
|
+
}
|