@burdenoff/microfe-movethewheels 2026.626.2 → 2026.703.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 (120) hide show
  1. package/dist/{AIAssistantPage-AvsMbunM.js → AIAssistantPage-C3ovZ0Fh.js} +100 -73
  2. package/dist/AnalyticsPage-O8SpY-V4.js +215 -0
  3. package/dist/{CreateOrderPage-CjlnEtk3.js → CreateOrderPage-CB1_-_mN.js} +33 -35
  4. package/dist/{CustomerDetailsPage-I6cdDHUi.js → CustomerDetailsPage-BaeKEI1D.js} +42 -44
  5. package/dist/{CustomersPage-CGGfD4ki.js → CustomersPage-DRaLuHMC.js} +26 -28
  6. package/dist/{DashboardPage-D2HRqwg9.js → DashboardPage-DPjhNfKB.js} +85 -83
  7. package/dist/{DataTable-CD9ZhpLB.js → DataTable-DmciIKKM.js} +3 -3
  8. package/dist/{DriverDetailsPage-rg_pGNuP.js → DriverDetailsPage-CSSQTOzn.js} +52 -54
  9. package/dist/{DriversPage-DKiynISH.js → DriversPage-C2Bk0w4Q.js} +24 -26
  10. package/dist/{FinancePage-Vg8WPs38.js → FinancePage-lmp0Sdsw.js} +9 -9
  11. package/dist/FleetPage-DZn5pIqd.js +365 -0
  12. package/dist/ImportExportPage-BIUJZc7O.js +290 -0
  13. package/dist/InventoryPage-Cxh1zYJd.js +253 -0
  14. package/dist/{LiveTrackingPage-DQ1q4Id3.js → LiveTrackingPage-BY1yAkIt.js} +60 -56
  15. package/dist/MarketplacePage-RMuL17EA.js +247 -0
  16. package/dist/{MetricCard-B7yMyvMX.js → MetricCard-7aYgzZHK.js} +1 -1
  17. package/dist/{OrderDetailsPage-CFASgsYX.js → OrderDetailsPage-C2Mf4KSy.js} +83 -85
  18. package/dist/{OrdersListPage-BHeT8S5o.js → OrdersListPage-BrPBtnhp.js} +31 -33
  19. package/dist/{PageLayout-D3N8MCTm.js → PageLayout-D4LDHJJq.js} +483 -422
  20. package/dist/{ProductDetailsPage-CFGcbaHo.js → ProductDetailsPage-CezzzZcj.js} +27 -29
  21. package/dist/{ProductsPage--xEpbQ8A.js → ProductsPage-I5DilIoY.js} +21 -23
  22. package/dist/ReportsPage-BDXNmUng.js +284 -0
  23. package/dist/RouteDetailsPage-BcomL20M.js +263 -0
  24. package/dist/RoutesPage-lqX9ylHP.js +144 -0
  25. package/dist/SettingsPage-CQVIOaNs.js +421 -0
  26. package/dist/StatusBadge-NpTeHVlw.js +152 -0
  27. package/dist/{TrackingPage-CcRc_dva.js → TrackingPage-BXrjz7V8.js} +59 -61
  28. package/dist/{VehicleDetailsPage-3YDMPsPs.js → VehicleDetailsPage-DZjLcJgr.js} +40 -42
  29. package/dist/{VehiclesPage-UlxPWP9K.js → VehiclesPage-6W39XT5W.js} +19 -21
  30. package/dist/{WarehouseDetailsPage-Db7KeN-d.js → WarehouseDetailsPage-C10pgRGW.js} +38 -40
  31. package/dist/{WarehousesPage-CuTO-OPp.js → WarehousesPage-CHvBqNaQ.js} +15 -17
  32. package/dist/{arrow-left-CBpZKkpq.js → arrow-left-n48Y-Wis.js} +1 -1
  33. package/dist/{box-CAaiElsb.js → box-D9Pal4HM.js} +1 -1
  34. package/dist/{chart-column-D1Ym15-S.js → chart-column-BFIJysDR.js} +1 -1
  35. package/dist/{chevron-right-BIFGi0UA.js → chevron-right-EMIIyyJ1.js} +1 -1
  36. package/dist/{circle-alert-DjmNo5_L.js → circle-alert-iY24lSlp.js} +1 -1
  37. package/dist/{circle-check-big-CjuPVo3_.js → circle-check-big-w0TrLKMF.js} +1 -1
  38. package/dist/{clock-BjkdnddD.js → clock-DJZtdl8u.js} +1 -1
  39. package/dist/{dollar-sign-IB4O0wuQ.js → dollar-sign-CgKQRz_e.js} +1 -1
  40. package/dist/{download-Dpa9vqoG.js → download-DRXxoiQR.js} +1 -1
  41. package/dist/{file-text-C8Uioy11.js → file-text-DWWY6_f5.js} +1 -1
  42. package/dist/{filter-Da5OZaHI.js → filter-DloLC2eE.js} +1 -1
  43. package/dist/index.js +119 -111
  44. package/dist/{map-_Per4sT9.js → map-CkJaULb2.js} +1 -1
  45. package/dist/{map-pin-BSaf5IDL.js → map-pin-BGiCvyCs.js} +1 -1
  46. package/dist/{navigation-DaTbrhw2.js → navigation-D36prnAn.js} +1 -1
  47. package/dist/{package-BE7U0wQE.js → package-BChUzpJS.js} +1 -1
  48. package/dist/{phone-DV4StZiA.js → phone-Ac9s3mo_.js} +1 -1
  49. package/dist/{plus-Dl1rLeJR.js → plus-Z76q-FZm.js} +1 -1
  50. package/dist/{refresh-cw-CWfERAit.js → refresh-cw-DKt6f5T6.js} +1 -1
  51. package/dist/{save-XH8vydOn.js → save-CojywNhG.js} +1 -1
  52. package/dist/{search-OxoIiL_C.js → search-CDGefqb3.js} +1 -1
  53. package/dist/{square-pen-hLIbFQFV.js → square-pen-CopE9GmB.js} +1 -1
  54. package/dist/{star-YsVV3MNQ.js → star-nHlLjDKR.js} +1 -1
  55. package/dist/{store-RCBl7HB-.js → store-BR9Voi5E.js} +1 -1
  56. package/dist/{trending-up-C-85AwGT.js → trending-up-B7W-GL6P.js} +1 -1
  57. package/dist/{triangle-alert-BOhfGgmh.js → triangle-alert-BonHFFNz.js} +1 -1
  58. package/dist/{truck-CtDjNFbi.js → truck-DrlfSpRC.js} +1 -1
  59. package/dist/{useAnalytics-ph7eTIK6.js → useAnalytics-CgdzUfY_.js} +40 -40
  60. package/dist/useAssistant-D4v3oxD3.js +84 -0
  61. package/dist/{useCustomers-bS3a4ytk.js → useCustomers-QrlozybF.js} +24 -24
  62. package/dist/{useFleet-BdETplNE.js → useFleet-DiAC-IiH.js} +70 -67
  63. package/dist/useImportExport-BLMnAKCC.js +55 -0
  64. package/dist/{useInventory-Dwn18FPz.js → useInventory-DKoks37x.js} +33 -33
  65. package/dist/{useOrders-D_3_hGMp.js → useOrders-D3UZVy8Y.js} +42 -42
  66. package/dist/useReports-Puq4DCH9.js +78 -0
  67. package/dist/{useRoutes-v4aBaS-E.js → useRoutes-Do57LvRb.js} +31 -31
  68. package/dist/useSettings-CkDH_-gM.js +81 -0
  69. package/dist/{useTracking-De2KIUNu.js → useTracking-GJify4HN.js} +22 -22
  70. package/dist/{user-Dz5TJZFs.js → user-CjSoudeu.js} +1 -1
  71. package/dist/{users-g2N0eL-Z.js → users-BAM4gHTn.js} +1 -1
  72. package/dist/{warehouse-D3WWDeTX.js → warehouse-C7MSr47B.js} +1 -1
  73. package/dist/{wrench-CmxY1n4I.js → wrench-CANXWxF-.js} +1 -1
  74. package/package.json +1 -1
  75. package/src/movethewheels/MoveTheWheelsRoutes.tsx +27 -3
  76. package/src/movethewheels/components/StatusBadge.tsx +18 -37
  77. package/src/movethewheels/constants/index.ts +67 -26
  78. package/src/movethewheels/hooks/index.ts +12 -0
  79. package/src/movethewheels/hooks/useAnalytics.ts +20 -6
  80. package/src/movethewheels/hooks/useAssistant.ts +74 -0
  81. package/src/movethewheels/hooks/useFleet.ts +63 -59
  82. package/src/movethewheels/hooks/useImportExport.ts +101 -0
  83. package/src/movethewheels/hooks/useReports.ts +158 -0
  84. package/src/movethewheels/hooks/useSettings.ts +120 -0
  85. package/src/movethewheels/pages/AIAssistantPage.tsx +53 -15
  86. package/src/movethewheels/pages/AnalyticsPage.tsx +67 -64
  87. package/src/movethewheels/pages/CustomerDetailsPage.tsx +1 -1
  88. package/src/movethewheels/pages/CustomersPage.tsx +1 -1
  89. package/src/movethewheels/pages/DashboardPage.tsx +42 -22
  90. package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
  91. package/src/movethewheels/pages/DriversPage.tsx +1 -1
  92. package/src/movethewheels/pages/FleetPage.tsx +96 -38
  93. package/src/movethewheels/pages/ImportExportPage.tsx +174 -68
  94. package/src/movethewheels/pages/InventoryPage.tsx +66 -28
  95. package/src/movethewheels/pages/LiveTrackingPage.tsx +12 -3
  96. package/src/movethewheels/pages/MarketplacePage.tsx +87 -45
  97. package/src/movethewheels/pages/OrderDetailsPage.tsx +2 -2
  98. package/src/movethewheels/pages/ReportsPage.tsx +194 -90
  99. package/src/movethewheels/pages/RouteDetailsPage.tsx +5 -11
  100. package/src/movethewheels/pages/RoutesPage.tsx +4 -9
  101. package/src/movethewheels/pages/SettingsPage.tsx +234 -56
  102. package/src/movethewheels/pages/TrackingPage.tsx +6 -6
  103. package/src/movethewheels/providers/MoveTheWheelsProvider.tsx +7 -6
  104. package/src/movethewheels/store/movethewheelsStore.ts +1 -1
  105. package/dist/AnalyticsPage-CCLfHgQZ.js +0 -212
  106. package/dist/FleetPage-DddXt8r8.js +0 -332
  107. package/dist/ImportExportPage-BW6qO3kc.js +0 -248
  108. package/dist/InventoryPage-rMGuaJN-.js +0 -222
  109. package/dist/MarketplacePage-DKrXAv62.js +0 -219
  110. package/dist/ReportsPage-Bb7_90rK.js +0 -235
  111. package/dist/RouteDetailsPage-CVtDVb1N.js +0 -282
  112. package/dist/RoutesPage-s57PtgcQ.js +0 -163
  113. package/dist/SettingsPage-BYNCBOpN.js +0 -298
  114. package/dist/StatusBadge-CijYQXtb.js +0 -209
  115. package/dist/graphqlClient-CdJyR_ed.js +0 -55
  116. package/dist/navigation-BgnOfsVd.js +0 -6
  117. package/dist/route-Dp9JiN_E.js +0 -51
  118. package/dist/settings-3Rw5NwFF.js +0 -28
  119. package/src/movethewheels/constants/mockData.ts +0 -1342
  120. /package/dist/{formatters-_vJlC-47.js → formatters-DhKZ-eA1.js} +0 -0
