@burdenoff/microfe-movethewheels 2026.703.2 → 2026.703.4
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-C3cPsWt-.js → AIAssistantPage-3YOnHO9Y.js} +3 -3
- package/dist/{AnalyticsPage-Dz6g2MvK.js → AnalyticsPage-CMpI1070.js} +11 -11
- package/dist/CreateOrderPage-DRAgGo9i.js +490 -0
- package/dist/CustomerDetailsPage-BCeMmOFe.js +615 -0
- package/dist/{CustomersPage-2Y05LVyh.js → CustomersPage-CaNBMXmM.js} +64 -53
- package/dist/{DashboardPage-BVjQi0EB.js → DashboardPage-DIKlXde3.js} +16 -16
- package/dist/DataTable-XvUcTjwi.js +340 -0
- package/dist/DocumentsCard-U2lSe-cn.js +169 -0
- package/dist/{DriverDetailsPage-CN3WQ0B9.js → DriverDetailsPage-BQrYd_Lp.js} +178 -195
- package/dist/{DriversPage-B0Zpx8OQ.js → DriversPage-PYmdFiby.js} +65 -54
- package/dist/FinancePage-Cwh9Bw4M.js +136 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-3ZHsB-TN.js} +23 -22
- package/dist/{FormError-BlOjnXOq.js → FormError-cuLV2jVD.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-C6f7DqhM.js} +50 -70
- package/dist/{InventoryPage-DwfgCUQq.js → InventoryPage-DpiqVu1v.js} +9 -9
- package/dist/{LiveTrackingPage-DCX-K-LR.js → LiveTrackingPage-FAzzlVnX.js} +9 -9
- package/dist/MarketplacePage-Bh_2ns4v.js +426 -0
- package/dist/Notice-CSoDYKw9.js +46 -0
- package/dist/OrderDetailsPage-zCj0lW0j.js +584 -0
- package/dist/{OrdersListPage-BjDlCKJd.js → OrdersListPage-CAMsuUck.js} +166 -89
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-wsxAQkA0.js} +12 -12
- package/dist/{ProductsPage-C1G9h7Xd.js → ProductsPage-DC5qDLfz.js} +60 -49
- package/dist/ReportsPage-BwUJ37eO.js +356 -0
- package/dist/RouteDetailsPage-DyMJE9AS.js +583 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-Dtc669yc.js} +57 -46
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-CpH9xhQg.js} +5 -27
- package/dist/{TrackingPage-W-jtdM3Y.js → TrackingPage-DaNEFG_h.js} +10 -10
- package/dist/{VehicleDetailsPage-Bceo5oz3.js → VehicleDetailsPage-DldYNsYV.js} +125 -142
- package/dist/{VehiclesPage-VBtOep91.js → VehiclesPage-BFw13iMt.js} +61 -50
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-CMG7or6G.js} +17 -17
- package/dist/{WarehousesPage-DCbXXXWE.js → WarehousesPage-Wkytnt86.js} +58 -47
- package/dist/arrow-right-Q3XQpTPR.js +11 -0
- package/dist/arrow-up-xNF1zJUG.js +17 -0
- package/dist/chevron-left-B2_r0Elx.js +8 -0
- package/dist/{wrench-CANXWxF-.js → fuel-B6_lnKDs.js} +2 -5
- package/dist/index.js +163 -174
- package/dist/upload-CHZofOqb.js +21 -0
- package/dist/{useAnalytics-DS8n0xgg.js → useAnalytics-D7HW2XRb.js} +52 -2
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-BPW4__PR.js} +29 -26
- package/dist/{useFleet-DKqXZ16m.js → useFleet-ChBkxAd6.js} +81 -69
- package/dist/{useInventory-DkyiVY_A.js → useInventory-CAAVMRp6.js} +83 -38
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-B465S2nt.js} +58 -47
- package/dist/{useReports-CTqZr-AB.js → useReports-jQ3GpNOm.js} +40 -34
- package/dist/{useRoutes-BQWO607N.js → useRoutes-hS3d5-xr.js} +49 -41
- package/dist/{useTracking-B-va1kO3.js → useTracking-RjGz9PZG.js} +2 -2
- package/dist/user-check-BEKRCi7v.js +20 -0
- package/dist/wrench-q_rgJfaP.js +8 -0
- package/dist/x-tMeDMcCw.js +11 -0
- package/package.json +1 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/Notice.tsx +61 -0
- package/src/movethewheels/components/index.ts +7 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +95 -0
- package/src/movethewheels/hooks/useCustomers.ts +11 -4
- package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
- package/src/movethewheels/hooks/useFleet.ts +26 -7
- package/src/movethewheels/hooks/useInventory.ts +70 -7
- package/src/movethewheels/hooks/useOrders.ts +32 -2
- package/src/movethewheels/hooks/useReports.ts +14 -4
- package/src/movethewheels/hooks/useRoutes.ts +20 -3
- package/src/movethewheels/pages/AnalyticsPage.tsx +2 -2
- package/src/movethewheels/pages/CreateOrderPage.tsx +44 -16
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +27 -11
- package/src/movethewheels/pages/DriverDetailsPage.tsx +16 -30
- package/src/movethewheels/pages/DriversPage.tsx +26 -9
- package/src/movethewheels/pages/FinancePage.tsx +67 -84
- package/src/movethewheels/pages/InventoryPage.tsx +1 -1
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +77 -14
- package/src/movethewheels/pages/ProductsPage.tsx +27 -7
- package/src/movethewheels/pages/ReportsPage.tsx +65 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +74 -13
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +3 -19
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +3 -33
- package/src/movethewheels/pages/VehiclesPage.tsx +26 -5
- package/src/movethewheels/pages/WarehousesPage.tsx +22 -17
- package/src/movethewheels/types/index.ts +8 -0
- package/src/movethewheels/utils/download.ts +62 -0
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- /package/dist/{MetricCard-BY0QrMD4.js → MetricCard-CVBVpu7o.js} +0 -0
- /package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-C87mDpDP.js} +0 -0
- /package/dist/{box-D9Pal4HM.js → box-Eu05PXDh.js} +0 -0
- /package/dist/{chart-column-BFIJysDR.js → chart-column-ouzgxIv_.js} +0 -0
- /package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-eV4LjaSi.js} +0 -0
- /package/dist/{circle-alert-iY24lSlp.js → circle-alert-Bzh2DBSF.js} +0 -0
- /package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-CGMSpQmD.js} +0 -0
- /package/dist/{clock-DJZtdl8u.js → clock-Bl3ptexL.js} +0 -0
- /package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-CAVU95SO.js} +0 -0
- /package/dist/{download-DRXxoiQR.js → download-C5NbvVDc.js} +0 -0
- /package/dist/{file-text-DWWY6_f5.js → file-text-D-1uvLZG.js} +0 -0
- /package/dist/{filter-DloLC2eE.js → filter-DUfUg6BO.js} +0 -0
- /package/dist/{formatters-CF2S2l1m.js → formatters-BQr-aR5W.js} +0 -0
- /package/dist/{map-CkJaULb2.js → map-C-M-FQLR.js} +0 -0
- /package/dist/{map-pin-BGiCvyCs.js → map-pin-Dznl5l40.js} +0 -0
- /package/dist/{navigation-D36prnAn.js → navigation-BJSJl-VG.js} +0 -0
- /package/dist/{package-BChUzpJS.js → package-CcRttV2N.js} +0 -0
- /package/dist/{phone-Ac9s3mo_.js → phone-DVSZHt_j.js} +0 -0
- /package/dist/{plus-Z76q-FZm.js → plus-C_XZDwBz.js} +0 -0
- /package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-CfgN67l3.js} +0 -0
- /package/dist/{save-CojywNhG.js → save-DJE8Fpdv.js} +0 -0
- /package/dist/{search-CDGefqb3.js → search-D_VyGcZT.js} +0 -0
- /package/dist/{square-pen-CopE9GmB.js → square-pen-D-6JYrVQ.js} +0 -0
- /package/dist/{star-nHlLjDKR.js → star-BpRlUZuU.js} +0 -0
- /package/dist/{store-BR9Voi5E.js → store-B-8HMuyZ.js} +0 -0
- /package/dist/{trash-2-BKhar6Hs.js → trash-2-8vmv-XV_.js} +0 -0
- /package/dist/{trending-up-Dohgd4t_.js → trending-up-Dz6PqPRj.js} +0 -0
- /package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-D7Y3J_4z.js} +0 -0
- /package/dist/{truck-C0v_hbLR.js → truck-D-y2IDzv.js} +0 -0
- /package/dist/{useAssistant-D4v3oxD3.js → useAssistant-DVW4QgQb.js} +0 -0
- /package/dist/{useImportExport-DHr-SWSw.js → useImportExport-DrPDHEDK.js} +0 -0
- /package/dist/{useSettings-CkDH_-gM.js → useSettings-BB8X2RLh.js} +0 -0
- /package/dist/{user-jyFU3lXk.js → user-DzfTn4dE.js} +0 -0
- /package/dist/{users-DZ-ilS6B.js → users-CCrq-w0C.js} +0 -0
- /package/dist/{warehouse-6m5kXMLu.js → warehouse-CaqfmMKh.js} +0 -0
|
@@ -66,12 +66,25 @@ interface DataTableProps<T> {
|
|
|
66
66
|
emptyAction?: ReactNode;
|
|
67
67
|
className?: string;
|
|
68
68
|
getRowId?: (row: T) => string;
|
|
69
|
+
/**
|
|
70
|
+
* Controlled (server-side) pagination. When `onPageChange` is provided the
|
|
71
|
+
* table stops slicing client-side — `data` is treated as the current page —
|
|
72
|
+
* and the pager Prev/Next/First/Last call the callbacks instead. Provide
|
|
73
|
+
* either `totalItems` (a real grand total is known) or `hasNextPage`
|
|
74
|
+
* (unknown total — Next is enabled while the last fetch returned a full page).
|
|
75
|
+
*/
|
|
76
|
+
pageIndex?: number;
|
|
77
|
+
onPageChange?: (pageIndex: number) => void;
|
|
78
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
79
|
+
totalItems?: number;
|
|
80
|
+
hasNextPage?: boolean;
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
type SortDirection = 'asc' | 'desc' | null;
|
|
72
84
|
|
|
73
85
|
/**
|
|
74
|
-
* DataTable - A reusable data table component with sorting, search, and
|
|
86
|
+
* DataTable - A reusable data table component with sorting, search, and both
|
|
87
|
+
* client-side and controlled (server-side) pagination.
|
|
75
88
|
*/
|
|
76
89
|
export function DataTable<T extends object>({
|
|
77
90
|
columns,
|
|
@@ -81,8 +94,8 @@ export function DataTable<T extends object>({
|
|
|
81
94
|
searchPlaceholder = 'Search...',
|
|
82
95
|
searchFields,
|
|
83
96
|
pagination = true,
|
|
84
|
-
pageSize:
|
|
85
|
-
pageSizeOptions = [
|
|
97
|
+
pageSize: pageSizeProp = 10,
|
|
98
|
+
pageSizeOptions = [10, 25, 50, 100],
|
|
86
99
|
onRowClick,
|
|
87
100
|
rowClassName,
|
|
88
101
|
emptyMessage = 'No data found',
|
|
@@ -92,14 +105,25 @@ export function DataTable<T extends object>({
|
|
|
92
105
|
emptyAction,
|
|
93
106
|
className,
|
|
94
107
|
getRowId,
|
|
108
|
+
pageIndex,
|
|
109
|
+
onPageChange,
|
|
110
|
+
onPageSizeChange,
|
|
111
|
+
totalItems,
|
|
112
|
+
hasNextPage,
|
|
95
113
|
}: DataTableProps<T>) {
|
|
114
|
+
const isControlled = typeof onPageChange === 'function';
|
|
115
|
+
const controlledPageIndex = pageIndex ?? 0;
|
|
116
|
+
|
|
96
117
|
const [searchQuery, setSearchQuery] = useState('');
|
|
97
118
|
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
98
119
|
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
99
120
|
const [currentPage, setCurrentPage] = useState(1);
|
|
100
|
-
const [
|
|
121
|
+
const [internalPageSize, setInternalPageSize] = useState(pageSizeProp);
|
|
122
|
+
|
|
123
|
+
const activePageSize = isControlled ? pageSizeProp : internalPageSize;
|
|
101
124
|
|
|
102
|
-
// Filter data based on search
|
|
125
|
+
// Filter data based on search (client-side; in controlled mode this scopes to
|
|
126
|
+
// the current page only, which is expected for server-paginated lists).
|
|
103
127
|
const filteredData = useMemo(() => {
|
|
104
128
|
if (!searchQuery || !searchFields || searchFields.length === 0) {
|
|
105
129
|
return data;
|
|
@@ -147,15 +171,20 @@ export function DataTable<T extends object>({
|
|
|
147
171
|
});
|
|
148
172
|
}, [filteredData, sortColumn, sortDirection, columns]);
|
|
149
173
|
|
|
150
|
-
// Paginate data
|
|
174
|
+
// Paginate data — controlled mode never slices (server already did).
|
|
151
175
|
const paginatedData = useMemo(() => {
|
|
152
|
-
if (!pagination) return sortedData;
|
|
176
|
+
if (!pagination || isControlled) return sortedData;
|
|
153
177
|
|
|
154
|
-
const start = (currentPage - 1) *
|
|
155
|
-
return sortedData.slice(start, start +
|
|
156
|
-
}, [sortedData, currentPage,
|
|
178
|
+
const start = (currentPage - 1) * internalPageSize;
|
|
179
|
+
return sortedData.slice(start, start + internalPageSize);
|
|
180
|
+
}, [sortedData, currentPage, internalPageSize, pagination, isControlled]);
|
|
157
181
|
|
|
158
|
-
const
|
|
182
|
+
const totalPagesClient = Math.max(1, Math.ceil(sortedData.length / internalPageSize));
|
|
183
|
+
|
|
184
|
+
const hasTotal = typeof totalItems === 'number';
|
|
185
|
+
const totalPagesControlled = hasTotal
|
|
186
|
+
? Math.max(1, Math.ceil(totalItems / activePageSize))
|
|
187
|
+
: undefined;
|
|
159
188
|
|
|
160
189
|
// Handle sort
|
|
161
190
|
const handleSort = (columnId: string) => {
|
|
@@ -206,7 +235,8 @@ export function DataTable<T extends object>({
|
|
|
206
235
|
<Skeleton className="h-10 w-64" />
|
|
207
236
|
</div>
|
|
208
237
|
)}
|
|
209
|
-
|
|
238
|
+
{/* Desktop skeleton */}
|
|
239
|
+
<div className="hidden md:block rounded-card border border-border-seam bg-bg-surface shadow-elevation-1">
|
|
210
240
|
<Table>
|
|
211
241
|
<TableHeader>
|
|
212
242
|
<TableRow>
|
|
@@ -230,10 +260,49 @@ export function DataTable<T extends object>({
|
|
|
230
260
|
</TableBody>
|
|
231
261
|
</Table>
|
|
232
262
|
</div>
|
|
263
|
+
{/* Mobile skeleton */}
|
|
264
|
+
<div className="md:hidden space-y-3">
|
|
265
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
266
|
+
<div
|
|
267
|
+
key={i}
|
|
268
|
+
className="rounded-card border border-border-seam bg-bg-surface p-4 shadow-elevation-1"
|
|
269
|
+
>
|
|
270
|
+
<Skeleton className="h-4 w-2/3 mb-2 motion-safe:animate-vc-shimmer" />
|
|
271
|
+
<Skeleton className="h-4 w-1/2 motion-safe:animate-vc-shimmer" />
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
233
275
|
</div>
|
|
234
276
|
);
|
|
235
277
|
}
|
|
236
278
|
|
|
279
|
+
const isEmpty = paginatedData.length === 0;
|
|
280
|
+
|
|
281
|
+
const renderEmpty = () =>
|
|
282
|
+
emptyIllustration ? (
|
|
283
|
+
searchQuery ? (
|
|
284
|
+
<IllustratedEmptyState
|
|
285
|
+
illustration="empty-search"
|
|
286
|
+
title="No matches"
|
|
287
|
+
description={`Nothing matches “${searchQuery}”. Try a different search.`}
|
|
288
|
+
/>
|
|
289
|
+
) : (
|
|
290
|
+
<IllustratedEmptyState
|
|
291
|
+
illustration={emptyIllustration}
|
|
292
|
+
title={emptyTitle ?? emptyMessage}
|
|
293
|
+
description={emptyDescription}
|
|
294
|
+
action={emptyAction}
|
|
295
|
+
/>
|
|
296
|
+
)
|
|
297
|
+
) : (
|
|
298
|
+
<p className="text-text-secondary">{emptyMessage}</p>
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Pagination footer visibility
|
|
302
|
+
const showFooter =
|
|
303
|
+
pagination &&
|
|
304
|
+
(isControlled ? sortedData.length > 0 || controlledPageIndex > 0 : sortedData.length > 0);
|
|
305
|
+
|
|
237
306
|
return (
|
|
238
307
|
<div className={cn('space-y-4', className)}>
|
|
239
308
|
{/* Search */}
|
|
@@ -254,8 +323,8 @@ export function DataTable<T extends object>({
|
|
|
254
323
|
</div>
|
|
255
324
|
)}
|
|
256
325
|
|
|
257
|
-
{/* Table */}
|
|
258
|
-
<div className="overflow-x-auto rounded-card border border-border-seam bg-bg-surface shadow-elevation-1">
|
|
326
|
+
{/* Table (desktop / tablet) */}
|
|
327
|
+
<div className="hidden md:block overflow-x-auto rounded-card border border-border-seam bg-bg-surface shadow-elevation-1">
|
|
259
328
|
<Table>
|
|
260
329
|
<TableHeader>
|
|
261
330
|
<TableRow>
|
|
@@ -277,27 +346,10 @@ export function DataTable<T extends object>({
|
|
|
277
346
|
</TableRow>
|
|
278
347
|
</TableHeader>
|
|
279
348
|
<TableBody>
|
|
280
|
-
{
|
|
349
|
+
{isEmpty ? (
|
|
281
350
|
<TableRow>
|
|
282
351
|
<TableCell colSpan={columns.length} className="py-8 text-center">
|
|
283
|
-
{
|
|
284
|
-
searchQuery ? (
|
|
285
|
-
<IllustratedEmptyState
|
|
286
|
-
illustration="empty-search"
|
|
287
|
-
title="No matches"
|
|
288
|
-
description={`Nothing matches “${searchQuery}”. Try a different search.`}
|
|
289
|
-
/>
|
|
290
|
-
) : (
|
|
291
|
-
<IllustratedEmptyState
|
|
292
|
-
illustration={emptyIllustration}
|
|
293
|
-
title={emptyTitle ?? emptyMessage}
|
|
294
|
-
description={emptyDescription}
|
|
295
|
-
action={emptyAction}
|
|
296
|
-
/>
|
|
297
|
-
)
|
|
298
|
-
) : (
|
|
299
|
-
<p className="text-text-secondary">{emptyMessage}</p>
|
|
300
|
-
)}
|
|
352
|
+
{renderEmpty()}
|
|
301
353
|
</TableCell>
|
|
302
354
|
</TableRow>
|
|
303
355
|
) : (
|
|
@@ -325,19 +377,78 @@ export function DataTable<T extends object>({
|
|
|
325
377
|
</Table>
|
|
326
378
|
</div>
|
|
327
379
|
|
|
380
|
+
{/* Card list (mobile) */}
|
|
381
|
+
<div className="md:hidden space-y-3">
|
|
382
|
+
{isEmpty ? (
|
|
383
|
+
<div className="rounded-card border border-border-seam bg-bg-surface p-6 text-center shadow-elevation-1">
|
|
384
|
+
{renderEmpty()}
|
|
385
|
+
</div>
|
|
386
|
+
) : (
|
|
387
|
+
paginatedData.map((row, index) => {
|
|
388
|
+
const rowId = getRowId ? getRowId(row) : index;
|
|
389
|
+
const rowClasses =
|
|
390
|
+
typeof rowClassName === 'function' ? rowClassName(row) : rowClassName;
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div
|
|
394
|
+
key={rowId}
|
|
395
|
+
role={onRowClick ? 'button' : undefined}
|
|
396
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
397
|
+
className={cn(
|
|
398
|
+
'rounded-card border border-border-seam bg-bg-surface p-4 shadow-elevation-1',
|
|
399
|
+
onRowClick && 'cursor-pointer hover:bg-bg-sunken/50',
|
|
400
|
+
rowClasses
|
|
401
|
+
)}
|
|
402
|
+
onClick={() => onRowClick?.(row)}
|
|
403
|
+
onKeyDown={(e) => {
|
|
404
|
+
if (onRowClick && (e.key === 'Enter' || e.key === ' ')) {
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
onRowClick(row);
|
|
407
|
+
}
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
<dl className="space-y-2">
|
|
411
|
+
{columns.map((column) => (
|
|
412
|
+
<div key={column.id} className="flex items-start justify-between gap-3">
|
|
413
|
+
<dt className="text-xs font-medium text-text-secondary shrink-0">
|
|
414
|
+
{column.header}
|
|
415
|
+
</dt>
|
|
416
|
+
<dd className="text-sm text-right min-w-0">{getCellValue(row, column)}</dd>
|
|
417
|
+
</div>
|
|
418
|
+
))}
|
|
419
|
+
</dl>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
})
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
|
|
328
426
|
{/* Pagination */}
|
|
329
|
-
{
|
|
330
|
-
<div className="flex items-center justify-between">
|
|
331
|
-
<div className="flex items-center gap-2 text-sm text-text-secondary">
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
427
|
+
{showFooter && (
|
|
428
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
429
|
+
<div className="flex flex-wrap items-center gap-2 text-sm text-text-secondary">
|
|
430
|
+
{isControlled ? (
|
|
431
|
+
<span>
|
|
432
|
+
Showing {sortedData.length === 0 ? 0 : controlledPageIndex * activePageSize + 1} to{' '}
|
|
433
|
+
{controlledPageIndex * activePageSize + sortedData.length}
|
|
434
|
+
{hasTotal ? ` of ${totalItems}` : ''}
|
|
435
|
+
</span>
|
|
436
|
+
) : (
|
|
437
|
+
<span>
|
|
438
|
+
Showing {(currentPage - 1) * internalPageSize + 1} to{' '}
|
|
439
|
+
{Math.min(currentPage * internalPageSize, sortedData.length)} of {sortedData.length}
|
|
440
|
+
</span>
|
|
441
|
+
)}
|
|
336
442
|
<Select
|
|
337
|
-
value={String(
|
|
443
|
+
value={String(activePageSize)}
|
|
338
444
|
onValueChange={(value) => {
|
|
339
|
-
|
|
340
|
-
|
|
445
|
+
const next = Number(value);
|
|
446
|
+
if (isControlled) {
|
|
447
|
+
onPageSizeChange?.(next);
|
|
448
|
+
} else {
|
|
449
|
+
setInternalPageSize(next);
|
|
450
|
+
setCurrentPage(1);
|
|
451
|
+
}
|
|
341
452
|
}}
|
|
342
453
|
>
|
|
343
454
|
<SelectTrigger className="w-[70px] h-8">
|
|
@@ -354,47 +465,104 @@ export function DataTable<T extends object>({
|
|
|
354
465
|
<span>per page</span>
|
|
355
466
|
</div>
|
|
356
467
|
|
|
357
|
-
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
468
|
+
{isControlled ? (
|
|
469
|
+
<div className="flex items-center gap-1">
|
|
470
|
+
<Button
|
|
471
|
+
variant="outline"
|
|
472
|
+
size="icon"
|
|
473
|
+
className="h-8 w-8"
|
|
474
|
+
aria-label="First page"
|
|
475
|
+
onClick={() => onPageChange?.(0)}
|
|
476
|
+
disabled={controlledPageIndex === 0}
|
|
477
|
+
>
|
|
478
|
+
<ChevronsLeft className="h-4 w-4" />
|
|
479
|
+
</Button>
|
|
480
|
+
<Button
|
|
481
|
+
variant="outline"
|
|
482
|
+
size="icon"
|
|
483
|
+
className="h-8 w-8"
|
|
484
|
+
aria-label="Previous page"
|
|
485
|
+
onClick={() => onPageChange?.(Math.max(0, controlledPageIndex - 1))}
|
|
486
|
+
disabled={controlledPageIndex === 0}
|
|
487
|
+
>
|
|
488
|
+
<ChevronLeft className="h-4 w-4" />
|
|
489
|
+
</Button>
|
|
490
|
+
<span className="px-3 text-sm">
|
|
491
|
+
Page {controlledPageIndex + 1}
|
|
492
|
+
{hasTotal ? ` of ${totalPagesControlled}` : ''}
|
|
493
|
+
</span>
|
|
494
|
+
<Button
|
|
495
|
+
variant="outline"
|
|
496
|
+
size="icon"
|
|
497
|
+
className="h-8 w-8"
|
|
498
|
+
aria-label="Next page"
|
|
499
|
+
onClick={() => onPageChange?.(controlledPageIndex + 1)}
|
|
500
|
+
disabled={
|
|
501
|
+
hasTotal ? controlledPageIndex + 1 >= (totalPagesControlled ?? 1) : !hasNextPage
|
|
502
|
+
}
|
|
503
|
+
>
|
|
504
|
+
<ChevronRight className="h-4 w-4" />
|
|
505
|
+
</Button>
|
|
506
|
+
{hasTotal && (
|
|
507
|
+
<Button
|
|
508
|
+
variant="outline"
|
|
509
|
+
size="icon"
|
|
510
|
+
className="h-8 w-8"
|
|
511
|
+
aria-label="Last page"
|
|
512
|
+
onClick={() => onPageChange?.((totalPagesControlled ?? 1) - 1)}
|
|
513
|
+
disabled={controlledPageIndex + 1 >= (totalPagesControlled ?? 1)}
|
|
514
|
+
>
|
|
515
|
+
<ChevronsRight className="h-4 w-4" />
|
|
516
|
+
</Button>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
519
|
+
) : (
|
|
520
|
+
<div className="flex items-center gap-1">
|
|
521
|
+
<Button
|
|
522
|
+
variant="outline"
|
|
523
|
+
size="icon"
|
|
524
|
+
className="h-8 w-8"
|
|
525
|
+
aria-label="First page"
|
|
526
|
+
onClick={() => setCurrentPage(1)}
|
|
527
|
+
disabled={currentPage === 1}
|
|
528
|
+
>
|
|
529
|
+
<ChevronsLeft className="h-4 w-4" />
|
|
530
|
+
</Button>
|
|
531
|
+
<Button
|
|
532
|
+
variant="outline"
|
|
533
|
+
size="icon"
|
|
534
|
+
className="h-8 w-8"
|
|
535
|
+
aria-label="Previous page"
|
|
536
|
+
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
|
537
|
+
disabled={currentPage === 1}
|
|
538
|
+
>
|
|
539
|
+
<ChevronLeft className="h-4 w-4" />
|
|
540
|
+
</Button>
|
|
541
|
+
<span className="px-3 text-sm">
|
|
542
|
+
Page {currentPage} of {totalPagesClient}
|
|
543
|
+
</span>
|
|
544
|
+
<Button
|
|
545
|
+
variant="outline"
|
|
546
|
+
size="icon"
|
|
547
|
+
className="h-8 w-8"
|
|
548
|
+
aria-label="Next page"
|
|
549
|
+
onClick={() => setCurrentPage((p) => Math.min(totalPagesClient, p + 1))}
|
|
550
|
+
disabled={currentPage === totalPagesClient}
|
|
551
|
+
>
|
|
552
|
+
<ChevronRight className="h-4 w-4" />
|
|
553
|
+
</Button>
|
|
554
|
+
<Button
|
|
555
|
+
variant="outline"
|
|
556
|
+
size="icon"
|
|
557
|
+
className="h-8 w-8"
|
|
558
|
+
aria-label="Last page"
|
|
559
|
+
onClick={() => setCurrentPage(totalPagesClient)}
|
|
560
|
+
disabled={currentPage === totalPagesClient}
|
|
561
|
+
>
|
|
562
|
+
<ChevronsRight className="h-4 w-4" />
|
|
563
|
+
</Button>
|
|
564
|
+
</div>
|
|
565
|
+
)}
|
|
398
566
|
</div>
|
|
399
567
|
)}
|
|
400
568
|
</div>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentsCard — reuse of the platform Files module (wspace-files-svc).
|
|
3
|
+
*
|
|
4
|
+
* Lists the real files tagged for a MoveTheWheels entity (vehicle / driver /
|
|
5
|
+
* order) and deep-links to the Files module for uploading and managing them.
|
|
6
|
+
* MoveTheWheels does not store documents itself — this is honest cross-module
|
|
7
|
+
* reuse. When there are no files (or the query errors), we show an honest empty
|
|
8
|
+
* state with a working link to Files rather than any fabricated rows.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { FileText, ExternalLink, Download, Upload } from 'lucide-react';
|
|
12
|
+
import { Card, CardContent, CardHeader, CardTitle, Button } from './ui';
|
|
13
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
14
|
+
import { useEntityDocuments, type EntityDocumentType } from '../hooks/useEntityDocuments';
|
|
15
|
+
import { formatDate } from '../utils/formatters';
|
|
16
|
+
|
|
17
|
+
interface DocumentsCardProps {
|
|
18
|
+
entityType: EntityDocumentType;
|
|
19
|
+
entityId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatFileSize(bytes: number): string {
|
|
23
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '—';
|
|
24
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
25
|
+
let value = bytes;
|
|
26
|
+
let unit = 0;
|
|
27
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
28
|
+
value /= 1024;
|
|
29
|
+
unit += 1;
|
|
30
|
+
}
|
|
31
|
+
return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function DocumentsCard({ entityType, entityId }: DocumentsCardProps) {
|
|
35
|
+
const { navigate } = useMoveTheWheels();
|
|
36
|
+
const { documents, isLoading } = useEntityDocuments(entityType, entityId);
|
|
37
|
+
|
|
38
|
+
// Deep-link to the host Files module (mounted at /files in the app shell).
|
|
39
|
+
// This is a host-absolute path, so it uses the raw navigate — not the
|
|
40
|
+
// MoveTheWheels base-path-joined one.
|
|
41
|
+
const openFiles = () => navigate('/files/browse');
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Card>
|
|
45
|
+
<CardHeader className="flex flex-row items-center justify-between">
|
|
46
|
+
<CardTitle className="flex items-center gap-2">
|
|
47
|
+
<FileText size={18} /> Documents
|
|
48
|
+
</CardTitle>
|
|
49
|
+
<Button variant="outline" size="sm" onClick={openFiles}>
|
|
50
|
+
<Upload size={14} /> Add / Manage
|
|
51
|
+
</Button>
|
|
52
|
+
</CardHeader>
|
|
53
|
+
<CardContent>
|
|
54
|
+
{isLoading ? (
|
|
55
|
+
<p className="text-text-secondary text-center py-4">Loading documents…</p>
|
|
56
|
+
) : documents.length > 0 ? (
|
|
57
|
+
<div className="space-y-3">
|
|
58
|
+
{documents.map((doc) => (
|
|
59
|
+
<div
|
|
60
|
+
key={doc.id}
|
|
61
|
+
className="flex items-center justify-between gap-3 p-3 border rounded-lg"
|
|
62
|
+
>
|
|
63
|
+
<div className="min-w-0">
|
|
64
|
+
<p className="font-medium truncate">{doc.name}</p>
|
|
65
|
+
<p className="text-sm text-text-secondary">
|
|
66
|
+
{formatFileSize(doc.size)} · {formatDate(doc.createdAt, 'relative')}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
{doc.downloadUrl ? (
|
|
70
|
+
<a
|
|
71
|
+
href={doc.downloadUrl}
|
|
72
|
+
target="_blank"
|
|
73
|
+
rel="noopener noreferrer"
|
|
74
|
+
className="shrink-0 inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
|
75
|
+
>
|
|
76
|
+
<Download size={14} /> Open
|
|
77
|
+
</a>
|
|
78
|
+
) : (
|
|
79
|
+
<Button variant="ghost" size="sm" onClick={openFiles}>
|
|
80
|
+
<ExternalLink size={14} /> View
|
|
81
|
+
</Button>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
) : (
|
|
87
|
+
<div className="text-center py-6">
|
|
88
|
+
<FileText size={32} className="mx-auto mb-2 text-text-secondary opacity-50" />
|
|
89
|
+
<p className="text-text-secondary">No documents yet</p>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={openFiles}
|
|
93
|
+
className="mt-1 text-sm text-primary hover:underline"
|
|
94
|
+
>
|
|
95
|
+
Add them in Files
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</CardContent>
|
|
100
|
+
</Card>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notice — inline success / info feedback banner.
|
|
3
|
+
*
|
|
4
|
+
* The MoveTheWheels MFE does not mount its own sonner Toaster (mounting a second
|
|
5
|
+
* Toaster inside a host shell causes duplicate toasts), so action feedback is
|
|
6
|
+
* shown inline via this banner. Renders nothing when there is no message, so it
|
|
7
|
+
* can be dropped in unconditionally. Uses semantic status tokens so it themes
|
|
8
|
+
* correctly. Pair it with `useNotice` for auto-dismiss behaviour.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useRef, useState } from 'react';
|
|
12
|
+
import { CheckCircle, Info } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
type NoticeVariant = 'success' | 'info';
|
|
15
|
+
|
|
16
|
+
interface NoticeProps {
|
|
17
|
+
message?: string | null;
|
|
18
|
+
variant?: NoticeVariant;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Notice({ message, variant = 'success' }: NoticeProps) {
|
|
22
|
+
if (!message) return null;
|
|
23
|
+
const isSuccess = variant === 'success';
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
role="status"
|
|
27
|
+
className={`flex items-start gap-2 rounded-lg border px-4 py-3 text-sm ${
|
|
28
|
+
isSuccess
|
|
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
|
+
>
|
|
33
|
+
{isSuccess ? (
|
|
34
|
+
<CheckCircle size={18} className="mt-0.5 shrink-0" />
|
|
35
|
+
) : (
|
|
36
|
+
<Info size={18} className="mt-0.5 shrink-0" />
|
|
37
|
+
)}
|
|
38
|
+
<span>{message}</span>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* useNotice — transient success/info message that auto-clears after a delay.
|
|
45
|
+
* Returns the current message plus a `notify` setter, mirroring a toast API.
|
|
46
|
+
*/
|
|
47
|
+
export function useNotice(autoDismissMs = 4000) {
|
|
48
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
49
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
50
|
+
|
|
51
|
+
const notify = useCallback(
|
|
52
|
+
(next: string) => {
|
|
53
|
+
setMessage(next);
|
|
54
|
+
if (timer.current) clearTimeout(timer.current);
|
|
55
|
+
timer.current = setTimeout(() => setMessage(null), autoDismissMs);
|
|
56
|
+
},
|
|
57
|
+
[autoDismissMs]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return { message, notify };
|
|
61
|
+
}
|
|
@@ -24,3 +24,10 @@ export {
|
|
|
24
24
|
|
|
25
25
|
// Table components
|
|
26
26
|
export { DataTable, type Column } from './DataTable';
|
|
27
|
+
|
|
28
|
+
// Feedback components
|
|
29
|
+
export { FormError } from './FormError';
|
|
30
|
+
export { Notice, useNotice } from './Notice';
|
|
31
|
+
|
|
32
|
+
// Documents (reuse of wspace-files-svc)
|
|
33
|
+
export { DocumentsCard } from './DocumentsCard';
|
|
@@ -41,6 +41,8 @@ export {
|
|
|
41
41
|
useOrderAnalytics,
|
|
42
42
|
useFleetAnalytics,
|
|
43
43
|
usePerformanceAnalytics,
|
|
44
|
+
useFinanceMetrics,
|
|
45
|
+
type FinanceMetrics,
|
|
44
46
|
} from './useAnalytics';
|
|
45
47
|
|
|
46
48
|
// Tracking
|
|
@@ -65,3 +67,10 @@ export { useImportExport } from './useImportExport';
|
|
|
65
67
|
|
|
66
68
|
// Module settings
|
|
67
69
|
export { useModuleSettings } from './useSettings';
|
|
70
|
+
|
|
71
|
+
// Entity documents (reuse of wspace-files-svc)
|
|
72
|
+
export {
|
|
73
|
+
useEntityDocuments,
|
|
74
|
+
type EntityDocument,
|
|
75
|
+
type EntityDocumentType,
|
|
76
|
+
} from './useEntityDocuments';
|