@burdenoff/microfe-movethewheels 2026.1031.2 → 2026.1031.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/MoveTheWheelsRoot.js +144 -136
  2. package/dist/MoveTheWheelsRoutes.js +61 -57
  3. package/dist/generated/tracking-quality-operations.d.ts +84 -0
  4. package/dist/hooks/useEntityImport.js +68 -0
  5. package/dist/movethewheels/hooks/useEntityImport.d.ts +19 -0
  6. package/dist/movethewheels/pages/ImportExportPage.d.ts +9 -0
  7. package/dist/movethewheels/utils/entityImport.d.ts +109 -0
  8. package/dist/node_modules/lucide-react/dist/esm/icons/arrow-down-up.js +22 -0
  9. package/dist/node_modules/lucide-react/dist/esm/icons/file-spreadsheet.js +30 -0
  10. package/dist/node_modules/lucide-react/dist/esm/icons/file-up.js +22 -0
  11. package/dist/pages/CustomersPage.js +88 -79
  12. package/dist/pages/DriversPage.js +115 -106
  13. package/dist/pages/ImportExportPage.js +435 -0
  14. package/dist/pages/ProductsPage.js +85 -76
  15. package/dist/pages/VehiclesPage.js +99 -90
  16. package/dist/pages/WarehousesPage.js +97 -88
  17. package/dist/utils/entityImport.js +73 -0
  18. package/package.json +3 -2
  19. package/src/generated/tracking-quality-operations.ts +122 -0
  20. package/src/movethewheels/MoveTheWheelsRoot.tsx +8 -0
  21. package/src/movethewheels/MoveTheWheelsRoutes.tsx +4 -0
  22. package/src/movethewheels/hooks/useEntityImport.ts +108 -0
  23. package/src/movethewheels/pages/CustomersPage.tsx +6 -1
  24. package/src/movethewheels/pages/DriversPage.tsx +6 -0
  25. package/src/movethewheels/pages/ImportExportPage.tsx +483 -0
  26. package/src/movethewheels/pages/ProductsPage.tsx +14 -1
  27. package/src/movethewheels/pages/VehiclesPage.tsx +6 -0
  28. package/src/movethewheels/pages/WarehousesPage.tsx +6 -1
  29. package/src/movethewheels/utils/entityImport.ts +239 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useState } from 'react';
6
6
  import { useTr } from '../hooks';
7
- import { Warehouse, Plus, RefreshCw, CircleCheck, Gauge, Filter } from 'lucide-react';
7
+ import { Warehouse, Plus, RefreshCw, CircleCheck, Gauge, Filter, Upload } from 'lucide-react';
8
8
  import { PageLayout } from '../components/PageLayout';
9
9
  import { MetricCard, MetricGrid } from '../components/MetricCard';
10
10
  import { DataTable, type Column } from '../components/DataTable';
@@ -154,6 +154,11 @@ export default function WarehousesPage() {
154
154
  onSelect: () => goTo('inventory/warehouses/pallet-preview'),
155
155
  },
156
156
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
157
+ {
158
+ label: 'Import',
159
+ icon: <Upload size={16} />,
160
+ onSelect: () => goTo('import-export?entity=WAREHOUSE'),
161
+ },
157
162
  ]}
158
163
  />
159
164
  }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Entity import — client side of wspace-movethewheels-svc's bulk import (BOFF-3990).
