@burdenoff/microfe-movethewheels 2026.703.3 → 2026.703.5
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-D2gaEL1U.js} +16 -15
- package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
- package/dist/CreateOrderPage-CWHx8yys.js +490 -0
- package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
- package/dist/CustomersPage-Bbvwvn8k.js +205 -0
- package/dist/DashboardPage-DrEhNf7N.js +542 -0
- package/dist/DataTable-CqMUD4er.js +340 -0
- package/dist/DocumentsCard-edDkgnJj.js +169 -0
- package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
- package/dist/DriversPage-CHF2ZEzd.js +221 -0
- package/dist/FinancePage-Dn9GrW-1.js +175 -0
- package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
- package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
- package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
- package/dist/InventoryPage-l5A1f753.js +259 -0
- package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
- package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
- package/dist/MetricCard-y3bWHKUQ.js +191 -0
- package/dist/Notice-DOO-tjpV.js +46 -0
- package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
- package/dist/OrdersListPage-DhZJoQeI.js +386 -0
- package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
- package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
- package/dist/ProductsPage-C8sn-1tN.js +186 -0
- package/dist/ReportsPage-D1tb0dcz.js +377 -0
- package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
- package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
- package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
- package/dist/Sparkline-K2RMJ58u.js +16729 -0
- package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
- package/dist/TrackingPage-BZrg4RCu.js +368 -0
- package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
- package/dist/VehiclesPage-D5BELbtM.js +184 -0
- package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
- package/dist/WarehousesPage-C1Y1M867.js +182 -0
- package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
- package/dist/arrow-right-CmKOmIkh.js +11 -0
- package/dist/arrow-up-BJijRLAZ.js +17 -0
- package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
- package/dist/boxes-B_OGvQiW.js +54 -0
- package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
- package/dist/charts-Tj_PPTfA.js +2018 -0
- package/dist/chevron-left-B1rZyWWb.js +8 -0
- package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
- package/dist/chunk-BdsCSvBP.js +20 -0
- package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
- package/dist/circle-check-Cs1OVjtV.js +13 -0
- package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
- package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
- package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
- package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
- package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
- package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
- package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
- package/dist/index.css +2 -0
- package/dist/index.js +150 -172
- package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
- package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
- package/dist/mapMarkers-NCV9MRKR.js +28 -0
- package/dist/maps-yzLqiK_C.js +5070 -0
- package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
- package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
- package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
- package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
- package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
- package/dist/route-CWjPrtAN.js +22 -0
- package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
- package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
- package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
- package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
- package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
- package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
- package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
- package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
- package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
- package/dist/upload-DanJY1Vo.js +21 -0
- package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
- package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
- package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
- package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
- package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
- package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
- package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
- package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
- package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
- package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
- package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
- package/dist/user-check-IMhk-qkX.js +20 -0
- package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
- package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
- package/dist/wrench-Dg-sgH2e.js +8 -0
- package/dist/x-C4ObLVEa.js +11 -0
- package/package.json +5 -1
- package/src/movethewheels/components/DataTable.tsx +252 -84
- package/src/movethewheels/components/DocumentsCard.tsx +102 -0
- package/src/movethewheels/components/GlassPanel.tsx +71 -0
- package/src/movethewheels/components/MetricCard.tsx +171 -73
- package/src/movethewheels/components/Notice.tsx +61 -0
- package/src/movethewheels/components/StatusBadge.tsx +7 -6
- package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
- package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
- package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
- package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
- package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
- package/src/movethewheels/components/charts/chartTokens.ts +43 -0
- package/src/movethewheels/components/charts/index.ts +9 -0
- package/src/movethewheels/components/index.ts +51 -1
- package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
- package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
- package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
- package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
- package/src/movethewheels/components/maps/index.ts +16 -0
- package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
- package/src/movethewheels/components/maps/map-markers.css +121 -0
- package/src/movethewheels/hooks/index.ts +9 -0
- package/src/movethewheels/hooks/useAnalytics.ts +109 -4
- 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 +93 -73
- package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
- package/src/movethewheels/pages/CustomersPage.tsx +53 -28
- package/src/movethewheels/pages/DashboardPage.tsx +238 -12
- package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
- package/src/movethewheels/pages/DriversPage.tsx +59 -31
- package/src/movethewheels/pages/FinancePage.tsx +103 -82
- package/src/movethewheels/pages/InventoryPage.tsx +15 -3
- package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
- package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
- package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
- package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
- package/src/movethewheels/pages/ProductsPage.tsx +52 -28
- package/src/movethewheels/pages/ReportsPage.tsx +88 -4
- package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
- package/src/movethewheels/pages/RoutesPage.tsx +22 -6
- package/src/movethewheels/pages/SettingsPage.tsx +5 -21
- package/src/movethewheels/pages/TrackingPage.tsx +76 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
- package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
- package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
- package/src/movethewheels/types/index.ts +9 -1
- package/src/movethewheels/utils/download.ts +62 -0
- package/src/movethewheels/utils/mapMarkers.ts +58 -0
- package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
- package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
- package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
- package/dist/CustomersPage-BKxMS1AV.js +0 -161
- package/dist/DashboardPage-BVjQi0EB.js +0 -387
- package/dist/DataTable-DmciIKKM.js +0 -249
- package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
- package/dist/DriversPage-CH9rjQC-.js +0 -165
- package/dist/FinancePage-DEDykSOP.js +0 -158
- package/dist/InventoryPage-e0ze63iX.js +0 -253
- package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
- package/dist/MarketplacePage-CIQ97_zp.js +0 -247
- package/dist/MetricCard-BY0QrMD4.js +0 -146
- package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
- package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
- package/dist/ProductsPage-QJfHHpqB.js +0 -169
- package/dist/ReportsPage-Zp53q3_8.js +0 -284
- package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
- package/dist/TrackingPage-W-jtdM3Y.js +0 -325
- package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
- package/dist/VehiclesPage-ApKThYeG.js +0 -165
- package/dist/WarehousesPage-DCbXXXWE.js +0 -162
- package/dist/useAnalytics-DS8n0xgg.js +0 -297
- /package/dist/{formatters-CF2S2l1m.js → formatters-FWJDwTFv.js} +0 -0
|
@@ -7,11 +7,15 @@ interface UseRoutesOptions {
|
|
|
7
7
|
driverId?: string;
|
|
8
8
|
vehicleId?: string;
|
|
9
9
|
status?: 'planned' | 'in_progress' | 'completed' | 'cancelled';
|
|
10
|
+
limit?: number;
|
|
11
|
+
offset?: number;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
interface UseRoutesResult {
|
|
13
15
|
routes: Route[];
|
|
14
16
|
totalRoutes: number;
|
|
17
|
+
/** True when the last fetch returned a full page (rawCount === limit). */
|
|
18
|
+
hasMore: boolean;
|
|
15
19
|
isLoading: boolean;
|
|
16
20
|
error: Error | null;
|
|
17
21
|
refetch: () => void;
|
|
@@ -36,6 +40,7 @@ interface ApiRoute {
|
|
|
36
40
|
waypoints?: unknown;
|
|
37
41
|
estimatedDistance?: number | null;
|
|
38
42
|
estimatedDuration?: number | null;
|
|
43
|
+
fuelCost?: number | null;
|
|
39
44
|
actualDistance?: number | null;
|
|
40
45
|
actualDuration?: number | null;
|
|
41
46
|
createdAt: string;
|
|
@@ -53,6 +58,7 @@ const LIST_ROUTES_QUERY = `
|
|
|
53
58
|
waypoints
|
|
54
59
|
estimatedDistance
|
|
55
60
|
estimatedDuration
|
|
61
|
+
fuelCost
|
|
56
62
|
actualDistance
|
|
57
63
|
actualDuration
|
|
58
64
|
createdAt
|
|
@@ -72,6 +78,7 @@ const GET_ROUTE_QUERY = `
|
|
|
72
78
|
waypoints
|
|
73
79
|
estimatedDistance
|
|
74
80
|
estimatedDuration
|
|
81
|
+
fuelCost
|
|
75
82
|
actualDistance
|
|
76
83
|
actualDuration
|
|
77
84
|
createdAt
|
|
@@ -91,6 +98,7 @@ const CREATE_ROUTE_MUTATION = `
|
|
|
91
98
|
waypoints
|
|
92
99
|
estimatedDistance
|
|
93
100
|
estimatedDuration
|
|
101
|
+
fuelCost
|
|
94
102
|
actualDistance
|
|
95
103
|
actualDuration
|
|
96
104
|
createdAt
|
|
@@ -110,6 +118,7 @@ const UPDATE_ROUTE_MUTATION = `
|
|
|
110
118
|
waypoints
|
|
111
119
|
estimatedDistance
|
|
112
120
|
estimatedDuration
|
|
121
|
+
fuelCost
|
|
113
122
|
actualDistance
|
|
114
123
|
actualDuration
|
|
115
124
|
createdAt
|
|
@@ -135,6 +144,7 @@ const OPTIMIZE_ROUTE_MUTATION = `
|
|
|
135
144
|
waypoints
|
|
136
145
|
estimatedDistance
|
|
137
146
|
estimatedDuration
|
|
147
|
+
fuelCost
|
|
138
148
|
actualDistance
|
|
139
149
|
actualDuration
|
|
140
150
|
createdAt
|
|
@@ -206,6 +216,7 @@ function mapRoute(route: ApiRoute): Route {
|
|
|
206
216
|
stops: mapWaypoints(route.waypoints),
|
|
207
217
|
distance: route.estimatedDistance ?? 0,
|
|
208
218
|
estimatedDuration: route.estimatedDuration ?? 0,
|
|
219
|
+
fuelCost: route.fuelCost ?? undefined,
|
|
209
220
|
actualDuration: route.actualDuration ?? undefined,
|
|
210
221
|
startTime: undefined,
|
|
211
222
|
endTime: undefined,
|
|
@@ -232,9 +243,10 @@ function useGraphqlOptions() {
|
|
|
232
243
|
}
|
|
233
244
|
|
|
234
245
|
export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
|
|
235
|
-
const { driverId, vehicleId, status } = options;
|
|
246
|
+
const { driverId, vehicleId, status, limit = 500, offset = 0 } = options;
|
|
236
247
|
const graphQLOptions = useGraphqlOptions();
|
|
237
248
|
const [routes, setRoutes] = useState<Route[]>([]);
|
|
249
|
+
const [hasMore, setHasMore] = useState(false);
|
|
238
250
|
const [isLoading, setIsLoading] = useState(false);
|
|
239
251
|
const [error, setError] = useState<Error | null>(null);
|
|
240
252
|
|
|
@@ -248,16 +260,17 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
|
|
|
248
260
|
vehicleId,
|
|
249
261
|
status: status ? toApiRouteStatus(status) : undefined,
|
|
250
262
|
},
|
|
251
|
-
pagination: { limit
|
|
263
|
+
pagination: { limit, offset },
|
|
252
264
|
});
|
|
253
265
|
|
|
266
|
+
setHasMore(data.routes.length === limit);
|
|
254
267
|
setRoutes(data.routes.map(mapRoute));
|
|
255
268
|
} catch (fetchError) {
|
|
256
269
|
setError(fetchError instanceof Error ? fetchError : new Error('Failed to load routes'));
|
|
257
270
|
} finally {
|
|
258
271
|
setIsLoading(false);
|
|
259
272
|
}
|
|
260
|
-
}, [driverId, graphQLOptions, status, vehicleId]);
|
|
273
|
+
}, [driverId, graphQLOptions, status, vehicleId, limit, offset]);
|
|
261
274
|
|
|
262
275
|
useEffect(() => {
|
|
263
276
|
void load();
|
|
@@ -282,6 +295,7 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
|
|
|
282
295
|
waypoints: route.stops ?? [],
|
|
283
296
|
estimatedDistance: route.distance ?? 0,
|
|
284
297
|
estimatedDuration: route.estimatedDuration ?? 0,
|
|
298
|
+
fuelCost: route.fuelCost,
|
|
285
299
|
actualDistance: route.distance,
|
|
286
300
|
actualDuration: route.actualDuration,
|
|
287
301
|
},
|
|
@@ -311,6 +325,7 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
|
|
|
311
325
|
waypoints: updates.stops,
|
|
312
326
|
estimatedDistance: updates.distance,
|
|
313
327
|
estimatedDuration: updates.estimatedDuration,
|
|
328
|
+
fuelCost: updates.fuelCost,
|
|
314
329
|
actualDistance: updates.distance,
|
|
315
330
|
actualDuration: updates.actualDuration,
|
|
316
331
|
},
|
|
@@ -376,6 +391,7 @@ export function useRoutes(options: UseRoutesOptions = {}): UseRoutesResult {
|
|
|
376
391
|
return {
|
|
377
392
|
routes,
|
|
378
393
|
totalRoutes: routes.length,
|
|
394
|
+
hasMore,
|
|
379
395
|
isLoading,
|
|
380
396
|
error,
|
|
381
397
|
refetch,
|
|
@@ -446,6 +462,7 @@ export function useRouteStats() {
|
|
|
446
462
|
completedRoutes: routes.filter((route) => route.status === 'completed').length,
|
|
447
463
|
inProgressRoutes: routes.filter((route) => route.status === 'in_progress').length,
|
|
448
464
|
plannedRoutes: routes.filter((route) => route.status === 'planned').length,
|
|
465
|
+
optimizedRoutes: routes.filter((route) => route.optimized).length,
|
|
449
466
|
};
|
|
450
467
|
}, [routes]);
|
|
451
468
|
|
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Analytics Page
|
|
3
|
+
*
|
|
4
|
+
* Real charts (recharts) fed entirely from Query.analytics / orderStats /
|
|
5
|
+
* fleetStats. Metrics we cannot source (e.g. per-lane volume) are simply not
|
|
6
|
+
* shown rather than faked.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import {
|
|
9
|
+
import { useMemo } from 'react';
|
|
10
|
+
import { BarChart3, TrendingUp, Package, DollarSign, Users, Star } from 'lucide-react';
|
|
6
11
|
import { PageLayout } from '../components/PageLayout';
|
|
7
12
|
import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
|
|
8
13
|
import { Card, CardContent, CardHeader, CardTitle, Progress, Button } from '../components/ui';
|
|
14
|
+
import { TrendChart, VolumeBar, StatusDonut, type DonutSlice } from '../components/charts';
|
|
9
15
|
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
10
16
|
import { useAnalytics, useOrderAnalytics, useFleetAnalytics } from '../hooks/useAnalytics';
|
|
11
17
|
import { formatCurrency } from '../utils/formatters';
|
|
12
18
|
|
|
19
|
+
const statusLabel = (status: string) =>
|
|
20
|
+
status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
21
|
+
|
|
13
22
|
export default function AnalyticsPage() {
|
|
14
23
|
const { analytics, isLoading, error, refetch } = useAnalytics();
|
|
15
24
|
const { orderData } = useOrderAnalytics();
|
|
16
25
|
const { fleetData } = useFleetAnalytics();
|
|
17
26
|
|
|
27
|
+
const orderSpark = useMemo(
|
|
28
|
+
() => analytics.trends.dailyOrders.map((d) => d.orders),
|
|
29
|
+
[analytics.trends.dailyOrders]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const revenueData = useMemo(
|
|
33
|
+
() => analytics.trends.revenue.map((r) => ({ month: r.month, revenue: r.revenue })),
|
|
34
|
+
[analytics.trends.revenue]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const statusSlices = useMemo<DonutSlice[]>(
|
|
38
|
+
() =>
|
|
39
|
+
orderData.byStatus
|
|
40
|
+
.filter((s) => s.count > 0)
|
|
41
|
+
.map((s) => ({ name: statusLabel(s.status), value: s.count })),
|
|
42
|
+
[orderData.byStatus]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const statusTotal = statusSlices.reduce((sum, s) => sum + s.value, 0);
|
|
46
|
+
|
|
18
47
|
return (
|
|
19
48
|
<PageLayout
|
|
20
49
|
title="Analytics"
|
|
@@ -38,17 +67,19 @@ export default function AnalyticsPage() {
|
|
|
38
67
|
</CardContent>
|
|
39
68
|
</Card>
|
|
40
69
|
) : isLoading ? (
|
|
41
|
-
<MetricGrid columns={
|
|
42
|
-
{Array.from({ length:
|
|
70
|
+
<MetricGrid columns={4}>
|
|
71
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
43
72
|
<MetricCardSkeleton key={i} />
|
|
44
73
|
))}
|
|
45
74
|
</MetricGrid>
|
|
46
75
|
) : (
|
|
47
|
-
<MetricGrid columns={
|
|
76
|
+
<MetricGrid columns={4}>
|
|
48
77
|
<MetricCard
|
|
49
|
-
label="
|
|
78
|
+
label="Order Volume"
|
|
50
79
|
value={analytics.overview.totalOrders.toLocaleString()}
|
|
51
80
|
icon={<Package size={20} />}
|
|
81
|
+
accent="brand"
|
|
82
|
+
sparklineData={orderSpark.length >= 2 ? orderSpark : undefined}
|
|
52
83
|
/>
|
|
53
84
|
<MetricCard
|
|
54
85
|
treatment="glass"
|
|
@@ -56,99 +87,83 @@ export default function AnalyticsPage() {
|
|
|
56
87
|
label="Revenue"
|
|
57
88
|
value={formatCurrency(analytics.overview.totalRevenue)}
|
|
58
89
|
icon={<DollarSign size={20} />}
|
|
90
|
+
accent="success"
|
|
59
91
|
/>
|
|
60
92
|
<MetricCard
|
|
61
|
-
label="
|
|
62
|
-
value={analytics.overview.activeVehicles}
|
|
63
|
-
icon={<Truck size={20} />}
|
|
64
|
-
/>
|
|
65
|
-
<MetricCard
|
|
66
|
-
label="On-Time Rate"
|
|
93
|
+
label="On-Time Delivery"
|
|
67
94
|
value={`${analytics.overview.onTimeDeliveryRate.toFixed(1)}%`}
|
|
68
95
|
icon={<TrendingUp size={20} />}
|
|
69
|
-
|
|
96
|
+
accent="blue"
|
|
97
|
+
delta={
|
|
98
|
+
analytics.overview.onTimeDeliveryRate >= 90
|
|
99
|
+
? { value: 'On target', direction: 'up' }
|
|
100
|
+
: { value: 'Below 90%', direction: 'down' }
|
|
101
|
+
}
|
|
70
102
|
/>
|
|
71
103
|
<MetricCard
|
|
72
|
-
label="Satisfaction"
|
|
104
|
+
label="Customer Satisfaction"
|
|
73
105
|
value={analytics.overview.customerSatisfaction.toFixed(1)}
|
|
74
|
-
icon={<
|
|
106
|
+
icon={<Star size={20} />}
|
|
107
|
+
accent="warning"
|
|
75
108
|
subtitle="out of 5"
|
|
76
109
|
/>
|
|
77
110
|
</MetricGrid>
|
|
78
111
|
)}
|
|
79
112
|
|
|
113
|
+
{/* Order & delivery trend */}
|
|
114
|
+
<Card>
|
|
115
|
+
<CardHeader>
|
|
116
|
+
<CardTitle>Order & delivery trend</CardTitle>
|
|
117
|
+
</CardHeader>
|
|
118
|
+
<CardContent>
|
|
119
|
+
<TrendChart
|
|
120
|
+
xKey="date"
|
|
121
|
+
height={300}
|
|
122
|
+
data={analytics.trends.dailyOrders}
|
|
123
|
+
series={[
|
|
124
|
+
{ dataKey: 'orders', name: 'Orders' },
|
|
125
|
+
{ dataKey: 'deliveries', name: 'Deliveries' },
|
|
126
|
+
]}
|
|
127
|
+
emptyMessage="No order trend recorded yet"
|
|
128
|
+
/>
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
|
|
80
132
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
81
133
|
<Card>
|
|
82
134
|
<CardHeader>
|
|
83
|
-
<CardTitle>
|
|
135
|
+
<CardTitle>Revenue by month</CardTitle>
|
|
84
136
|
</CardHeader>
|
|
85
137
|
<CardContent>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<div key={t.date} className="text-center p-2 bg-bg-sunken/50 rounded">
|
|
96
|
-
<p className="text-xs text-text-secondary">
|
|
97
|
-
{t.date.split('-').slice(1).join('/')}
|
|
98
|
-
</p>
|
|
99
|
-
<p className="font-medium">{t.orders}</p>
|
|
100
|
-
</div>
|
|
101
|
-
))}
|
|
102
|
-
</div>
|
|
138
|
+
<VolumeBar
|
|
139
|
+
xKey="month"
|
|
140
|
+
height={280}
|
|
141
|
+
showLegend={false}
|
|
142
|
+
data={revenueData}
|
|
143
|
+
series={[{ dataKey: 'revenue', name: 'Revenue' }]}
|
|
144
|
+
valueFormatter={(v) => formatCurrency(Number(v))}
|
|
145
|
+
emptyMessage="No revenue recorded yet"
|
|
146
|
+
/>
|
|
103
147
|
</CardContent>
|
|
104
148
|
</Card>
|
|
105
149
|
|
|
106
150
|
<Card>
|
|
107
151
|
<CardHeader>
|
|
108
|
-
<CardTitle>
|
|
152
|
+
<CardTitle>Orders by status</CardTitle>
|
|
109
153
|
</CardHeader>
|
|
110
154
|
<CardContent>
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{analytics.trends.revenue.map((r) => (
|
|
119
|
-
<div key={r.month} className="text-center p-2 bg-bg-sunken/50 rounded">
|
|
120
|
-
<p className="text-xs text-text-secondary">{r.month}</p>
|
|
121
|
-
<p className="font-medium text-sm">{formatCurrency(r.revenue / 1000)}k</p>
|
|
122
|
-
</div>
|
|
123
|
-
))}
|
|
124
|
-
</div>
|
|
155
|
+
<StatusDonut
|
|
156
|
+
data={statusSlices}
|
|
157
|
+
height={240}
|
|
158
|
+
centerLabel={statusTotal.toLocaleString()}
|
|
159
|
+
centerSubLabel="orders"
|
|
160
|
+
emptyMessage="No orders to break down yet"
|
|
161
|
+
/>
|
|
125
162
|
</CardContent>
|
|
126
163
|
</Card>
|
|
127
164
|
</div>
|
|
128
165
|
|
|
129
|
-
<div className="grid grid-cols-1 lg:grid-cols-
|
|
130
|
-
<Card>
|
|
131
|
-
<CardHeader>
|
|
132
|
-
<CardTitle>Orders by Status</CardTitle>
|
|
133
|
-
</CardHeader>
|
|
134
|
-
<CardContent className="space-y-3">
|
|
135
|
-
{orderData.byStatus
|
|
136
|
-
.filter((s) => s.count > 0)
|
|
137
|
-
.map(({ status, count }) => (
|
|
138
|
-
<div key={status} className="flex items-center justify-between">
|
|
139
|
-
<span className="text-sm capitalize">{status.replace('_', ' ')}</span>
|
|
140
|
-
<div className="flex items-center gap-2">
|
|
141
|
-
<Progress
|
|
142
|
-
value={(count / analytics.overview.totalOrders) * 100}
|
|
143
|
-
className="w-20 h-2"
|
|
144
|
-
/>
|
|
145
|
-
<span className="font-medium w-8 text-right">{count}</span>
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
))}
|
|
149
|
-
</CardContent>
|
|
150
|
-
</Card>
|
|
151
|
-
|
|
166
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
152
167
|
<Card>
|
|
153
168
|
<CardHeader>
|
|
154
169
|
<CardTitle>Fleet Performance</CardTitle>
|
|
@@ -188,9 +203,14 @@ export default function AnalyticsPage() {
|
|
|
188
203
|
{analytics.overview.customerSatisfaction.toFixed(1)}/5
|
|
189
204
|
</p>
|
|
190
205
|
</div>
|
|
191
|
-
<div>
|
|
192
|
-
<
|
|
193
|
-
<
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
<Users size={16} className="text-text-secondary" />
|
|
208
|
+
<div>
|
|
209
|
+
<p className="text-sm text-text-secondary">Driver Ratings</p>
|
|
210
|
+
<p className="text-2xl font-bold">
|
|
211
|
+
{analytics.performance.driverRatings.toFixed(1)}/5
|
|
212
|
+
</p>
|
|
213
|
+
</div>
|
|
194
214
|
</div>
|
|
195
215
|
</EmphasisPanel>
|
|
196
216
|
</div>
|
|
@@ -8,6 +8,7 @@ import { useState } from 'react';
|
|
|
8
8
|
import { useParams } from 'react-router-dom';
|
|
9
9
|
import { Package, ArrowLeft, Save, Plus, Trash2, Users, MapPin } from 'lucide-react';
|
|
10
10
|
import { PageLayout, EmptyState } from '../components/PageLayout';
|
|
11
|
+
import { FormError } from '../components/FormError';
|
|
11
12
|
import {
|
|
12
13
|
Card,
|
|
13
14
|
CardContent,
|
|
@@ -62,6 +63,7 @@ export default function CreateOrderPage() {
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
66
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
65
67
|
|
|
66
68
|
const goBack = () => {
|
|
67
69
|
if (navigate) {
|
|
@@ -69,27 +71,49 @@ export default function CreateOrderPage() {
|
|
|
69
71
|
}
|
|
70
72
|
};
|
|
71
73
|
|
|
74
|
+
const validate = (): string | null => {
|
|
75
|
+
const selectedCustomer = customers.find((c) => c.id === formData.customerId);
|
|
76
|
+
if (!formData.customerId || !selectedCustomer) {
|
|
77
|
+
return 'Select a customer for this order.';
|
|
78
|
+
}
|
|
79
|
+
if (!formData.pickupAddress.street.trim()) {
|
|
80
|
+
return 'Enter a pickup street address.';
|
|
81
|
+
}
|
|
82
|
+
if (!formData.deliveryAddress.street.trim()) {
|
|
83
|
+
return 'Enter a delivery street address.';
|
|
84
|
+
}
|
|
85
|
+
if (formData.items.length === 0) {
|
|
86
|
+
return 'Add at least one item to the order.';
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
72
91
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
73
92
|
e.preventDefault();
|
|
74
|
-
|
|
93
|
+
setFormError(null);
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
const validationError = validate();
|
|
96
|
+
if (validationError) {
|
|
97
|
+
setFormError(validationError);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const selectedCustomer = customers.find((c) => c.id === formData.customerId);
|
|
102
|
+
if (!selectedCustomer) {
|
|
103
|
+
setFormError('Select a customer for this order.');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
78
106
|
|
|
107
|
+
setIsSubmitting(true);
|
|
108
|
+
try {
|
|
79
109
|
await createOrder({
|
|
80
110
|
customerId: formData.customerId,
|
|
81
|
-
customerInfo:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
: {
|
|
89
|
-
name: 'New Customer',
|
|
90
|
-
email: 'customer@example.com',
|
|
91
|
-
phone: '+1-555-0000',
|
|
92
|
-
},
|
|
111
|
+
customerInfo: {
|
|
112
|
+
name: selectedCustomer.name,
|
|
113
|
+
email: selectedCustomer.email,
|
|
114
|
+
phone: selectedCustomer.phone,
|
|
115
|
+
company: selectedCustomer.company,
|
|
116
|
+
},
|
|
93
117
|
orderType: formData.orderType as 'b2b' | 'b2c',
|
|
94
118
|
priority: formData.priority as 'low' | 'normal' | 'high' | 'urgent',
|
|
95
119
|
pickupAddress: formData.pickupAddress,
|
|
@@ -100,7 +124,9 @@ export default function CreateOrderPage() {
|
|
|
100
124
|
|
|
101
125
|
goBack();
|
|
102
126
|
} catch (error) {
|
|
103
|
-
|
|
127
|
+
setFormError(
|
|
128
|
+
error instanceof Error ? error.message : 'Could not create the order. Please try again.'
|
|
129
|
+
);
|
|
104
130
|
} finally {
|
|
105
131
|
setIsSubmitting(false);
|
|
106
132
|
}
|
|
@@ -209,6 +235,8 @@ export default function CreateOrderPage() {
|
|
|
209
235
|
/>
|
|
210
236
|
|
|
211
237
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
238
|
+
<FormError message={formError} />
|
|
239
|
+
|
|
212
240
|
{/* Order Details — the page's single emphasis zone (the form's starting point) */}
|
|
213
241
|
<EmphasisPanel>
|
|
214
242
|
<h2 className="text-base font-semibold text-text-primary mb-4">Order Details</h2>
|
|
@@ -285,7 +313,7 @@ export default function CreateOrderPage() {
|
|
|
285
313
|
placeholder="123 Main St"
|
|
286
314
|
/>
|
|
287
315
|
</div>
|
|
288
|
-
<div className="grid grid-cols-2 gap-4">
|
|
316
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
289
317
|
<div className="space-y-2">
|
|
290
318
|
<Label htmlFor="pickupCity">City</Label>
|
|
291
319
|
<Input
|
|
@@ -305,7 +333,7 @@ export default function CreateOrderPage() {
|
|
|
305
333
|
/>
|
|
306
334
|
</div>
|
|
307
335
|
</div>
|
|
308
|
-
<div className="grid grid-cols-2 gap-4">
|
|
336
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
309
337
|
<div className="space-y-2">
|
|
310
338
|
<Label htmlFor="pickupPostal">Postal Code</Label>
|
|
311
339
|
<Input
|
|
@@ -343,7 +371,7 @@ export default function CreateOrderPage() {
|
|
|
343
371
|
placeholder="456 Oak Ave"
|
|
344
372
|
/>
|
|
345
373
|
</div>
|
|
346
|
-
<div className="grid grid-cols-2 gap-4">
|
|
374
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
347
375
|
<div className="space-y-2">
|
|
348
376
|
<Label htmlFor="deliveryCity">City</Label>
|
|
349
377
|
<Input
|
|
@@ -363,7 +391,7 @@ export default function CreateOrderPage() {
|
|
|
363
391
|
/>
|
|
364
392
|
</div>
|
|
365
393
|
</div>
|
|
366
|
-
<div className="grid grid-cols-2 gap-4">
|
|
394
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
367
395
|
<div className="space-y-2">
|
|
368
396
|
<Label htmlFor="deliveryPostal">Postal Code</Label>
|
|
369
397
|
<Input
|