@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.
- package/dist/{AIAssistantPage-AvsMbunM.js → AIAssistantPage-C3ovZ0Fh.js} +100 -73
- package/dist/AnalyticsPage-O8SpY-V4.js +215 -0
- package/dist/{CreateOrderPage-CjlnEtk3.js → CreateOrderPage-CB1_-_mN.js} +33 -35
- package/dist/{CustomerDetailsPage-I6cdDHUi.js → CustomerDetailsPage-BaeKEI1D.js} +42 -44
- package/dist/{CustomersPage-CGGfD4ki.js → CustomersPage-DRaLuHMC.js} +26 -28
- package/dist/{DashboardPage-D2HRqwg9.js → DashboardPage-DPjhNfKB.js} +85 -83
- package/dist/{DataTable-CD9ZhpLB.js → DataTable-DmciIKKM.js} +3 -3
- package/dist/{DriverDetailsPage-rg_pGNuP.js → DriverDetailsPage-CSSQTOzn.js} +52 -54
- package/dist/{DriversPage-DKiynISH.js → DriversPage-C2Bk0w4Q.js} +24 -26
- package/dist/{FinancePage-Vg8WPs38.js → FinancePage-lmp0Sdsw.js} +9 -9
- package/dist/FleetPage-DZn5pIqd.js +365 -0
- package/dist/ImportExportPage-BIUJZc7O.js +290 -0
- package/dist/InventoryPage-Cxh1zYJd.js +253 -0
- package/dist/{LiveTrackingPage-DQ1q4Id3.js → LiveTrackingPage-BY1yAkIt.js} +60 -56
- package/dist/MarketplacePage-RMuL17EA.js +247 -0
- package/dist/{MetricCard-B7yMyvMX.js → MetricCard-7aYgzZHK.js} +1 -1
- package/dist/{OrderDetailsPage-CFASgsYX.js → OrderDetailsPage-C2Mf4KSy.js} +83 -85
- package/dist/{OrdersListPage-BHeT8S5o.js → OrdersListPage-BrPBtnhp.js} +31 -33
- package/dist/{PageLayout-D3N8MCTm.js → PageLayout-D4LDHJJq.js} +483 -422
- package/dist/{ProductDetailsPage-CFGcbaHo.js → ProductDetailsPage-CezzzZcj.js} +27 -29
- package/dist/{ProductsPage--xEpbQ8A.js → ProductsPage-I5DilIoY.js} +21 -23
- package/dist/ReportsPage-BDXNmUng.js +284 -0
- package/dist/RouteDetailsPage-BcomL20M.js +263 -0
- package/dist/RoutesPage-lqX9ylHP.js +144 -0
- package/dist/SettingsPage-CQVIOaNs.js +421 -0
- package/dist/StatusBadge-NpTeHVlw.js +152 -0
- package/dist/{TrackingPage-CcRc_dva.js → TrackingPage-BXrjz7V8.js} +59 -61
- package/dist/{VehicleDetailsPage-3YDMPsPs.js → VehicleDetailsPage-DZjLcJgr.js} +40 -42
- package/dist/{VehiclesPage-UlxPWP9K.js → VehiclesPage-6W39XT5W.js} +19 -21
- package/dist/{WarehouseDetailsPage-Db7KeN-d.js → WarehouseDetailsPage-C10pgRGW.js} +38 -40
- package/dist/{WarehousesPage-CuTO-OPp.js → WarehousesPage-CHvBqNaQ.js} +15 -17
- package/dist/{arrow-left-CBpZKkpq.js → arrow-left-n48Y-Wis.js} +1 -1
- package/dist/{box-CAaiElsb.js → box-D9Pal4HM.js} +1 -1
- package/dist/{chart-column-D1Ym15-S.js → chart-column-BFIJysDR.js} +1 -1
- package/dist/{chevron-right-BIFGi0UA.js → chevron-right-EMIIyyJ1.js} +1 -1
- package/dist/{circle-alert-DjmNo5_L.js → circle-alert-iY24lSlp.js} +1 -1
- package/dist/{circle-check-big-CjuPVo3_.js → circle-check-big-w0TrLKMF.js} +1 -1
- package/dist/{clock-BjkdnddD.js → clock-DJZtdl8u.js} +1 -1
- package/dist/{dollar-sign-IB4O0wuQ.js → dollar-sign-CgKQRz_e.js} +1 -1
- package/dist/{download-Dpa9vqoG.js → download-DRXxoiQR.js} +1 -1
- package/dist/{file-text-C8Uioy11.js → file-text-DWWY6_f5.js} +1 -1
- package/dist/{filter-Da5OZaHI.js → filter-DloLC2eE.js} +1 -1
- package/dist/index.js +119 -111
- package/dist/{map-_Per4sT9.js → map-CkJaULb2.js} +1 -1
- package/dist/{map-pin-BSaf5IDL.js → map-pin-BGiCvyCs.js} +1 -1
- package/dist/{navigation-DaTbrhw2.js → navigation-D36prnAn.js} +1 -1
- package/dist/{package-BE7U0wQE.js → package-BChUzpJS.js} +1 -1
- package/dist/{phone-DV4StZiA.js → phone-Ac9s3mo_.js} +1 -1
- package/dist/{plus-Dl1rLeJR.js → plus-Z76q-FZm.js} +1 -1
- package/dist/{refresh-cw-CWfERAit.js → refresh-cw-DKt6f5T6.js} +1 -1
- package/dist/{save-XH8vydOn.js → save-CojywNhG.js} +1 -1
- package/dist/{search-OxoIiL_C.js → search-CDGefqb3.js} +1 -1
- package/dist/{square-pen-hLIbFQFV.js → square-pen-CopE9GmB.js} +1 -1
- package/dist/{star-YsVV3MNQ.js → star-nHlLjDKR.js} +1 -1
- package/dist/{store-RCBl7HB-.js → store-BR9Voi5E.js} +1 -1
- package/dist/{trending-up-C-85AwGT.js → trending-up-B7W-GL6P.js} +1 -1
- package/dist/{triangle-alert-BOhfGgmh.js → triangle-alert-BonHFFNz.js} +1 -1
- package/dist/{truck-CtDjNFbi.js → truck-DrlfSpRC.js} +1 -1
- package/dist/{useAnalytics-ph7eTIK6.js → useAnalytics-CgdzUfY_.js} +40 -40
- package/dist/useAssistant-D4v3oxD3.js +84 -0
- package/dist/{useCustomers-bS3a4ytk.js → useCustomers-QrlozybF.js} +24 -24
- package/dist/{useFleet-BdETplNE.js → useFleet-DiAC-IiH.js} +70 -67
- package/dist/useImportExport-BLMnAKCC.js +55 -0
- package/dist/{useInventory-Dwn18FPz.js → useInventory-DKoks37x.js} +33 -33
- package/dist/{useOrders-D_3_hGMp.js → useOrders-D3UZVy8Y.js} +42 -42
- package/dist/useReports-Puq4DCH9.js +78 -0
- package/dist/{useRoutes-v4aBaS-E.js → useRoutes-Do57LvRb.js} +31 -31
- package/dist/useSettings-CkDH_-gM.js +81 -0
- package/dist/{useTracking-De2KIUNu.js → useTracking-GJify4HN.js} +22 -22
- package/dist/{user-Dz5TJZFs.js → user-CjSoudeu.js} +1 -1
- package/dist/{users-g2N0eL-Z.js → users-BAM4gHTn.js} +1 -1
- package/dist/{warehouse-D3WWDeTX.js → warehouse-C7MSr47B.js} +1 -1
- package/dist/{wrench-CmxY1n4I.js → wrench-CANXWxF-.js} +1 -1
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +27 -3
- package/src/movethewheels/components/StatusBadge.tsx +18 -37
- package/src/movethewheels/constants/index.ts +67 -26
- package/src/movethewheels/hooks/index.ts +12 -0
- package/src/movethewheels/hooks/useAnalytics.ts +20 -6
- package/src/movethewheels/hooks/useAssistant.ts +74 -0
- package/src/movethewheels/hooks/useFleet.ts +63 -59
- package/src/movethewheels/hooks/useImportExport.ts +101 -0
- package/src/movethewheels/hooks/useReports.ts +158 -0
- package/src/movethewheels/hooks/useSettings.ts +120 -0
- package/src/movethewheels/pages/AIAssistantPage.tsx +53 -15
- package/src/movethewheels/pages/AnalyticsPage.tsx +67 -64
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/CustomersPage.tsx +1 -1
- package/src/movethewheels/pages/DashboardPage.tsx +42 -22
- package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/DriversPage.tsx +1 -1
- package/src/movethewheels/pages/FleetPage.tsx +96 -38
- package/src/movethewheels/pages/ImportExportPage.tsx +174 -68
- package/src/movethewheels/pages/InventoryPage.tsx +66 -28
- package/src/movethewheels/pages/LiveTrackingPage.tsx +12 -3
- package/src/movethewheels/pages/MarketplacePage.tsx +87 -45
- package/src/movethewheels/pages/OrderDetailsPage.tsx +2 -2
- package/src/movethewheels/pages/ReportsPage.tsx +194 -90
- package/src/movethewheels/pages/RouteDetailsPage.tsx +5 -11
- package/src/movethewheels/pages/RoutesPage.tsx +4 -9
- package/src/movethewheels/pages/SettingsPage.tsx +234 -56
- package/src/movethewheels/pages/TrackingPage.tsx +6 -6
- package/src/movethewheels/providers/MoveTheWheelsProvider.tsx +7 -6
- package/src/movethewheels/store/movethewheelsStore.ts +1 -1
- package/dist/AnalyticsPage-CCLfHgQZ.js +0 -212
- package/dist/FleetPage-DddXt8r8.js +0 -332
- package/dist/ImportExportPage-BW6qO3kc.js +0 -248
- package/dist/InventoryPage-rMGuaJN-.js +0 -222
- package/dist/MarketplacePage-DKrXAv62.js +0 -219
- package/dist/ReportsPage-Bb7_90rK.js +0 -235
- package/dist/RouteDetailsPage-CVtDVb1N.js +0 -282
- package/dist/RoutesPage-s57PtgcQ.js +0 -163
- package/dist/SettingsPage-BYNCBOpN.js +0 -298
- package/dist/StatusBadge-CijYQXtb.js +0 -209
- package/dist/graphqlClient-CdJyR_ed.js +0 -55
- package/dist/navigation-BgnOfsVd.js +0 -6
- package/dist/route-Dp9JiN_E.js +0 -51
- package/dist/settings-3Rw5NwFF.js +0 -28
- package/src/movethewheels/constants/mockData.ts +0 -1342
- /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 {
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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/
|
|
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
|
-
<
|
|
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">
|
|
102
|
-
<Button
|
|
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
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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">{
|
|
269
|
+
<p className="font-medium">{entry.name}</p>
|
|
166
270
|
<p className="text-sm text-text-secondary">
|
|
167
|
-
{
|
|
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=
|
|
172
|
-
<Check size={12} /> {
|
|
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
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
321
|
-
: 'bg-
|
|
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 {
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
{
|
|
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'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
|
-
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
{provider.
|
|
201
|
-
|
|
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
|
-
<
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 */}
|