3
+ *
4
+ * The service owns parsing and validation; this module only picks the right
5
+ * mutation for an entity, detects the file format, builds CSV templates from
6
+ * the server's column specification and shapes the result for display.
7
+ * Kept free of React so the contract can be unit-tested.
8
+ */
9
+
10
+ export const MTW_IMPORT_ENTITIES = [
11
+ { entityType: 'CUSTOMER', label: 'Customers', noun: 'customer', field: 'importMtwCustomers' },
12
+ { entityType: 'VEHICLE', label: 'Vehicles', noun: 'vehicle', field: 'importMtwVehicles' },
13
+ { entityType: 'DRIVER', label: 'Drivers', noun: 'driver', field: 'importMtwDrivers' },
14
+ { entityType: 'WAREHOUSE', label: 'Warehouses', noun: 'warehouse', field: 'importMtwWarehouses' },
15
+ { entityType: 'CARGO_ITEM', label: 'Products', noun: 'product', field: 'importMtwCargoItems' },
16
+ ] as const;
17
+
18
+ export type MtwImportEntityType = (typeof MTW_IMPORT_ENTITIES)[number]['entityType'];
19
+ export type MtwImportFormat = 'CSV' | 'JSON';
20
+ export type MtwImportStatus = 'DRY_RUN' | 'COMPLETED' | 'PARTIAL' | 'REJECTED';
21
+
22
+ /** Mirrors the service limits so an oversized file is refused before upload. */
23
+ export const MTW_IMPORT_MAX_BYTES = 2 * 1024 * 1024;
24
+ export const MTW_IMPORT_MAX_ROWS = 1000;
25
+
26
+ export function isImportEntityType(value: string | null | undefined): value is MtwImportEntityType {
27
+ return MTW_IMPORT_ENTITIES.some((entity) => entity.entityType === value);
28
+ }
29
+
30
+ export function importEntity(entityType: MtwImportEntityType) {
31
+ const entity = MTW_IMPORT_ENTITIES.find((candidate) => candidate.entityType === entityType);
32
+ if (!entity) throw new Error(`Unknown import entity ${entityType}`);
33
+ return entity;
34
+ }
35
+
36
+ // One static document per entity so graphql-schema-drift can validate each
37
+ // against the subgraph schema; the selection sets are identical.
38
+ export const IMPORT_MUTATIONS: Record<MtwImportEntityType, string> = {
39
+ CUSTOMER: `
40
+ mutation ImportMtwCustomers($input: MtwImportInput!) {
41
+ importMtwCustomers(input: $input) {
42
+ entityType
43
+ status
44
+ dryRun
45
+ totalRows
46
+ validRows
47
+ importedRows
48
+ failedRows
49
+ createdIds
50
+ errorsTruncated
51
+ errors {
52
+ row
53
+ field
54
+ message
55
+ }
56
+ }
57
+ }
58
+ `,
59
+ VEHICLE: `
60
+ mutation ImportMtwVehicles($input: MtwImportInput!) {
61
+ importMtwVehicles(input: $input) {
62
+ entityType
63
+ status
64
+ dryRun
65
+ totalRows
66
+ validRows
67
+ importedRows
68
+ failedRows
69
+ createdIds
70
+ errorsTruncated
71
+ errors {
72
+ row
73
+ field
74
+ message
75
+ }
76
+ }
77
+ }
78
+ `,
79
+ DRIVER: `
80
+ mutation ImportMtwDrivers($input: MtwImportInput!) {
81
+ importMtwDrivers(input: $input) {
82
+ entityType
83
+ status
84
+ dryRun
85
+ totalRows
86
+ validRows
87
+ importedRows
88
+ failedRows
89
+ createdIds
90
+ errorsTruncated
91
+ errors {
92
+ row
93
+ field
94
+ message
95
+ }
96
+ }
97
+ }
98
+ `,
99
+ WAREHOUSE: `
100
+ mutation ImportMtwWarehouses($input: MtwImportInput!) {
101
+ importMtwWarehouses(input: $input) {
102
+ entityType
103
+ status
104
+ dryRun
105
+ totalRows
106
+ validRows
107
+ importedRows
108
+ failedRows
109
+ createdIds
110
+ errorsTruncated
111
+ errors {
112
+ row
113
+ field
114
+ message
115
+ }
116
+ }
117
+ }
118
+ `,
119
+ CARGO_ITEM: `
120
+ mutation ImportMtwCargoItems($input: MtwImportInput!) {
121
+ importMtwCargoItems(input: $input) {
122
+ entityType
123
+ status
124
+ dryRun
125
+ totalRows
126
+ validRows
127
+ importedRows
128
+ failedRows
129
+ createdIds
130
+ errorsTruncated
131
+ errors {
132
+ row
133
+ field
134
+ message
135
+ }
136
+ }
137
+ }
138
+ `,
139
+ };
140
+
141
+ /** The mutation document for one entity's import. */
142
+ export function importMutation(entityType: MtwImportEntityType): string {
143
+ return IMPORT_MUTATIONS[importEntity(entityType).entityType];
144
+ }
145
+
146
+ export const MTW_IMPORT_SPEC_QUERY = `
147
+ query MtwImportSpec {
148
+ mtwImportSpec {
149
+ entityType
150
+ keyColumn
151
+ columns {
152
+ name
153
+ type
154
+ required
155
+ description
156
+ allowedValues
157
+ maxLength
158
+ min
159
+ max
160
+ }
161
+ }
162
+ }
163
+ `;
164
+
165
+ export interface MtwImportColumn {
166
+ name: string;
167
+ type: string;
168
+ required: boolean;
169
+ description: string;
170
+ allowedValues: string[] | null;
171
+ maxLength: number | null;
172
+ min: number | null;
173
+ max: number | null;
174
+ }
175
+
176
+ export interface MtwImportEntitySpec {
177
+ entityType: MtwImportEntityType;
178
+ keyColumn: string | null;
179
+ columns: MtwImportColumn[];
180
+ }
181
+
182
+ export interface MtwImportRowError {
183
+ row: number;
184
+ field: string | null;
185
+ message: string;
186
+ }
187
+
188
+ export interface MtwImportResult {
189
+ entityType: MtwImportEntityType;
190
+ status: MtwImportStatus;
191
+ dryRun: boolean;
192
+ totalRows: number;
193
+ validRows: number;
194
+ importedRows: number;
195
+ failedRows: number;
196
+ createdIds: string[];
197
+ errorsTruncated: boolean;
198
+ errors: MtwImportRowError[];
199
+ }
200
+
201
+ /** Picks the format from the file name, falling back to the MIME type. */
202
+ export function detectImportFormat(fileName: string, mimeType = ''): MtwImportFormat | null {
203
+ const lower = fileName.toLowerCase();
204
+ if (lower.endsWith('.csv')) return 'CSV';
205
+ if (lower.endsWith('.json')) return 'JSON';
206
+ if (mimeType.includes('csv')) return 'CSV';
207
+ if (mimeType.includes('json')) return 'JSON';
208
+ return null;
209
+ }
210
+
211
+ function csvCell(value: string): string {
212
+ return /[",\r\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
213
+ }
214
+
215
+ /** A CSV template: the header row, required columns first, then optional ones. */
216
+ export function buildCsvTemplate(spec: MtwImportEntitySpec): string {
217
+ const ordered = [
218
+ ...spec.columns.filter((column) => column.required),
219
+ ...spec.columns.filter((column) => !column.required),
220
+ ];
221
+ return `${ordered.map((column) => csvCell(column.name)).join(',')}\n`;
222
+ }
223
+
224
+ /** One-line summary of an import result for the status banner. */
225
+ export function summariseImport(result: MtwImportResult, noun: string): string {
226
+ const rows = (count: number) => `${count} ${noun}${count === 1 ? '' : 's'}`;
227
+ switch (result.status) {
228
+ case 'DRY_RUN':
229
+ return result.failedRows === 0
230
+ ? `All ${rows(result.totalRows)} are valid. Nothing was imported yet.`
231
+ : `${result.validRows} of ${rows(result.totalRows)} are valid; ${result.failedRows} need fixing. Nothing was imported.`;
232
+ case 'COMPLETED':
233
+ return `Imported ${rows(result.importedRows)}.`;
234
+ case 'PARTIAL':
235
+ return `Imported ${rows(result.importedRows)}; ${result.failedRows} row${result.failedRows === 1 ? '' : 's'} failed.`;
236
+ case 'REJECTED':
237
+ return `Nothing was imported: ${result.failedRows} of ${result.totalRows} rows have problems. Fix them, or allow a partial import.`;
238
+ }
239
+ }