@@ -1,68 +1,151 @@
1
1
  /**
2
2
  * Import/Export Page
3
+ *
4
+ * Bulk data in/out wired to the backend exportData / importData mutations.
5
+ * Activity is session-local (reflects actions the operator actually ran) — no
6
+ * fabricated history is shown; the empty state renders until something happens.
3
7
  */
4
8
 
5
- import { Upload, Download, FileText, FileSpreadsheet, Check } from 'lucide-react';
9
+ import { useRef, useState } from 'react';
10
+ import { Upload, Download, FileText, FileSpreadsheet, Check, X } from 'lucide-react';
6
11
  import { PageLayout } from '../components/PageLayout';
7
12
  import { Card, CardContent, CardHeader, CardTitle, Button, Badge } from '../components/ui';
8
13
  import { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';
14
+ import { useImportExport } from '../hooks/useImportExport';
15
+
16
+ interface ImportOption {
17
+ name: string;
18
+ description: string;
19
+ resource: string;
20
+ }
21
+
22
+ interface ExportOption {
23
+ name: string;
24
+ description: string;
25
+ resource: string;
26
+ formats: string[];
27
+ }
28
+
29
+ interface ActivityEntry {
30
+ type: 'import' | 'export';
31
+ name: string;
32
+ at: Date;
33
+ success: boolean;
34
+ message: string;
35
+ }
36
+
37
+ const importOptions: ImportOption[] = [
38
+ { name: 'Orders', description: 'Import orders from CSV or JSON', resource: 'orders' },
39
+ { name: 'Customers', description: 'Import customer database', resource: 'customers' },
40
+ { name: 'Products', description: 'Import product catalog', resource: 'products' },
41
+ { name: 'Inventory', description: 'Import inventory levels', resource: 'inventory' },
42
+ ];
43
+
44
+ const exportOptions: ExportOption[] = [
45
+ {
46
+ name: 'Orders Report',
47
+ description: 'Export all orders data',
48
+ resource: 'orders',
49
+ formats: ['CSV', 'JSON'],
50
+ },
51
+ {
52
+ name: 'Financial Report',
53
+ description: 'Export revenue and payments',
54
+ resource: 'finance',
55
+ formats: ['CSV', 'JSON'],
56
+ },
57
+ {
58
+ name: 'Fleet Report',
59
+ description: 'Export vehicle and driver data',
60
+ resource: 'fleet',
61
+ formats: ['CSV', 'JSON'],
62
+ },
63
+ {
64
+ name: 'Analytics Report',
65
+ description: 'Export performance metrics',
66
+ resource: 'analytics',
67
+ formats: ['CSV', 'JSON'],
68
+ },
69
+ ];
9
70
 
10
71
  export default function ImportExportPage() {
11
- const importOptions = [
12
- {
13
- name: 'Orders',
14
- description: 'Import orders from CSV or Excel',
15
- icon: FileSpreadsheet,
16
- format: 'CSV, XLSX',
17
- },
18
- {
19
- name: 'Customers',
20
- description: 'Import customer database',
21
- icon: FileSpreadsheet,
22
- format: 'CSV, XLSX',
23
- },
24
- {
25
- name: 'Products',
26
- description: 'Import product catalog',
27
- icon: FileSpreadsheet,
28
- format: 'CSV, XLSX',
29
- },
30
- {
31
- name: 'Inventory',
32
- description: 'Import inventory levels',
33
- icon: FileSpreadsheet,
34
- format: 'CSV, XLSX',
35
- },
36
- ];
37
-
38
- const exportOptions = [
39
- {
40
- name: 'Orders Report',
41
- description: 'Export all orders data',
42
- formats: ['CSV', 'Excel', 'PDF'],
43
- },
44
- {
45
- name: 'Financial Report',
46
- description: 'Export revenue and payments',
47
- formats: ['CSV', 'Excel', 'PDF'],
48
- },
49
- {
50
- name: 'Fleet Report',
51
- description: 'Export vehicle and driver data',
52
- formats: ['CSV', 'Excel'],
53
- },
54
- {
55
- name: 'Analytics Report',
56
- description: 'Export performance metrics',
57
- formats: ['CSV', 'Excel', 'PDF'],
58
- },
59
- ];
60
-
61
- const recentActivity = [
62
- { type: 'export', name: 'Orders Report', date: '2024-01-15', status: 'completed' },
63
- { type: 'import', name: 'Customer Data', date: '2024-01-14', status: 'completed' },
64
- { type: 'export', name: 'Financial Report', date: '2024-01-13', status: 'completed' },
65
- ];
72
+ const { exportData, importData, isBusy } = useImportExport();
73
+ const [activity, setActivity] = useState<ActivityEntry[]>([]);
74
+ const [pendingResource, setPendingResource] = useState<string | null>(null);
75
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
76
+ const importTargetRef = useRef<ImportOption | null>(null);
77
+
78
+ const record = (entry: ActivityEntry) => setActivity((previous) => [entry, ...previous]);
79
+
80
+ const handleExport = async (option: ExportOption, format: string) => {
81
+ setPendingResource(`${option.resource}:${format}`);
82
+ try {
83
+ const result = await exportData(option.resource, format);
84
+ if (result.success && result.payload) {
85
+ const contents = JSON.stringify(result.payload, null, 2);
86
+ const blob = new Blob([contents], { type: 'application/json' });
87
+ const url = URL.createObjectURL(blob);
88
+ const link = document.createElement('a');
89
+ link.href = url;
90
+ link.download = `${option.resource}-export.${format.toLowerCase() === 'csv' ? 'csv' : 'json'}`;
91
+ document.body.appendChild(link);
92
+ link.click();
93
+ link.remove();
94
+ URL.revokeObjectURL(url);
95
+ }
96
+ record({
97
+ type: 'export',
98
+ name: `${option.name} (${format})`,
99
+ at: new Date(),
100
+ success: result.success,
101
+ message: result.message,
102
+ });
103
+ } catch (exportError) {
104
+ record({
105
+ type: 'export',
106
+ name: `${option.name} (${format})`,
107
+ at: new Date(),
108
+ success: false,
109
+ message: exportError instanceof Error ? exportError.message : 'Export failed',
110
+ });
111
+ } finally {
112
+ setPendingResource(null);
113
+ }
114
+ };
115
+
116
+ const triggerImport = (option: ImportOption) => {
117
+ importTargetRef.current = option;
118
+ fileInputRef.current?.click();
119
+ };
120
+
121
+ const handleFileSelected = async (event: React.ChangeEvent<HTMLInputElement>) => {
122
+ const file = event.target.files?.[0];
123
+ const option = importTargetRef.current;
124
+ event.target.value = '';
125
+ if (!file || !option) return;
126
+ setPendingResource(`import:${option.resource}`);
127
+ try {
128
+ const content = await file.text();
129
+ const result = await importData(option.resource, { filename: file.name, content });
130
+ record({
131
+ type: 'import',
132
+ name: `${option.name} — ${file.name}`,
133
+ at: new Date(),
134
+ success: result.success,
135
+ message: result.message,
136
+ });
137
+ } catch (importError) {
138
+ record({
139
+ type: 'import',
140
+ name: `${option.name} — ${file.name}`,
141
+ at: new Date(),
142
+ success: false,
143
+ message: importError instanceof Error ? importError.message : 'Import failed',
144
+ });
145
+ } finally {
146
+ setPendingResource(null);
147
+ }
148
+ };
66
149
 
67
150
  return (
68
151
  <PageLayout
@@ -71,11 +154,19 @@ export default function ImportExportPage() {
71
154
  icon={<FileText size={24} />}
72
155
  >
73
156
  <PagePurpose icon={<FileText size={18} />}>
74
- Move data in and out in bulk. Import orders, customers, products or inventory from CSV/Excel
157
+ Move data in and out in bulk. Import orders, customers, products or inventory from CSV/JSON
75
158
  to onboard or migrate without re-keying, and export any dataset for accounting, BI tools or
76
159
  backups — no manual copy-paste required.
77
160
  </PagePurpose>
78
161
 
162
+ <input
163
+ ref={fileInputRef}
164
+ type="file"
165
+ accept=".csv,.json,application/json,text/csv"
166
+ className="hidden"
167
+ onChange={handleFileSelected}
168
+ />
169
+
79
170
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
80
171
  {/* Import Data — the page's single emphasis zone */}
81
172
  <EmphasisPanel className="space-y-4">
@@ -90,7 +181,7 @@ export default function ImportExportPage() {
90
181
  >
91
182
  <div className="flex items-center gap-3">
92
183
  <div className="w-10 h-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center">
93
- <opt.icon size={20} className="text-primary" />
184
+ <FileSpreadsheet size={20} className="text-primary" />
94
185
  </div>
95
186
  <div>
96
187
  <p className="font-medium">{opt.name}</p>
@@ -98,8 +189,15 @@ export default function ImportExportPage() {
98
189
  </div>
99
190
  </div>
100
191
  <div className="text-right">
101
- <Badge variant="outline">{opt.format}</Badge>
102
- <Button variant="outline" size="sm" className="ml-2">
192
+ <Badge variant="outline">CSV, JSON</Badge>
193
+ <Button
194
+ variant="outline"
195
+ size="sm"
196
+ className="ml-2"
197
+ aria-label={`Import ${opt.name}`}
198
+ disabled={isBusy}
199
+ onClick={() => triggerImport(opt)}
200
+ >
103
201
  <Upload size={14} />
104
202
  </Button>
105
203
  </div>
@@ -126,7 +224,13 @@ export default function ImportExportPage() {
126
224
  </div>
127
225
  <div className="flex gap-1">
128
226
  {opt.formats.map((f) => (
129
- <Button key={f} variant="outline" size="sm">
227
+ <Button
228
+ key={f}
229
+ variant="outline"
230
+ size="sm"
231
+ disabled={pendingResource === `${opt.resource}:${f}`}
232
+ onClick={() => void handleExport(opt, f)}
233
+ >
130
234
  {f}
131
235
  </Button>
132
236
  ))}
@@ -142,7 +246,7 @@ export default function ImportExportPage() {
142
246
  <CardTitle>Recent Activity</CardTitle>
143
247
  </CardHeader>
144
248
  <CardContent>
145
- {recentActivity.length === 0 ? (
249
+ {activity.length === 0 ? (
146
250
  <IllustratedEmptyState
147
251
  illustration="empty-data"
148
252
  title="No import or export activity yet"
@@ -150,26 +254,28 @@ export default function ImportExportPage() {
150
254
  />
151
255
  ) : (
152
256
  <div className="space-y-3">
153
- {recentActivity.map((activity, i) => (
257
+ {activity.map((entry, i) => (
154
258
  <div
155
259
  key={i}
156
260
  className="flex items-center justify-between p-3 bg-bg-sunken/50 rounded-lg"
157
261
  >
158
262
  <div className="flex items-center gap-3">
159
- {activity.type === 'import' ? (
263
+ {entry.type === 'import' ? (
160
264
  <Upload size={16} className="text-primary" />
161
265
  ) : (
162
266
  <Download size={16} className="text-primary" />
163
267
  )}
164
268
  <div>
165
- <p className="font-medium">{activity.name}</p>
269
+ <p className="font-medium">{entry.name}</p>
166
270
  <p className="text-sm text-text-secondary">
167
- {activity.type === 'import' ? 'Imported' : 'Exported'} on {activity.date}
271
+ {entry.type === 'import' ? 'Imported' : 'Exported'} at{' '}
272
+ {entry.at.toLocaleTimeString()} — {entry.message}
168
273
  </p>
169
274
  </div>
170
275
  </div>
171
- <Badge variant="default" className="gap-1">
172
- <Check size={12} /> {activity.status}
276
+ <Badge variant={entry.success ? 'default' : 'destructive'} className="gap-1">
277
+ {entry.success ? <Check size={12} /> : <X size={12} />}
278
+ {entry.success ? 'done' : 'failed'}
173
279
  </Badge>
174
280
  </div>
175
281
  ))}
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Package, Warehouse, AlertTriangle, ChevronRight, Plus } from 'lucide-react';
6
6
  import { PageLayout, PageSection } from '../components/PageLayout';
7
- import { MetricCard, MetricGrid } from '../components/MetricCard';
7
+ import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
8
8
  import { Card, CardContent, Button, Badge, Progress } from '../components/ui';
9
9
  import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
10
10
  import { useInventory, useInventoryStats } from '../hooks/useInventory';
@@ -20,10 +20,16 @@ export default function InventoryPage() {
20
20
  return value === key ? fallback : value;
21
21
  };
22
22
  const { basePath, navigate } = useMoveTheWheels();
23
- const { inventory, products, warehouses } = useInventory();
23
+ const { inventory, products, warehouses, isLoading, error, refetch } = useInventory();
24
24
  const { stats } = useInventoryStats();
25
25
 
26
26
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
27
+ const onRowKeyDown = (e: React.KeyboardEvent, action: () => void) => {
28
+ if (e.key === 'Enter' || e.key === ' ') {
29
+ e.preventDefault();
30
+ action();
31
+ }
32
+ };
27
33
  const lowStockItems = inventory.filter((inv) => inv.availableQuantity <= inv.reorderLevel);
28
34
 
29
35
  return (
@@ -58,30 +64,50 @@ export default function InventoryPage() {
58
64
  )}
59
65
  </PagePurpose>
60
66
 
61
- <MetricGrid columns={4}>
62
- <MetricCard
63
- label={t('movethewheels.inventory.metrics.totalProducts', 'Total Products')}
64
- value={stats.totalProducts}
65
- icon={<Package size={20} />}
66
- onClick={() => goTo('inventory/products')}
67
- />
68
- <MetricCard
69
- label={t('movethewheels.inventory.metrics.totalItems', 'Total Items')}
70
- value={stats.totalItems.toLocaleString()}
71
- icon={<Package size={20} />}
72
- />
73
- <MetricCard
74
- label={t('movethewheels.inventory.metrics.inventoryValue', 'Inventory Value')}
75
- value={formatCurrency(stats.totalValue)}
76
- icon={<Package size={20} />}
77
- />
78
- <MetricCard
79
- label={t('movethewheels.inventory.metrics.warehouses', 'Warehouses')}
80
- value={stats.totalWarehouses}
81
- icon={<Warehouse size={20} />}
82
- onClick={() => goTo('inventory/warehouses')}
83
- />
84
- </MetricGrid>
67
+ {error && !isLoading ? (
68
+ <Card className="border-status-error-border">
69
+ <CardContent className="p-6 text-center space-y-3">
70
+ <p className="text-status-error-text font-medium">
71
+ {t('movethewheels.inventory.error.title', "Couldn't load inventory")}
72
+ </p>
73
+ <p className="text-sm text-text-secondary">{error.message}</p>
74
+ <Button variant="outline" onClick={refetch}>
75
+ {t('movethewheels.inventory.error.retry', 'Retry')}
76
+ </Button>
77
+ </CardContent>
78
+ </Card>
79
+ ) : isLoading ? (
80
+ <MetricGrid columns={4}>
81
+ {Array.from({ length: 4 }).map((_, i) => (
82
+ <MetricCardSkeleton key={i} />
83
+ ))}
84
+ </MetricGrid>
85
+ ) : (
86
+ <MetricGrid columns={4}>
87
+ <MetricCard
88
+ label={t('movethewheels.inventory.metrics.totalProducts', 'Total Products')}
89
+ value={stats.totalProducts}
90
+ icon={<Package size={20} />}
91
+ onClick={() => goTo('inventory/products')}
92
+ />
93
+ <MetricCard
94
+ label={t('movethewheels.inventory.metrics.totalItems', 'Total Items')}
95
+ value={stats.totalItems.toLocaleString()}
96
+ icon={<Package size={20} />}
97
+ />
98
+ <MetricCard
99
+ label={t('movethewheels.inventory.metrics.inventoryValue', 'Inventory Value')}
100
+ value={formatCurrency(stats.totalValue)}
101
+ icon={<Package size={20} />}
102
+ />
103
+ <MetricCard
104
+ label={t('movethewheels.inventory.metrics.warehouses', 'Warehouses')}
105
+ value={stats.totalWarehouses}
106
+ icon={<Warehouse size={20} />}
107
+ onClick={() => goTo('inventory/warehouses')}
108
+ />
109
+ </MetricGrid>
110
+ )}
85
111
 
86
112
  {lowStockItems.length > 0 && (
87
113
  <EmphasisPanel className="space-y-4">
@@ -141,8 +167,14 @@ export default function InventoryPage() {
141
167
  {warehouses.map((warehouse) => (
142
168
  <div
143
169
  key={warehouse.id}
144
- className="p-4 hover:bg-bg-sunken/50 cursor-pointer"
170
+ role="button"
171
+ tabIndex={0}
172
+ aria-label={`View warehouse ${warehouse.name}`}
173
+ className="p-4 hover:bg-bg-sunken/50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg"
145
174
  onClick={() => goTo(`inventory/warehouses/${warehouse.id}`)}
175
+ onKeyDown={(e) =>
176
+ onRowKeyDown(e, () => goTo(`inventory/warehouses/${warehouse.id}`))
177
+ }
146
178
  >
147
179
  <div className="flex items-center justify-between">
148
180
  <div>
@@ -191,8 +223,14 @@ export default function InventoryPage() {
191
223
  {products.slice(0, 5).map((product) => (
192
224
  <div
193
225
  key={product.id}
194
- className="p-4 hover:bg-bg-sunken/50 cursor-pointer"
226
+ role="button"
227
+ tabIndex={0}
228
+ aria-label={`View product ${product.name}`}
229
+ className="p-4 hover:bg-bg-sunken/50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg"
195
230
  onClick={() => goTo(`inventory/products/${product.id}`)}
231
+ onKeyDown={(e) =>
232
+ onRowKeyDown(e, () => goTo(`inventory/products/${product.id}`))
233
+ }
196
234
  >
197
235
  <div className="flex items-center justify-between">
198
236
  <div>
@@ -166,10 +166,19 @@ export default function LiveTrackingPage() {
166
166
  return (
167
167
  <div
168
168
  key={delivery.order.id}
169
- className={`p-4 hover:bg-bg-sunken/50 cursor-pointer transition-colors ${
169
+ role="button"
170
+ tabIndex={0}
171
+ aria-label={`Select delivery ${delivery.order.orderNumber}`}
172
+ className={`p-4 hover:bg-bg-sunken/50 cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg ${
170
173
  selectedDelivery === delivery.order.id ? 'bg-bg-sunken/50' : ''
171
174
  }`}
172
175
  onClick={() => setSelectedDelivery(delivery.order.id)}
176
+ onKeyDown={(e) => {
177
+ if ((e.key === 'Enter' || e.key === ' ') && delivery.order) {
178
+ e.preventDefault();
179
+ setSelectedDelivery(delivery.order.id);
180
+ }
181
+ }}
173
182
  >
174
183
  <div className="flex items-start justify-between">
175
184
  <div>
@@ -317,8 +326,8 @@ export default function LiveTrackingPage() {
317
326
  key={alert.id}
318
327
  className={`p-3 rounded-lg text-sm ${
319
328
  alert.severity === 'critical' || alert.severity === 'high'
320
- ? 'bg-red-50 dark:bg-red-900/20'
321
- : 'bg-yellow-50 dark:bg-yellow-900/20'
329
+ ? 'bg-status-error-bg-subtle'
330
+ : 'bg-status-warning-bg-subtle'
322
331
  }`}
323
332
  >
324
333
  <p className="font-medium">{alert.message}</p>
@@ -5,7 +5,16 @@
5
5
  import { useCallback, useEffect, useMemo, useState } from 'react';
6
6
  import { Store, Star, MapPin, Clock, Filter, Search } from 'lucide-react';
7
7
  import { PageLayout } from '../components/PageLayout';
8
- import { Card, CardContent, CardHeader, CardTitle, Button, Badge, Input } from '../components/ui';
8
+ import {
9
+ Card,
10
+ CardContent,
11
+ CardHeader,
12
+ CardTitle,
13
+ Button,
14
+ Badge,
15
+ Input,
16
+ Skeleton,
17
+ } from '../components/ui';
9
18
  import { EmphasisPanel, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';
10
19
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
11
20
  import { formatCurrency } from '../utils/formatters';
@@ -83,6 +92,8 @@ export default function MarketplacePage() {
83
92
  const [providers, setProviders] = useState<ApiServiceProvider[]>([]);
84
93
  const [offerings, setOfferings] = useState<ApiServiceOffering[]>([]);
85
94
  const [search, setSearch] = useState('');
95
+ const [isLoading, setIsLoading] = useState(false);
96
+ const [error, setError] = useState<Error | null>(null);
86
97
 
87
98
  const graphQLOptions = useMemo(
88
99
  () => ({
@@ -97,17 +108,31 @@ export default function MarketplacePage() {
97
108
  );
98
109
 
99
110
  const load = useCallback(async () => {
100
- const [providersData, offeringsData] = await Promise.all([
101
- executeGraphQL<{ serviceProviders: ApiServiceProvider[] }>(graphQLOptions, PROVIDERS_QUERY, {
102
- pagination: { limit: 200, offset: 0 },
103
- }),
104
- executeGraphQL<{ serviceOfferings: ApiServiceOffering[] }>(graphQLOptions, OFFERINGS_QUERY, {
105
- pagination: { limit: 200, offset: 0 },
106
- }),
107
- ]);
111
+ setIsLoading(true);
112
+ setError(null);
113
+ try {
114
+ const [providersData, offeringsData] = await Promise.all([
115
+ executeGraphQL<{ serviceProviders: ApiServiceProvider[] }>(
116
+ graphQLOptions,
117
+ PROVIDERS_QUERY,
118
+ { pagination: { limit: 200, offset: 0 } }
119
+ ),
120
+ executeGraphQL<{ serviceOfferings: ApiServiceOffering[] }>(
121
+ graphQLOptions,
122
+ OFFERINGS_QUERY,
123
+ {
124
+ pagination: { limit: 200, offset: 0 },
125
+ }
126
+ ),
127
+ ]);
108
128
 
109
- setProviders(providersData.serviceProviders);
110
- setOfferings(offeringsData.serviceOfferings);
129
+ setProviders(providersData.serviceProviders);
130
+ setOfferings(offeringsData.serviceOfferings);
131
+ } catch (fetchError) {
132
+ setError(fetchError instanceof Error ? fetchError : new Error('Failed to load marketplace'));
133
+ } finally {
134
+ setIsLoading(false);
135
+ }
111
136
  }, [graphQLOptions]);
112
137
 
113
138
  useEffect(() => {
@@ -173,7 +198,22 @@ export default function MarketplacePage() {
173
198
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
174
199
  <div className="lg:col-span-2 space-y-4">
175
200
  <h3 className="font-semibold">Service Providers ({filteredProviders.length})</h3>
176
- {filteredProviders.length === 0 && (
201
+ {error && !isLoading && (
202
+ <Card className="border-status-error-border">
203
+ <CardContent className="p-6 text-center space-y-3">
204
+ <p className="text-status-error-text font-medium">Couldn&apos;t load providers</p>
205
+ <p className="text-sm text-text-secondary">{error.message}</p>
206
+ <Button variant="outline" onClick={() => void load()}>
207
+ Retry
208
+ </Button>
209
+ </CardContent>
210
+ </Card>
211
+ )}
212
+ {isLoading &&
213
+ Array.from({ length: 3 }).map((_, i) => (
214
+ <Skeleton key={i} className="h-32 w-full rounded-lg" />
215
+ ))}
216
+ {!isLoading && !error && filteredProviders.length === 0 && (
177
217
  <Card>
178
218
  <CardContent className="p-0">
179
219
  <IllustratedEmptyState
@@ -188,42 +228,44 @@ export default function MarketplacePage() {
188
228
  </CardContent>
189
229
  </Card>
190
230
  )}
191
- {filteredProviders.map((provider) => (
192
- <Card key={provider.id} className="hover:shadow-md transition-shadow cursor-pointer">
193
- <CardContent className="p-6">
194
- <div className="flex items-start justify-between">
195
- <div>
196
- <div className="flex items-center gap-2">
197
- <h4 className="font-semibold text-lg">{provider.name}</h4>
198
- {provider.isVerified && <Badge variant="default">Verified</Badge>}
199
- <Badge variant={provider.isActive ? 'default' : 'secondary'}>
200
- {provider.isActive ? 'active' : 'inactive'}
201
- </Badge>
231
+ {!isLoading &&
232
+ !error &&
233
+ filteredProviders.map((provider) => (
234
+ <Card key={provider.id} className="hover:shadow-md transition-shadow">
235
+ <CardContent className="p-6">
236
+ <div className="flex items-start justify-between">
237
+ <div>
238
+ <div className="flex items-center gap-2">
239
+ <h4 className="font-semibold text-lg">{provider.name}</h4>
240
+ {provider.isVerified && <Badge variant="default">Verified</Badge>}
241
+ <Badge variant={provider.isActive ? 'default' : 'secondary'}>
242
+ {provider.isActive ? 'active' : 'inactive'}
243
+ </Badge>
244
+ </div>
245
+ <p className="text-text-secondary mt-1">{provider.description}</p>
246
+ <div className="flex items-center gap-4 mt-3 text-sm text-text-secondary">
247
+ <span className="flex items-center gap-1">
248
+ <Star size={14} className="text-status-warning-text fill-current" />{' '}
249
+ {provider.rating ?? 0}
250
+ </span>
251
+ <span className="flex items-center gap-1">
252
+ <MapPin size={14} /> {provider.type}
253
+ </span>
254
+ <span className="flex items-center gap-1">
255
+ <Clock size={14} /> Listed
256
+ </span>
257
+ </div>
202
258
  </div>
203
- <p className="text-text-secondary mt-1">{provider.description}</p>
204
- <div className="flex items-center gap-4 mt-3 text-sm text-text-secondary">
205
- <span className="flex items-center gap-1">
206
- <Star size={14} className="text-yellow-500 fill-yellow-500" />{' '}
207
- {provider.rating ?? 0}
208
- </span>
209
- <span className="flex items-center gap-1">
210
- <MapPin size={14} /> {provider.type}
211
- </span>
212
- <span className="flex items-center gap-1">
213
- <Clock size={14} /> Listed
214
- </span>
259
+ <div className="text-right">
260
+ <p className="text-sm text-text-secondary">Services</p>
261
+ <p className="text-lg font-bold">
262
+ {Array.isArray(provider.services) ? provider.services.length : 0}
263
+ </p>
215
264
  </div>
216
265
  </div>
217
- <div className="text-right">
218
- <p className="text-sm text-text-secondary">Services</p>
219
- <p className="text-lg font-bold">
220
- {Array.isArray(provider.services) ? provider.services.length : 0}
221
- </p>
222
- </div>
223
- </div>
224
- </CardContent>
225
- </Card>
226
- ))}
266
+ </CardContent>
267
+ </Card>
268
+ ))}
227
269
  </div>
228
270
 
229
271
  {/* Popular Services — the page's single emphasis zone */}