@burdenoff/microfe-movethewheels 2026.920.3 → 2026.921.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CarbonFootprintPanel.js +1 -1
- package/dist/components/CustomerPicker.js +76 -65
- package/dist/components/DataTable.js +140 -139
- package/dist/components/DocumentsCard.js +14 -8
- package/dist/generated/wspace-types.d.ts +1043 -5
- package/dist/hooks/useAnalytics.js +21 -1
- package/dist/hooks/useCustomers.js +94 -78
- package/dist/hooks/useDebouncedValue.js +11 -0
- package/dist/hooks/useEntityDocuments.js +28 -18
- package/dist/hooks/useEpod.js +24 -17
- package/dist/hooks/useFleet.js +146 -125
- package/dist/hooks/useInventory.js +255 -186
- package/dist/hooks/useMultiModalShipments.js +35 -27
- package/dist/hooks/useOrders.js +73 -73
- package/dist/hooks/useRoutes.js +97 -75
- package/dist/hooks/useTracking.js +100 -88
- package/dist/movethewheels/components/CustomerPicker.d.ts +3 -1
- package/dist/movethewheels/components/DataTable.d.ts +5 -1
- package/dist/movethewheels/hooks/useCustomers.d.ts +0 -1
- package/dist/movethewheels/hooks/useDebouncedValue.d.ts +1 -0
- package/dist/movethewheels/hooks/useEntityDocuments.d.ts +2 -0
- package/dist/movethewheels/hooks/useEpod.d.ts +6 -1
- package/dist/movethewheels/hooks/useFleet.d.ts +49 -5
- package/dist/movethewheels/hooks/useInventory.d.ts +24 -17
- package/dist/movethewheels/hooks/useMultiModalShipments.d.ts +6 -1
- package/dist/movethewheels/hooks/useOrders.d.ts +33 -5
- package/dist/movethewheels/hooks/useRoutes.d.ts +2 -1
- package/dist/movethewheels/hooks/useTracking.d.ts +12 -11
- package/dist/movethewheels/types/index.d.ts +7 -5
- package/dist/pages/CreateOrderPage.js +207 -201
- package/dist/pages/CustomersPage.js +61 -54
- package/dist/pages/DeliveryCapturePage.js +131 -109
- package/dist/pages/DriversPage.js +100 -92
- package/dist/pages/InventoryPage.js +239 -236
- package/dist/pages/MultiModalShipmentPage.js +163 -130
- package/dist/pages/OrderDetailsPage.js +224 -208
- package/dist/pages/OrdersListPage.js +66 -60
- package/dist/pages/ProductDetailsPage.js +241 -200
- package/dist/pages/ProductsPage.js +59 -52
- package/dist/pages/RouteDetailsPage.js +343 -321
- package/dist/pages/RoutesPage.js +55 -48
- package/dist/pages/VehiclesPage.js +63 -56
- package/dist/pages/WarehousesPage.js +60 -53
- package/package.json +1 -1
- package/src/generated/wspace-types.ts +1188 -5
- package/src/movethewheels/components/CarbonFootprintPanel.tsx +0 -1
- package/src/movethewheels/components/CustomerPicker.tsx +14 -0
- package/src/movethewheels/components/DataTable.tsx +34 -18
- package/src/movethewheels/components/DocumentsCard.tsx +6 -1
- package/src/movethewheels/components/maps/leaflet-setup.ts +1 -8
- package/src/movethewheels/hooks/useAnalytics.ts +27 -0
- package/src/movethewheels/hooks/useCustomers.ts +64 -24
- package/src/movethewheels/hooks/useDebouncedValue.ts +12 -0
- package/src/movethewheels/hooks/useEntityDocuments.ts +43 -25
- package/src/movethewheels/hooks/useEpod.ts +14 -7
- package/src/movethewheels/hooks/useFleet.ts +58 -37
- package/src/movethewheels/hooks/useFleetIntelligence.ts +1 -4
- package/src/movethewheels/hooks/useInventory.ts +211 -90
- package/src/movethewheels/hooks/useMultiModalShipments.ts +15 -11
- package/src/movethewheels/hooks/useOrders.ts +13 -14
- package/src/movethewheels/hooks/useRoutes.ts +80 -23
- package/src/movethewheels/hooks/useTracking.ts +130 -98
- package/src/movethewheels/pages/CreateOrderPage.tsx +9 -1
- package/src/movethewheels/pages/CustomersPage.tsx +11 -1
- package/src/movethewheels/pages/DeliveryCapturePage.tsx +31 -1
- package/src/movethewheels/pages/DriversPage.tsx +24 -12
- package/src/movethewheels/pages/InventoryPage.tsx +57 -32
- package/src/movethewheels/pages/MultiModalShipmentPage.tsx +52 -3
- package/src/movethewheels/pages/OrderDetailsPage.tsx +17 -1
- package/src/movethewheels/pages/OrdersListPage.tsx +20 -3
- package/src/movethewheels/pages/ProductDetailsPage.tsx +52 -3
- package/src/movethewheels/pages/ProductsPage.tsx +11 -1
- package/src/movethewheels/pages/RouteDetailsPage.tsx +35 -5
- package/src/movethewheels/pages/RoutesPage.tsx +11 -1
- package/src/movethewheels/pages/VehiclesPage.tsx +19 -2
- package/src/movethewheels/pages/WarehousesPage.tsx +11 -1
- package/src/movethewheels/types/index.ts +7 -5
|
@@ -30,6 +30,8 @@ interface CustomerPickerProps {
|
|
|
30
30
|
onRetry: () => void;
|
|
31
31
|
onOpenCustomers?: () => void;
|
|
32
32
|
selectedCustomer?: Customer | null;
|
|
33
|
+
searchQuery?: string;
|
|
34
|
+
onSearchQueryChange?: (query: string) => void;
|
|
33
35
|
disabled?: boolean;
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -67,6 +69,8 @@ export function CustomerPicker({
|
|
|
67
69
|
onRetry,
|
|
68
70
|
onOpenCustomers,
|
|
69
71
|
selectedCustomer,
|
|
72
|
+
searchQuery = '',
|
|
73
|
+
onSearchQueryChange,
|
|
70
74
|
disabled = false,
|
|
71
75
|
}: CustomerPickerProps) {
|
|
72
76
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
@@ -135,6 +139,16 @@ export function CustomerPicker({
|
|
|
135
139
|
</Button>
|
|
136
140
|
</div>
|
|
137
141
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start">
|
|
142
|
+
{onSearchQueryChange && (
|
|
143
|
+
<Input
|
|
144
|
+
value={searchQuery}
|
|
145
|
+
onChange={(event) => onSearchQueryChange(event.target.value)}
|
|
146
|
+
placeholder="Search customers"
|
|
147
|
+
aria-label="Search customers"
|
|
148
|
+
disabled={disabled}
|
|
149
|
+
className="sm:max-w-56"
|
|
150
|
+
/>
|
|
151
|
+
)}
|
|
138
152
|
<Select
|
|
139
153
|
value={value}
|
|
140
154
|
onValueChange={onValueChange}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, type ReactNode } from 'react';
|
|
1
|
+
import { useState, useMemo, type ReactNode, type ChangeEvent } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Table,
|
|
4
4
|
TableBody,
|
|
@@ -56,6 +56,10 @@ interface DataTableProps<T> {
|
|
|
56
56
|
searchable?: boolean;
|
|
57
57
|
searchPlaceholder?: string;
|
|
58
58
|
searchFields?: (keyof T)[];
|
|
59
|
+
/** Controlled search query for server-side filtering. */
|
|
60
|
+
searchQuery?: string;
|
|
61
|
+
/** Called when the search input changes; disables current-page filtering. */
|
|
62
|
+
onSearchQueryChange?: (query: string) => void;
|
|
59
63
|
pagination?: boolean;
|
|
60
64
|
pageSize?: number;
|
|
61
65
|
pageSizeOptions?: number[];
|
|
@@ -106,6 +110,8 @@ export function DataTable<T extends object>({
|
|
|
106
110
|
searchable = false,
|
|
107
111
|
searchPlaceholder = 'Search...',
|
|
108
112
|
searchFields,
|
|
113
|
+
searchQuery: controlledSearchQuery,
|
|
114
|
+
onSearchQueryChange,
|
|
109
115
|
pagination = true,
|
|
110
116
|
pageSize: pageSizeProp = 10,
|
|
111
117
|
pageSizeOptions = [10, 25, 50, 100],
|
|
@@ -128,6 +134,8 @@ export function DataTable<T extends object>({
|
|
|
128
134
|
const controlledPageIndex = pageIndex ?? 0;
|
|
129
135
|
|
|
130
136
|
const [searchQuery, setSearchQuery] = useState('');
|
|
137
|
+
const isServerSearch = typeof onSearchQueryChange === 'function';
|
|
138
|
+
const activeSearchQuery = isServerSearch ? (controlledSearchQuery ?? '') : searchQuery;
|
|
131
139
|
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
132
140
|
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
|
133
141
|
const [currentPage, setCurrentPage] = useState(1);
|
|
@@ -135,25 +143,26 @@ export function DataTable<T extends object>({
|
|
|
135
143
|
|
|
136
144
|
const activePageSize = isControlled ? pageSizeProp : internalPageSize;
|
|
137
145
|
|
|
138
|
-
//
|
|
139
|
-
// the current page
|
|
146
|
+
// In server-search mode, `data` is already filtered by the backend. Never
|
|
147
|
+
// filter just the current page again, or matching rows on other pages vanish.
|
|
140
148
|
const filteredData = useMemo(() => {
|
|
141
|
-
if (!
|
|
149
|
+
if (isServerSearch || !activeSearchQuery || !searchFields || searchFields.length === 0) {
|
|
142
150
|
return data;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
const query =
|
|
153
|
+
const query = activeSearchQuery.toLowerCase();
|
|
146
154
|
return data.filter((row) =>
|
|
147
155
|
searchFields.some((field) => {
|
|
148
156
|
const value = row[field] as unknown;
|
|
149
157
|
return value != null && String(value).toLowerCase().includes(query);
|
|
150
158
|
})
|
|
151
159
|
);
|
|
152
|
-
}, [data,
|
|
160
|
+
}, [data, activeSearchQuery, searchFields, isServerSearch]);
|
|
153
161
|
|
|
154
|
-
//
|
|
162
|
+
// Controlled pages use the backend's stable order. Local sorting a fetched
|
|
163
|
+
// page would produce a misleading partial-workspace sort.
|
|
155
164
|
const sortedData = useMemo(() => {
|
|
156
|
-
if (!sortColumn || !sortDirection) {
|
|
165
|
+
if (isControlled || !sortColumn || !sortDirection) {
|
|
157
166
|
return filteredData;
|
|
158
167
|
}
|
|
159
168
|
|
|
@@ -201,6 +210,7 @@ export function DataTable<T extends object>({
|
|
|
201
210
|
|
|
202
211
|
// Handle sort
|
|
203
212
|
const handleSort = (columnId: string) => {
|
|
213
|
+
if (isControlled) return;
|
|
204
214
|
if (sortColumn === columnId) {
|
|
205
215
|
if (sortDirection === 'asc') {
|
|
206
216
|
setSortDirection('desc');
|
|
@@ -320,11 +330,11 @@ export function DataTable<T extends object>({
|
|
|
320
330
|
|
|
321
331
|
const renderEmpty = () =>
|
|
322
332
|
emptyIllustration ? (
|
|
323
|
-
|
|
333
|
+
activeSearchQuery ? (
|
|
324
334
|
<IllustratedEmptyState
|
|
325
335
|
illustration="empty-search"
|
|
326
336
|
title="No matches"
|
|
327
|
-
description={`Nothing matches “${
|
|
337
|
+
description={`Nothing matches “${activeSearchQuery}”. Try a different search.`}
|
|
328
338
|
/>
|
|
329
339
|
) : (
|
|
330
340
|
<IllustratedEmptyState
|
|
@@ -352,10 +362,14 @@ export function DataTable<T extends object>({
|
|
|
352
362
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-secondary" />
|
|
353
363
|
<Input
|
|
354
364
|
placeholder={searchPlaceholder}
|
|
355
|
-
value={
|
|
356
|
-
onChange={(e) => {
|
|
357
|
-
|
|
358
|
-
|
|
365
|
+
value={activeSearchQuery}
|
|
366
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
|
367
|
+
if (isServerSearch) {
|
|
368
|
+
onSearchQueryChange?.(e.target.value);
|
|
369
|
+
} else {
|
|
370
|
+
setSearchQuery(e.target.value);
|
|
371
|
+
setCurrentPage(1);
|
|
372
|
+
}
|
|
359
373
|
}}
|
|
360
374
|
className="pl-9"
|
|
361
375
|
/>
|
|
@@ -372,14 +386,16 @@ export function DataTable<T extends object>({
|
|
|
372
386
|
<TableHead
|
|
373
387
|
key={column.id}
|
|
374
388
|
className={cn(
|
|
375
|
-
|
|
389
|
+
!isControlled &&
|
|
390
|
+
column.sortable &&
|
|
391
|
+
'cursor-pointer select-none hover:bg-bg-sunken/50',
|
|
376
392
|
column.headerClassName
|
|
377
393
|
)}
|
|
378
|
-
onClick={() => column.sortable && handleSort(column.id)}
|
|
394
|
+
onClick={() => !isControlled && column.sortable && handleSort(column.id)}
|
|
379
395
|
>
|
|
380
396
|
<div className="flex items-center gap-2">
|
|
381
397
|
{column.header}
|
|
382
|
-
{column.sortable && getSortIcon(column.id)}
|
|
398
|
+
{!isControlled && column.sortable && getSortIcon(column.id)}
|
|
383
399
|
</div>
|
|
384
400
|
</TableHead>
|
|
385
401
|
))}
|
|
@@ -481,7 +497,7 @@ export function DataTable<T extends object>({
|
|
|
481
497
|
)}
|
|
482
498
|
<Select
|
|
483
499
|
value={String(activePageSize)}
|
|
484
|
-
onValueChange={(value) => {
|
|
500
|
+
onValueChange={(value: string) => {
|
|
485
501
|
const next = Number(value);
|
|
486
502
|
if (isControlled) {
|
|
487
503
|
onPageSizeChange?.(next);
|
|
@@ -33,7 +33,7 @@ function formatFileSize(bytes: number): string {
|
|
|
33
33
|
|
|
34
34
|
export function DocumentsCard({ entityType, entityId }: DocumentsCardProps) {
|
|
35
35
|
const { navigate } = useMoveTheWheels();
|
|
36
|
-
const { documents, isLoading } = useEntityDocuments(entityType, entityId);
|
|
36
|
+
const { documents, hasMore, isLoading, loadMore } = useEntityDocuments(entityType, entityId);
|
|
37
37
|
|
|
38
38
|
// Deep-link to the host Files module (mounted at /files in the app shell).
|
|
39
39
|
// This is a host-absolute path, so it uses the raw navigate — not the
|
|
@@ -82,6 +82,11 @@ export function DocumentsCard({ entityType, entityId }: DocumentsCardProps) {
|
|
|
82
82
|
)}
|
|
83
83
|
</div>
|
|
84
84
|
))}
|
|
85
|
+
{hasMore && (
|
|
86
|
+
<Button variant="outline" size="sm" onClick={loadMore} disabled={isLoading}>
|
|
87
|
+
Load more documents
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
85
90
|
</div>
|
|
86
91
|
) : (
|
|
87
92
|
<div className="text-center py-6">
|
|
@@ -13,14 +13,7 @@ export const OSM_ATTRIBUTION =
|
|
|
13
13
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
|
14
14
|
|
|
15
15
|
export type MapMarkerStatus =
|
|
16
|
-
| '
|
|
17
|
-
| 'in_transit'
|
|
18
|
-
| 'idle'
|
|
19
|
-
| 'delayed'
|
|
20
|
-
| 'delivered'
|
|
21
|
-
| 'origin'
|
|
22
|
-
| 'destination'
|
|
23
|
-
| 'default';
|
|
16
|
+
'active' | 'in_transit' | 'idle' | 'delayed' | 'delivered' | 'origin' | 'destination' | 'default';
|
|
24
17
|
|
|
25
18
|
/**
|
|
26
19
|
* A coordinate is "real" only when it is finite and not the null-island (0,0)
|
|
@@ -67,10 +67,15 @@ const DASHBOARD_QUERY = `
|
|
|
67
67
|
deliveryJobStats {
|
|
68
68
|
total
|
|
69
69
|
pending
|
|
70
|
+
confirmed
|
|
71
|
+
assigned
|
|
72
|
+
pickedUp
|
|
70
73
|
inTransit
|
|
74
|
+
outForDelivery
|
|
71
75
|
delivered
|
|
72
76
|
cancelled
|
|
73
77
|
failed
|
|
78
|
+
returned
|
|
74
79
|
}
|
|
75
80
|
fleetStats {
|
|
76
81
|
totalVehicles
|
|
@@ -94,10 +99,15 @@ const ORDER_ANALYTICS_QUERY = `
|
|
|
94
99
|
query GetOrderAnalytics {
|
|
95
100
|
deliveryJobStats {
|
|
96
101
|
pending
|
|
102
|
+
confirmed
|
|
103
|
+
assigned
|
|
104
|
+
pickedUp
|
|
97
105
|
inTransit
|
|
106
|
+
outForDelivery
|
|
98
107
|
delivered
|
|
99
108
|
cancelled
|
|
100
109
|
failed
|
|
110
|
+
returned
|
|
101
111
|
}
|
|
102
112
|
analytics {
|
|
103
113
|
orderTrend {
|
|
@@ -307,8 +317,15 @@ export function useDashboardMetrics() {
|
|
|
307
317
|
deliveryJobStats: {
|
|
308
318
|
total: number;
|
|
309
319
|
pending: number;
|
|
320
|
+
confirmed: number;
|
|
321
|
+
assigned: number;
|
|
322
|
+
pickedUp: number;
|
|
310
323
|
inTransit: number;
|
|
324
|
+
outForDelivery: number;
|
|
311
325
|
delivered: number;
|
|
326
|
+
cancelled: number;
|
|
327
|
+
failed: number;
|
|
328
|
+
returned: number;
|
|
312
329
|
};
|
|
313
330
|
fleetStats: {
|
|
314
331
|
totalVehicles: number;
|
|
@@ -416,10 +433,15 @@ export function useOrderAnalytics() {
|
|
|
416
433
|
const data = await executeGraphQL<{
|
|
417
434
|
deliveryJobStats: {
|
|
418
435
|
pending: number;
|
|
436
|
+
confirmed: number;
|
|
437
|
+
assigned: number;
|
|
438
|
+
pickedUp: number;
|
|
419
439
|
inTransit: number;
|
|
440
|
+
outForDelivery: number;
|
|
420
441
|
delivered: number;
|
|
421
442
|
cancelled: number;
|
|
422
443
|
failed: number;
|
|
444
|
+
returned: number;
|
|
423
445
|
};
|
|
424
446
|
analytics: {
|
|
425
447
|
orderTrend: Array<{ label: string; value: number }>;
|
|
@@ -429,10 +451,15 @@ export function useOrderAnalytics() {
|
|
|
429
451
|
setOrderData({
|
|
430
452
|
byStatus: [
|
|
431
453
|
{ status: 'pending', count: data.deliveryJobStats.pending },
|
|
454
|
+
{ status: 'confirmed', count: data.deliveryJobStats.confirmed },
|
|
455
|
+
{ status: 'assigned', count: data.deliveryJobStats.assigned },
|
|
456
|
+
{ status: 'picked_up', count: data.deliveryJobStats.pickedUp },
|
|
432
457
|
{ status: 'in_transit', count: data.deliveryJobStats.inTransit },
|
|
458
|
+
{ status: 'out_for_delivery', count: data.deliveryJobStats.outForDelivery },
|
|
433
459
|
{ status: 'delivered', count: data.deliveryJobStats.delivered },
|
|
434
460
|
{ status: 'cancelled', count: data.deliveryJobStats.cancelled },
|
|
435
461
|
{ status: 'failed', count: data.deliveryJobStats.failed },
|
|
462
|
+
{ status: 'returned', count: data.deliveryJobStats.returned },
|
|
436
463
|
],
|
|
437
464
|
byType: [],
|
|
438
465
|
byPriority: [],
|
|
@@ -12,7 +12,6 @@ interface UseCustomersOptions {
|
|
|
12
12
|
interface UseCustomersResult {
|
|
13
13
|
customers: Customer[];
|
|
14
14
|
totalCustomers: number;
|
|
15
|
-
/** True when the last fetch returned a full page (rawCount === limit). */
|
|
16
15
|
hasMore: boolean;
|
|
17
16
|
isLoading: boolean;
|
|
18
17
|
error: Error | null;
|
|
@@ -55,6 +54,7 @@ const LIST_CUSTOMERS_QUERY = `
|
|
|
55
54
|
updatedAt
|
|
56
55
|
deletedAt
|
|
57
56
|
}
|
|
57
|
+
customersCount(filter: $filter)
|
|
58
58
|
}
|
|
59
59
|
`;
|
|
60
60
|
|
|
@@ -165,6 +165,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
165
165
|
useMoveTheWheels();
|
|
166
166
|
|
|
167
167
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
|
168
|
+
const [totalCustomers, setTotalCustomers] = useState(0);
|
|
168
169
|
const [hasMore, setHasMore] = useState(false);
|
|
169
170
|
const [isLoading, setIsLoading] = useState(false);
|
|
170
171
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -187,6 +188,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
187
188
|
try {
|
|
188
189
|
const data = await executeGraphQL<{
|
|
189
190
|
customers: ApiCustomer[];
|
|
191
|
+
customersCount: number;
|
|
190
192
|
}>(graphQLOptions, LIST_CUSTOMERS_QUERY, {
|
|
191
193
|
filter: {
|
|
192
194
|
type: filters?.type,
|
|
@@ -196,11 +198,9 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
196
198
|
});
|
|
197
199
|
|
|
198
200
|
const raw = data.customers;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
mapped = mapped.filter((item) => item.isActive === filters.isActive);
|
|
203
|
-
}
|
|
201
|
+
setTotalCustomers(data.customersCount);
|
|
202
|
+
setHasMore(offset + raw.length < data.customersCount);
|
|
203
|
+
const mapped = raw.map(mapCustomer);
|
|
204
204
|
setCustomers(mapped);
|
|
205
205
|
} catch (fetchError) {
|
|
206
206
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load customers'));
|
|
@@ -280,7 +280,7 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
280
280
|
|
|
281
281
|
return {
|
|
282
282
|
customers,
|
|
283
|
-
totalCustomers
|
|
283
|
+
totalCustomers,
|
|
284
284
|
hasMore,
|
|
285
285
|
isLoading,
|
|
286
286
|
error,
|
|
@@ -342,25 +342,65 @@ export function useCustomer(customerId: string | undefined) {
|
|
|
342
342
|
return { customer, isLoading, error, refetch };
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
const CUSTOMER_STATS_QUERY = `
|
|
346
|
+
query CustomerStats {
|
|
347
|
+
customerStats {
|
|
348
|
+
total
|
|
349
|
+
b2b
|
|
350
|
+
b2c
|
|
351
|
+
active
|
|
352
|
+
inactive
|
|
353
|
+
avgRating
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
`;
|
|
357
|
+
|
|
345
358
|
export function useCustomerStats() {
|
|
346
|
-
const {
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
360
|
+
useMoveTheWheels();
|
|
361
|
+
const graphQLOptions = useMemo(
|
|
362
|
+
() => ({
|
|
363
|
+
apiGatewayUrl,
|
|
364
|
+
authToken,
|
|
365
|
+
workspaceToken,
|
|
366
|
+
workspaceId,
|
|
367
|
+
tenantId,
|
|
368
|
+
actorId: currentUser?.id,
|
|
369
|
+
}),
|
|
370
|
+
[apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
|
|
371
|
+
);
|
|
372
|
+
const [stats, setStats] = useState({
|
|
373
|
+
total: 0,
|
|
374
|
+
b2b: 0,
|
|
375
|
+
b2c: 0,
|
|
376
|
+
active: 0,
|
|
377
|
+
inactive: 0,
|
|
378
|
+
avgRating: 0,
|
|
379
|
+
});
|
|
380
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
381
|
+
const [error, setError] = useState<Error | null>(null);
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
const load = async () => {
|
|
385
|
+
setIsLoading(true);
|
|
386
|
+
setError(null);
|
|
387
|
+
try {
|
|
388
|
+
const data = await executeGraphQL<{ customerStats: typeof stats }>(
|
|
389
|
+
graphQLOptions,
|
|
390
|
+
CUSTOMER_STATS_QUERY
|
|
391
|
+
);
|
|
392
|
+
setStats(data.customerStats);
|
|
393
|
+
} catch (fetchError) {
|
|
394
|
+
setError(
|
|
395
|
+
fetchError instanceof Error ? fetchError : new Error('Failed to load customer stats')
|
|
396
|
+
);
|
|
397
|
+
} finally {
|
|
398
|
+
setIsLoading(false);
|
|
399
|
+
}
|
|
362
400
|
};
|
|
363
|
-
|
|
401
|
+
|
|
402
|
+
void load();
|
|
403
|
+
}, [graphQLOptions]);
|
|
364
404
|
|
|
365
405
|
return { stats, isLoading, error };
|
|
366
406
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useDebouncedValue<T>(value: T, delay = 300): T {
|
|
4
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const timer = window.setTimeout(() => setDebouncedValue(value), delay);
|
|
8
|
+
return () => window.clearTimeout(timer);
|
|
9
|
+
}, [value, delay]);
|
|
10
|
+
|
|
11
|
+
return debouncedValue;
|
|
12
|
+
}
|
|
@@ -55,9 +55,11 @@ interface ApiFile {
|
|
|
55
55
|
|
|
56
56
|
interface UseEntityDocumentsResult {
|
|
57
57
|
documents: EntityDocument[];
|
|
58
|
+
hasMore: boolean;
|
|
58
59
|
isLoading: boolean;
|
|
59
60
|
error: Error | null;
|
|
60
61
|
refetch: () => void;
|
|
62
|
+
loadMore: () => void;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const LIST_ENTITY_FILES_QUERY = `
|
|
@@ -71,6 +73,7 @@ const LIST_ENTITY_FILES_QUERY = `
|
|
|
71
73
|
createdAt
|
|
72
74
|
downloadUrl
|
|
73
75
|
}
|
|
76
|
+
hasMore
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
`;
|
|
@@ -93,6 +96,8 @@ export function useEntityDocuments(
|
|
|
93
96
|
const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
|
|
94
97
|
useMoveTheWheels();
|
|
95
98
|
const [documents, setDocuments] = useState<EntityDocument[]>([]);
|
|
99
|
+
const [hasMore, setHasMore] = useState(false);
|
|
100
|
+
const [offset, setOffset] = useState(0);
|
|
96
101
|
const [isLoading, setIsLoading] = useState(false);
|
|
97
102
|
const [error, setError] = useState<Error | null>(null);
|
|
98
103
|
|
|
@@ -108,43 +113,56 @@ export function useEntityDocuments(
|
|
|
108
113
|
[apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
|
|
109
114
|
);
|
|
110
115
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
116
|
+
const pageSize = 20;
|
|
117
|
+
|
|
118
|
+
const load = useCallback(
|
|
119
|
+
async (requestedOffset = 0, append = false) => {
|
|
120
|
+
if (!entityId) {
|
|
121
|
+
setDocuments([]);
|
|
122
|
+
setHasMore(false);
|
|
123
|
+
setOffset(0);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setIsLoading(true);
|
|
127
|
+
setError(null);
|
|
128
|
+
try {
|
|
129
|
+
const data = await executeGraphQL<{
|
|
130
|
+
getFiles: { files: ApiFile[]; hasMore?: boolean | null };
|
|
131
|
+
}>(graphQLOptions, LIST_ENTITY_FILES_QUERY, {
|
|
123
132
|
filter: {
|
|
124
133
|
tags: [
|
|
125
134
|
{ key: 'mtwEntityType', label: entityType },
|
|
126
135
|
{ key: 'mtwEntityId', label: entityId },
|
|
127
136
|
],
|
|
128
137
|
},
|
|
129
|
-
pagination: { limit:
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
pagination: { limit: pageSize, offset: requestedOffset },
|
|
139
|
+
});
|
|
140
|
+
const nextDocuments = (data.getFiles?.files ?? []).map(mapFile);
|
|
141
|
+
setDocuments((previous) => (append ? [...previous, ...nextDocuments] : nextDocuments));
|
|
142
|
+
setHasMore(data.getFiles?.hasMore ?? nextDocuments.length === pageSize);
|
|
143
|
+
setOffset(requestedOffset);
|
|
144
|
+
} catch (fetchError) {
|
|
145
|
+
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load documents'));
|
|
146
|
+
if (!append) setDocuments([]);
|
|
147
|
+
} finally {
|
|
148
|
+
setIsLoading(false);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
[entityId, entityType, graphQLOptions]
|
|
152
|
+
);
|
|
140
153
|
|
|
141
154
|
useEffect(() => {
|
|
142
155
|
void load();
|
|
143
156
|
}, [load]);
|
|
144
157
|
|
|
145
158
|
const refetch = useCallback(() => {
|
|
146
|
-
void load();
|
|
159
|
+
void load(0, false);
|
|
147
160
|
}, [load]);
|
|
148
161
|
|
|
149
|
-
|
|
162
|
+
const loadMore = useCallback(() => {
|
|
163
|
+
if (!hasMore || isLoading) return;
|
|
164
|
+
void load(offset + pageSize, true);
|
|
165
|
+
}, [hasMore, isLoading, load, offset]);
|
|
166
|
+
|
|
167
|
+
return { documents, hasMore, isLoading, error, refetch, loadMore };
|
|
150
168
|
}
|
|
@@ -70,6 +70,7 @@ const MY_PENDING_EPOD_JOBS_QUERY = `
|
|
|
70
70
|
phone
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
myPendingEpodDeliveryJobsCount
|
|
73
74
|
}
|
|
74
75
|
`;
|
|
75
76
|
|
|
@@ -205,9 +206,12 @@ interface ApiPendingJob {
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
/** Type C self-list: the CALLING driver's own jobs still awaiting ePOD capture — feeds the capture flow's job picker. */
|
|
208
|
-
export function useMyPendingEpodDeliveryJobs() {
|
|
209
|
+
export function useMyPendingEpodDeliveryJobs(options: { limit?: number; offset?: number } = {}) {
|
|
210
|
+
const { limit = 20, offset = 0 } = options;
|
|
209
211
|
const graphQLOptions = useGraphQLOptions();
|
|
210
212
|
const [jobs, setJobs] = useState<PendingEpodDeliveryJob[]>([]);
|
|
213
|
+
const [totalJobs, setTotalJobs] = useState(0);
|
|
214
|
+
const [hasMore, setHasMore] = useState(false);
|
|
211
215
|
const [isLoading, setIsLoading] = useState(false);
|
|
212
216
|
const [error, setError] = useState<Error | null>(null);
|
|
213
217
|
|
|
@@ -215,10 +219,13 @@ export function useMyPendingEpodDeliveryJobs() {
|
|
|
215
219
|
setIsLoading(true);
|
|
216
220
|
setError(null);
|
|
217
221
|
try {
|
|
218
|
-
const data = await executeGraphQL<{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
const data = await executeGraphQL<{
|
|
223
|
+
myPendingEpodDeliveryJobs: ApiPendingJob[];
|
|
224
|
+
myPendingEpodDeliveryJobsCount: number;
|
|
225
|
+
}>(graphQLOptions, MY_PENDING_EPOD_JOBS_QUERY, { pagination: { limit, offset } });
|
|
226
|
+
setTotalJobs(data.myPendingEpodDeliveryJobsCount ?? 0);
|
|
227
|
+
setHasMore(
|
|
228
|
+
offset + data.myPendingEpodDeliveryJobs.length < data.myPendingEpodDeliveryJobsCount
|
|
222
229
|
);
|
|
223
230
|
setJobs(
|
|
224
231
|
data.myPendingEpodDeliveryJobs.map((job) => ({
|
|
@@ -240,13 +247,13 @@ export function useMyPendingEpodDeliveryJobs() {
|
|
|
240
247
|
} finally {
|
|
241
248
|
setIsLoading(false);
|
|
242
249
|
}
|
|
243
|
-
}, [graphQLOptions]);
|
|
250
|
+
}, [graphQLOptions, limit, offset]);
|
|
244
251
|
|
|
245
252
|
useEffect(() => {
|
|
246
253
|
void load();
|
|
247
254
|
}, [load]);
|
|
248
255
|
|
|
249
|
-
return { jobs, isLoading, error, refetch: load };
|
|
256
|
+
return { jobs, totalJobs, hasMore, isLoading, error, refetch: load };
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
export interface CaptureEpodArgs {
|