@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.
Files changed (173) hide show
  1. package/dist/{AIAssistantPage-C3cPsWt-.js → AIAssistantPage-D2gaEL1U.js} +16 -15
  2. package/dist/AnalyticsPage-DZMlEoBi.js +194 -0
  3. package/dist/CreateOrderPage-CWHx8yys.js +490 -0
  4. package/dist/CustomerDetailsPage-nR4oZ_ju.js +615 -0
  5. package/dist/CustomersPage-Bbvwvn8k.js +205 -0
  6. package/dist/DashboardPage-DrEhNf7N.js +542 -0
  7. package/dist/DataTable-CqMUD4er.js +340 -0
  8. package/dist/DocumentsCard-edDkgnJj.js +169 -0
  9. package/dist/DriverDetailsPage-whEMOWs9.js +490 -0
  10. package/dist/DriversPage-CHF2ZEzd.js +221 -0
  11. package/dist/FinancePage-Dn9GrW-1.js +175 -0
  12. package/dist/{FleetPage-DqkFi3s3.js → FleetPage-CrtGPy-p.js} +36 -35
  13. package/dist/{FormError-BlOjnXOq.js → FormError-BKcb_wT-.js} +1 -1
  14. package/dist/{ImportExportPage-DKJRCezl.js → ImportExportPage-WYdwZomZ.js} +52 -72
  15. package/dist/InventoryPage-l5A1f753.js +259 -0
  16. package/dist/LiveTrackingPage-DhEaNKZ4.js +429 -0
  17. package/dist/MarketplacePage-B9RYOb1Y.js +426 -0
  18. package/dist/MetricCard-y3bWHKUQ.js +191 -0
  19. package/dist/Notice-DOO-tjpV.js +46 -0
  20. package/dist/OrderDetailsPage-CwYHwk1K.js +584 -0
  21. package/dist/OrdersListPage-DhZJoQeI.js +386 -0
  22. package/dist/{PageLayout-D4LDHJJq.js → PageLayout-BniRJaQ6.js} +1 -1
  23. package/dist/{ProductDetailsPage-C9uOiYLp.js → ProductDetailsPage-1Gm9JKy0.js} +56 -56
  24. package/dist/ProductsPage-C8sn-1tN.js +186 -0
  25. package/dist/ReportsPage-D1tb0dcz.js +377 -0
  26. package/dist/RouteDetailsPage-CkMyR9b_.js +610 -0
  27. package/dist/{RoutesPage-DMfoicM1.js → RoutesPage-QkENzsE5.js} +58 -47
  28. package/dist/{SettingsPage-Bn-v0mbQ.js → SettingsPage-DV3_tU_X.js} +16 -38
  29. package/dist/Sparkline-K2RMJ58u.js +16729 -0
  30. package/dist/{StatusBadge-BZurYHMy.js → StatusBadge-BNHXYvZx.js} +7 -7
  31. package/dist/TrackingPage-BZrg4RCu.js +368 -0
  32. package/dist/VehicleDetailsPage-D-8XPZ2G.js +393 -0
  33. package/dist/VehiclesPage-D5BELbtM.js +184 -0
  34. package/dist/{WarehouseDetailsPage-L_6sWigM.js → WarehouseDetailsPage-Cmg-8Frn.js} +70 -70
  35. package/dist/WarehousesPage-C1Y1M867.js +182 -0
  36. package/dist/{arrow-left-n48Y-Wis.js → arrow-left-ClW4Isj7.js} +1 -1
  37. package/dist/arrow-right-CmKOmIkh.js +11 -0
  38. package/dist/arrow-up-BJijRLAZ.js +17 -0
  39. package/dist/{box-D9Pal4HM.js → box-BT3NsRQP.js} +1 -1
  40. package/dist/boxes-B_OGvQiW.js +54 -0
  41. package/dist/{chart-column-BFIJysDR.js → chart-column-DQTTdUPk.js} +1 -1
  42. package/dist/charts-Tj_PPTfA.js +2018 -0
  43. package/dist/chevron-left-B1rZyWWb.js +8 -0
  44. package/dist/{chevron-right-EMIIyyJ1.js → chevron-right-BIcwUJZq.js} +1 -1
  45. package/dist/chunk-BdsCSvBP.js +20 -0
  46. package/dist/{circle-alert-iY24lSlp.js → circle-alert-CNkuDNc6.js} +1 -1
  47. package/dist/circle-check-Cs1OVjtV.js +13 -0
  48. package/dist/{circle-check-big-w0TrLKMF.js → circle-check-big-C-p4eNwn.js} +1 -1
  49. package/dist/{clock-DJZtdl8u.js → clock-dRGG4rkF.js} +1 -1
  50. package/dist/{dollar-sign-CgKQRz_e.js → dollar-sign-B6RoYHHz.js} +1 -1
  51. package/dist/{download-DRXxoiQR.js → download-DP04iWTd.js} +1 -1
  52. package/dist/{file-text-DWWY6_f5.js → file-text-BuDx4hZp.js} +1 -1
  53. package/dist/{filter-DloLC2eE.js → filter-BD483_8g.js} +1 -1
  54. package/dist/{wrench-CANXWxF-.js → fuel-DLH1ag04.js} +3 -6
  55. package/dist/index.css +2 -0
  56. package/dist/index.js +150 -172
  57. package/dist/{map-CkJaULb2.js → map-DNQn9fyY.js} +1 -1
  58. package/dist/{map-pin-BGiCvyCs.js → map-pin-CqN1bbT1.js} +1 -1
  59. package/dist/mapMarkers-NCV9MRKR.js +28 -0
  60. package/dist/maps-yzLqiK_C.js +5070 -0
  61. package/dist/{navigation-D36prnAn.js → navigation-CgGUt2tR.js} +1 -1
  62. package/dist/{package-BChUzpJS.js → package-DmRF7iQD.js} +1 -1
  63. package/dist/{phone-Ac9s3mo_.js → phone-Cl32EXKW.js} +1 -1
  64. package/dist/{plus-Z76q-FZm.js → plus-DlANgOFx.js} +1 -1
  65. package/dist/{refresh-cw-DKt6f5T6.js → refresh-cw-rW59D4b7.js} +1 -1
  66. package/dist/route-CWjPrtAN.js +22 -0
  67. package/dist/{save-CojywNhG.js → save-UDlog7hg.js} +1 -1
  68. package/dist/{search-CDGefqb3.js → search-DNIG5Ck7.js} +1 -1
  69. package/dist/{square-pen-CopE9GmB.js → square-pen-BcfB3Grd.js} +1 -1
  70. package/dist/{star-nHlLjDKR.js → star-CVZHFwDx.js} +1 -1
  71. package/dist/{store-BR9Voi5E.js → store-bwnmCK9f.js} +1 -1
  72. package/dist/{trash-2-BKhar6Hs.js → trash-2-B80G12d4.js} +1 -1
  73. package/dist/{trending-up-Dohgd4t_.js → trending-up-CH4R4KOa.js} +1 -1
  74. package/dist/{triangle-alert-i-XQL9Rd.js → triangle-alert-CmS8AUU6.js} +1 -1
  75. package/dist/{truck-C0v_hbLR.js → truck-9rLDncav.js} +1 -1
  76. package/dist/upload-DanJY1Vo.js +21 -0
  77. package/dist/{useAssistant-D4v3oxD3.js → useAssistant-CnEwkTLm.js} +11 -28
  78. package/dist/{useCustomers-BmTNGxiI.js → useCustomers-ChJao1ET.js} +30 -27
  79. package/dist/{useFleet-DKqXZ16m.js → useFleet-g5Yri0G8.js} +82 -70
  80. package/dist/{useImportExport-DHr-SWSw.js → useImportExport-MYwIT3rE.js} +1 -1
  81. package/dist/{useInventory-DkyiVY_A.js → useInventory-BSOn-R1c.js} +84 -39
  82. package/dist/{useOrders-Cfk1GLiq.js → useOrders-BSiqo61w.js} +59 -48
  83. package/dist/{useReports-CTqZr-AB.js → useReports-fWESfsF9.js} +41 -35
  84. package/dist/{useRoutes-BQWO607N.js → useRoutes-zD8Yp323.js} +50 -42
  85. package/dist/{useSettings-CkDH_-gM.js → useSettings-B1lsWeZ1.js} +6 -6
  86. package/dist/{useTracking-B-va1kO3.js → useTracking-BKG0I2ln.js} +3 -3
  87. package/dist/{user-jyFU3lXk.js → user-DU51el3m.js} +1 -1
  88. package/dist/user-check-IMhk-qkX.js +20 -0
  89. package/dist/{users-DZ-ilS6B.js → users-B8KhJBGg.js} +1 -1
  90. package/dist/{warehouse-6m5kXMLu.js → warehouse-CcMMioeY.js} +1 -1
  91. package/dist/wrench-Dg-sgH2e.js +8 -0
  92. package/dist/x-C4ObLVEa.js +11 -0
  93. package/package.json +5 -1
  94. package/src/movethewheels/components/DataTable.tsx +252 -84
  95. package/src/movethewheels/components/DocumentsCard.tsx +102 -0
  96. package/src/movethewheels/components/GlassPanel.tsx +71 -0
  97. package/src/movethewheels/components/MetricCard.tsx +171 -73
  98. package/src/movethewheels/components/Notice.tsx +61 -0
  99. package/src/movethewheels/components/StatusBadge.tsx +7 -6
  100. package/src/movethewheels/components/charts/ChartShell.tsx +78 -0
  101. package/src/movethewheels/components/charts/Sparkline.tsx +85 -0
  102. package/src/movethewheels/components/charts/StatusDonut.tsx +115 -0
  103. package/src/movethewheels/components/charts/TrendChart.tsx +179 -0
  104. package/src/movethewheels/components/charts/VolumeBar.tsx +97 -0
  105. package/src/movethewheels/components/charts/chartTokens.ts +43 -0
  106. package/src/movethewheels/components/charts/index.ts +9 -0
  107. package/src/movethewheels/components/index.ts +51 -1
  108. package/src/movethewheels/components/maps/LogisticsMap.tsx +94 -0
  109. package/src/movethewheels/components/maps/MapCanvas.tsx +118 -0
  110. package/src/movethewheels/components/maps/RouteMap.tsx +71 -0
  111. package/src/movethewheels/components/maps/TrackingMap.tsx +102 -0
  112. package/src/movethewheels/components/maps/index.ts +16 -0
  113. package/src/movethewheels/components/maps/leaflet-setup.ts +124 -0
  114. package/src/movethewheels/components/maps/map-markers.css +121 -0
  115. package/src/movethewheels/hooks/index.ts +9 -0
  116. package/src/movethewheels/hooks/useAnalytics.ts +109 -4
  117. package/src/movethewheels/hooks/useCustomers.ts +11 -4
  118. package/src/movethewheels/hooks/useEntityDocuments.ts +132 -0
  119. package/src/movethewheels/hooks/useFleet.ts +26 -7
  120. package/src/movethewheels/hooks/useInventory.ts +70 -7
  121. package/src/movethewheels/hooks/useOrders.ts +32 -2
  122. package/src/movethewheels/hooks/useReports.ts +14 -4
  123. package/src/movethewheels/hooks/useRoutes.ts +20 -3
  124. package/src/movethewheels/pages/AnalyticsPage.tsx +93 -73
  125. package/src/movethewheels/pages/CreateOrderPage.tsx +48 -20
  126. package/src/movethewheels/pages/CustomerDetailsPage.tsx +239 -33
  127. package/src/movethewheels/pages/CustomersPage.tsx +53 -28
  128. package/src/movethewheels/pages/DashboardPage.tsx +238 -12
  129. package/src/movethewheels/pages/DriverDetailsPage.tsx +17 -31
  130. package/src/movethewheels/pages/DriversPage.tsx +59 -31
  131. package/src/movethewheels/pages/FinancePage.tsx +103 -82
  132. package/src/movethewheels/pages/InventoryPage.tsx +15 -3
  133. package/src/movethewheels/pages/LiveTrackingPage.tsx +124 -16
  134. package/src/movethewheels/pages/MarketplacePage.tsx +265 -50
  135. package/src/movethewheels/pages/OrderDetailsPage.tsx +214 -10
  136. package/src/movethewheels/pages/OrdersListPage.tsx +118 -47
  137. package/src/movethewheels/pages/ProductsPage.tsx +52 -28
  138. package/src/movethewheels/pages/ReportsPage.tsx +88 -4
  139. package/src/movethewheels/pages/RouteDetailsPage.tsx +119 -19
  140. package/src/movethewheels/pages/RoutesPage.tsx +22 -6
  141. package/src/movethewheels/pages/SettingsPage.tsx +5 -21
  142. package/src/movethewheels/pages/TrackingPage.tsx +76 -6
  143. package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -34
  144. package/src/movethewheels/pages/VehiclesPage.tsx +58 -27
  145. package/src/movethewheels/pages/WarehouseDetailsPage.tsx +1 -1
  146. package/src/movethewheels/pages/WarehousesPage.tsx +47 -36
  147. package/src/movethewheels/types/index.ts +9 -1
  148. package/src/movethewheels/utils/download.ts +62 -0
  149. package/src/movethewheels/utils/mapMarkers.ts +58 -0
  150. package/dist/AnalyticsPage-Dz6g2MvK.js +0 -215
  151. package/dist/CreateOrderPage-C5iwnjp4.js +0 -479
  152. package/dist/CustomerDetailsPage-B6nwFyLO.js +0 -406
  153. package/dist/CustomersPage-BKxMS1AV.js +0 -161
  154. package/dist/DashboardPage-BVjQi0EB.js +0 -387
  155. package/dist/DataTable-DmciIKKM.js +0 -249
  156. package/dist/DriverDetailsPage-CN3WQ0B9.js +0 -507
  157. package/dist/DriversPage-CH9rjQC-.js +0 -165
  158. package/dist/FinancePage-DEDykSOP.js +0 -158
  159. package/dist/InventoryPage-e0ze63iX.js +0 -253
  160. package/dist/LiveTrackingPage-DCX-K-LR.js +0 -379
  161. package/dist/MarketplacePage-CIQ97_zp.js +0 -247
  162. package/dist/MetricCard-BY0QrMD4.js +0 -146
  163. package/dist/OrderDetailsPage-DYiZ-9UK.js +0 -429
  164. package/dist/OrdersListPage-Cn90P1Xx.js +0 -307
  165. package/dist/ProductsPage-QJfHHpqB.js +0 -169
  166. package/dist/ReportsPage-Zp53q3_8.js +0 -284
  167. package/dist/RouteDetailsPage-DUStXSf9.js +0 -531
  168. package/dist/TrackingPage-W-jtdM3Y.js +0 -325
  169. package/dist/VehicleDetailsPage-Bceo5oz3.js +0 -410
  170. package/dist/VehiclesPage-ApKThYeG.js +0 -165
  171. package/dist/WarehousesPage-DCbXXXWE.js +0 -162
  172. package/dist/useAnalytics-DS8n0xgg.js +0 -297
  173. /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: 500, offset: 0 },
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 { BarChart3, TrendingUp, Package, Truck, DollarSign, Users } from 'lucide-react';
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={5}>
42
- {Array.from({ length: 5 }).map((_, i) => (
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={5}>
76
+ <MetricGrid columns={4}>
48
77
  <MetricCard
49
- label="Total Orders"
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="Active Vehicles"
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
- changeType={analytics.overview.onTimeDeliveryRate >= 90 ? 'positive' : 'neutral'}
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={<Users size={20} />}
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 &amp; 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>Order Trends</CardTitle>
135
+ <CardTitle>Revenue by month</CardTitle>
84
136
  </CardHeader>
85
137
  <CardContent>
86
- <div className="h-64 flex items-center justify-center bg-bg-sunken/50 rounded-lg">
87
- <div className="text-center text-text-secondary">
88
- <BarChart3 size={48} className="mx-auto mb-2 opacity-50" />
89
- <p>Order trend chart</p>
90
- <p className="text-xs">(Chart library integration needed)</p>
91
- </div>
92
- </div>
93
- <div className="mt-4 grid grid-cols-4 gap-2">
94
- {orderData.trends.slice(-4).map((t) => (
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>Revenue Trends</CardTitle>
152
+ <CardTitle>Orders by status</CardTitle>
109
153
  </CardHeader>
110
154
  <CardContent>
111
- <div className="h-64 flex items-center justify-center bg-bg-sunken/50 rounded-lg">
112
- <div className="text-center text-text-secondary">
113
- <TrendingUp size={48} className="mx-auto mb-2 opacity-50" />
114
- <p>Revenue trend chart</p>
115
- </div>
116
- </div>
117
- <div className="mt-4 grid grid-cols-5 gap-2">
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-3 gap-6">
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
- <p className="text-sm text-text-secondary">Driver Ratings</p>
193
- <p className="text-2xl font-bold">{analytics.performance.driverRatings.toFixed(1)}/5</p>
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
- setIsSubmitting(true);
93
+ setFormError(null);
75
94
 
76
- try {
77
- const selectedCustomer = customers.find((c) => c.id === formData.customerId);
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: selectedCustomer
82
- ? {
83
- name: selectedCustomer.name,
84
- email: selectedCustomer.email,
85
- phone: selectedCustomer.phone,
86
- company: selectedCustomer.company,
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
- console.error('Failed to create order:', error);
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