@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
@@ -2,7 +2,7 @@
2
2
  * Customer Details Page
3
3
  */
4
4
 
5
- import { useState } from 'react';
5
+ import { useEffect, useState } from 'react';
6
6
  import { useParams } from 'react-router-dom';
7
7
  import {
8
8
  Users,
@@ -15,9 +15,11 @@ import {
15
15
  Building,
16
16
  FileText,
17
17
  Save,
18
+ Package,
19
+ X,
18
20
  } from 'lucide-react';
19
21
  import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
20
- import { TypeBadge } from '../components/StatusBadge';
22
+ import { TypeBadge, StatusBadge } from '../components/StatusBadge';
21
23
  import {
22
24
  Card,
23
25
  CardContent,
@@ -35,10 +37,28 @@ import {
35
37
  } from '../components/ui';
36
38
  import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
37
39
  import { useCustomer, useCustomers } from '../hooks/useCustomers';
40
+ import { useOrders } from '../hooks/useOrders';
38
41
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
39
- import { formatAddress, formatDate } from '../utils/formatters';
42
+ import { formatAddress, formatCurrency, formatDate } from '../utils/formatters';
40
43
  import { joinPath } from '../utils/navigation';
41
44
  import { FormError } from '../components/FormError';
45
+ import { Notice, useNotice } from '../components/Notice';
46
+
47
+ /**
48
+ * Only surface http(s) links. Anything else (javascript:, data:, mailto:, etc.)
49
+ * is unsafe to render as a raw href, so we drop it.
50
+ */
51
+ function safeHttpUrl(raw: string | undefined): string | null {
52
+ if (!raw) return null;
53
+ const value = raw.trim();
54
+ const candidate = /^https?:\/\//i.test(value) ? value : `https://${value}`;
55
+ try {
56
+ const parsed = new URL(candidate);
57
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.href : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
42
62
 
43
63
  function NewCustomerForm() {
44
64
  const { basePath, navigate } = useMoveTheWheels();
@@ -177,10 +197,77 @@ export default function CustomerDetailsPage() {
177
197
  const { customerId } = useParams<{ customerId: string }>();
178
198
  const { basePath, navigate } = useMoveTheWheels();
179
199
  const { customer, isLoading, error, refetch } = useCustomer(customerId);
200
+ const { updateCustomer } = useCustomers();
201
+ const { orders, isLoading: ordersLoading } = useOrders({ filters: { customerId } });
202
+ const { message: notice, notify } = useNotice();
203
+
204
+ const [editing, setEditing] = useState(false);
205
+ const [saving, setSaving] = useState(false);
206
+ const [editError, setEditError] = useState<string | null>(null);
207
+ const [form, setForm] = useState({
208
+ name: '',
209
+ email: '',
210
+ phone: '',
211
+ company: '',
212
+ });
213
+
214
+ useEffect(() => {
215
+ if (customer) {
216
+ setForm({
217
+ name: customer.name,
218
+ email: customer.email,
219
+ phone: customer.phone,
220
+ company: customer.company ?? '',
221
+ });
222
+ }
223
+ }, [customer]);
180
224
 
181
225
  const goBack = () => navigate?.(joinPath(basePath, 'customers'));
182
226
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
183
227
 
228
+ const startEdit = () => {
229
+ setEditError(null);
230
+ setEditing(true);
231
+ };
232
+
233
+ const cancelEdit = () => {
234
+ setEditError(null);
235
+ setEditing(false);
236
+ if (customer) {
237
+ setForm({
238
+ name: customer.name,
239
+ email: customer.email,
240
+ phone: customer.phone,
241
+ company: customer.company ?? '',
242
+ });
243
+ }
244
+ };
245
+
246
+ const handleSave = async () => {
247
+ if (!customerId) return;
248
+ if (!form.name.trim()) {
249
+ setEditError('Customer name is required.');
250
+ return;
251
+ }
252
+ setSaving(true);
253
+ setEditError(null);
254
+ try {
255
+ await updateCustomer(customerId, {
256
+ name: form.name.trim(),
257
+ email: form.email.trim(),
258
+ phone: form.phone.trim(),
259
+ company: form.company.trim() || undefined,
260
+ });
261
+ notify('Customer updated.');
262
+ setEditing(false);
263
+ refetch();
264
+ } catch (err) {
265
+ setEditError(err instanceof Error ? err.message : 'Failed to update customer.');
266
+ } finally {
267
+ setSaving(false);
268
+ }
269
+ };
270
+
184
271
  // Handle new customer form
185
272
  if (!customerId) {
186
273
  return <NewCustomerForm />;
@@ -199,9 +286,20 @@ export default function CustomerDetailsPage() {
199
286
  <Button variant="outline" onClick={goBack}>
200
287
  <ArrowLeft size={16} /> Back
201
288
  </Button>
202
- <Button>
203
- <Edit size={16} /> Edit
204
- </Button>
289
+ {editing ? (
290
+ <>
291
+ <Button variant="outline" onClick={cancelEdit} disabled={saving}>
292
+ <X size={16} /> Cancel
293
+ </Button>
294
+ <Button onClick={handleSave} disabled={saving}>
295
+ <Save size={16} /> {saving ? 'Saving…' : 'Save'}
296
+ </Button>
297
+ </>
298
+ ) : (
299
+ <Button onClick={startEdit}>
300
+ <Edit size={16} /> Edit
301
+ </Button>
302
+ )}
205
303
  </div>
206
304
  }
207
305
  headerContent={
@@ -238,53 +336,112 @@ export default function CustomerDetailsPage() {
238
336
  label: 'Update contact details',
239
337
  description: 'Keep email, phone and address current.',
240
338
  icon: <Edit size={16} />,
241
- onClick: () => {},
339
+ onClick: startEdit,
242
340
  },
243
341
  ]}
244
342
  />
245
343
 
344
+ {notice && (
345
+ <div className="mb-2">
346
+ <Notice message={notice} />
347
+ </div>
348
+ )}
349
+
246
350
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
247
351
  <div className="lg:col-span-2 space-y-6">
248
352
  {/* Contact Information — the page's single emphasis zone */}
249
353
  <EmphasisPanel>
250
354
  <h2 className="text-base font-semibold text-text-primary mb-4">Contact Information</h2>
251
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
355
+ {editing ? (
252
356
  <div className="space-y-4">
253
- <div className="flex items-center gap-3">
254
- <Mail size={18} className="text-text-secondary" />
255
- <div>
256
- <p className="text-sm text-text-secondary">Email</p>
257
- <p className="font-medium">{customer.email}</p>
357
+ <FormError message={editError} />
358
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
359
+ <div className="space-y-2">
360
+ <Label htmlFor="edit-name">
361
+ Name <span className="text-status-error-text">*</span>
362
+ </Label>
363
+ <Input
364
+ id="edit-name"
365
+ value={form.name}
366
+ onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
367
+ />
258
368
  </div>
259
- </div>
260
- <div className="flex items-center gap-3">
261
- <Phone size={18} className="text-text-secondary" />
262
- <div>
263
- <p className="text-sm text-text-secondary">Phone</p>
264
- <p className="font-medium">{customer.phone}</p>
369
+ <div className="space-y-2">
370
+ <Label htmlFor="edit-company">Company</Label>
371
+ <Input
372
+ id="edit-company"
373
+ value={form.company}
374
+ onChange={(e) => setForm((prev) => ({ ...prev, company: e.target.value }))}
375
+ placeholder="Company name (for B2B accounts)"
376
+ />
377
+ </div>
378
+ <div className="space-y-2">
379
+ <Label htmlFor="edit-email">Email</Label>
380
+ <Input
381
+ id="edit-email"
382
+ type="email"
383
+ value={form.email}
384
+ onChange={(e) => setForm((prev) => ({ ...prev, email: e.target.value }))}
385
+ />
386
+ </div>
387
+ <div className="space-y-2">
388
+ <Label htmlFor="edit-phone">Phone</Label>
389
+ <Input
390
+ id="edit-phone"
391
+ value={form.phone}
392
+ onChange={(e) => setForm((prev) => ({ ...prev, phone: e.target.value }))}
393
+ />
265
394
  </div>
266
395
  </div>
267
396
  </div>
268
- <div className="space-y-4">
269
- {customer.company && (
397
+ ) : (
398
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
399
+ <div className="space-y-4">
270
400
  <div className="flex items-center gap-3">
271
- <Building size={18} className="text-text-secondary" />
401
+ <Mail size={18} className="text-text-secondary" />
272
402
  <div>
273
- <p className="text-sm text-text-secondary">Company</p>
274
- <p className="font-medium">{customer.company}</p>
403
+ <p className="text-sm text-text-secondary">Email</p>
404
+ <p className="font-medium">{customer.email || '—'}</p>
275
405
  </div>
276
406
  </div>
277
- )}
278
- {customer.website && (
279
- <div>
280
- <p className="text-sm text-text-secondary">Website</p>
281
- <a href={customer.website} className="text-primary hover:underline">
282
- {customer.website}
283
- </a>
407
+ <div className="flex items-center gap-3">
408
+ <Phone size={18} className="text-text-secondary" />
409
+ <div>
410
+ <p className="text-sm text-text-secondary">Phone</p>
411
+ <p className="font-medium">{customer.phone || '—'}</p>
412
+ </div>
284
413
  </div>
285
- )}
414
+ </div>
415
+ <div className="space-y-4">
416
+ {customer.company && (
417
+ <div className="flex items-center gap-3">
418
+ <Building size={18} className="text-text-secondary" />
419
+ <div>
420
+ <p className="text-sm text-text-secondary">Company</p>
421
+ <p className="font-medium">{customer.company}</p>
422
+ </div>
423
+ </div>
424
+ )}
425
+ {(() => {
426
+ const website = safeHttpUrl(customer.website);
427
+ if (!website) return null;
428
+ return (
429
+ <div>
430
+ <p className="text-sm text-text-secondary">Website</p>
431
+ <a
432
+ href={website}
433
+ target="_blank"
434
+ rel="noopener noreferrer"
435
+ className="text-primary hover:underline"
436
+ >
437
+ {website}
438
+ </a>
439
+ </div>
440
+ );
441
+ })()}
442
+ </div>
286
443
  </div>
287
- </div>
444
+ )}
288
445
  </EmphasisPanel>
289
446
 
290
447
  <Card>
@@ -310,6 +467,55 @@ export default function CustomerDetailsPage() {
310
467
  </CardContent>
311
468
  </Card>
312
469
  )}
470
+
471
+ {/* Orders for this customer */}
472
+ <Card>
473
+ <CardHeader className="flex flex-row items-center justify-between">
474
+ <CardTitle className="flex items-center gap-2">
475
+ <Package size={18} /> Orders
476
+ </CardTitle>
477
+ <Button variant="outline" size="sm" onClick={() => goTo('orders/new')}>
478
+ <FileText size={14} /> New order
479
+ </Button>
480
+ </CardHeader>
481
+ <CardContent>
482
+ {ordersLoading ? (
483
+ <p className="text-text-secondary text-center py-4">Loading orders…</p>
484
+ ) : orders.length > 0 ? (
485
+ <div className="space-y-2">
486
+ {orders.map((order) => (
487
+ <button
488
+ key={order.id}
489
+ type="button"
490
+ onClick={() => goTo(`orders/${order.id}`)}
491
+ className="w-full flex items-center justify-between gap-3 p-3 border rounded-lg text-left hover:bg-bg-sunken/50 transition-colors"
492
+ >
493
+ <div className="min-w-0">
494
+ <p className="font-medium truncate">{order.orderNumber}</p>
495
+ <p className="text-sm text-text-secondary">
496
+ {formatDate(order.createdAt, 'relative')} ·{' '}
497
+ {formatCurrency(order.pricing.total)}
498
+ </p>
499
+ </div>
500
+ <StatusBadge status={order.status} type="order" size="sm" />
501
+ </button>
502
+ ))}
503
+ </div>
504
+ ) : (
505
+ <div className="text-center py-6">
506
+ <Package size={32} className="mx-auto mb-2 text-text-secondary opacity-50" />
507
+ <p className="text-text-secondary">No orders yet for this customer</p>
508
+ <button
509
+ type="button"
510
+ onClick={() => goTo('orders/new')}
511
+ className="mt-1 text-sm text-primary hover:underline"
512
+ >
513
+ Book their first delivery
514
+ </button>
515
+ </div>
516
+ )}
517
+ </CardContent>
518
+ </Card>
313
519
  </div>
314
520
 
315
521
  <div className="space-y-6">
@@ -2,13 +2,15 @@
2
2
  * Customers Page
3
3
  */
4
4
 
5
- import { Users, Plus, RefreshCw, Download, Star } from 'lucide-react';
5
+ import { useState } from 'react';
6
+ import { Users, Plus, RefreshCw, Download, Star, UserCheck, Building2 } from 'lucide-react';
6
7
  import { PageLayout } from '../components/PageLayout';
8
+ import { MetricCard, MetricGrid } from '../components/MetricCard';
7
9
  import { DataTable, type Column } from '../components/DataTable';
8
10
  import { TypeBadge } from '../components/StatusBadge';
9
11
  import { Badge, Avatar, AvatarFallback, Button } from '../components/ui';
10
- import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
11
- import { useCustomers } from '../hooks/useCustomers';
12
+ import { CTAOverflowMenu, PagePurpose } from '@burdenoff/fe-libs/ui';
13
+ import { useCustomers, useCustomerStats } from '../hooks/useCustomers';
12
14
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
13
15
  import { formatDate } from '../utils/formatters';
14
16
  import { joinPath } from '../utils/navigation';
@@ -16,7 +18,14 @@ import type { Customer } from '../types';
16
18
 
17
19
  export default function CustomersPage() {
18
20
  const { basePath, navigate } = useMoveTheWheels();
19
- const { customers, totalCustomers, isLoading, refetch } = useCustomers({ limit: 100 });
21
+ const [page, setPage] = useState(0);
22
+ const [pageSize, setPageSize] = useState(25);
23
+ const { customers, hasMore, isLoading, refetch } = useCustomers({
24
+ limit: pageSize,
25
+ offset: page * pageSize,
26
+ });
27
+ // Bounded aggregate for the tiles + subtitle — never the truncated page.
28
+ const { stats } = useCustomerStats();
20
29
 
21
30
  const goTo = (path: string) => navigate?.(joinPath(basePath, path));
22
31
 
@@ -85,7 +94,7 @@ export default function CustomersPage() {
85
94
  return (
86
95
  <PageLayout
87
96
  title="Customers"
88
- subtitle={`${totalCustomers} customers`}
97
+ subtitle={`${stats.total} customers`}
89
98
  icon={<Users size={24} />}
90
99
  headerActions={
91
100
  <CTAOverflowMenu
@@ -96,7 +105,11 @@ export default function CustomersPage() {
96
105
  }}
97
106
  actions={[
98
107
  { label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
99
- { label: 'Export', icon: <Download size={16} />, onSelect: () => goTo('import-export') },
108
+ {
109
+ label: 'Export',
110
+ icon: <Download size={16} />,
111
+ onSelect: () => goTo('import-export'),
112
+ },
100
113
  ]}
101
114
  />
102
115
  }
@@ -107,28 +120,32 @@ export default function CustomersPage() {
107
120
  next shipment.
108
121
  </PagePurpose>
109
122
 
110
- {/* Customer base at a glance — the page's single emphasis zone */}
111
- <EmphasisPanel>
112
- <p className="text-sm font-medium text-text-secondary">Customer base at a glance</p>
113
- <div className="mt-3 grid grid-cols-3 gap-4">
114
- <div>
115
- <p className="text-2xl font-bold text-text-primary tabular-nums">{totalCustomers}</p>
116
- <p className="text-xs text-text-secondary">Total customers</p>
117
- </div>
118
- <div>
119
- <p className="text-2xl font-bold text-text-primary tabular-nums">
120
- {customers.filter((c) => c.isActive).length}
121
- </p>
122
- <p className="text-xs text-text-secondary">Active</p>
123
- </div>
124
- <div>
125
- <p className="text-2xl font-bold text-text-primary tabular-nums">
126
- {customers.filter((c) => c.type === 'b2b').length}
127
- </p>
128
- <p className="text-xs text-text-secondary">Business accounts</p>
129
- </div>
130
- </div>
131
- </EmphasisPanel>
123
+ {/* Customer base at a glance — premium stat cards */}
124
+ <div>
125
+ <p className="mb-3 text-sm font-medium text-text-secondary">Customer base at a glance</p>
126
+ <MetricGrid columns={3}>
127
+ <MetricCard
128
+ label="Total customers"
129
+ value={stats.total}
130
+ icon={<Users size={20} />}
131
+ accent="brand"
132
+ />
133
+ <MetricCard
134
+ label="Active"
135
+ value={stats.active}
136
+ icon={<UserCheck size={20} />}
137
+ accent="success"
138
+ subtitle="Currently transacting"
139
+ />
140
+ <MetricCard
141
+ label="Business accounts"
142
+ value={stats.b2b}
143
+ icon={<Building2 size={20} />}
144
+ accent="blue"
145
+ subtitle="B2B relationships"
146
+ />
147
+ </MetricGrid>
148
+ </div>
132
149
 
133
150
  <DataTable
134
151
  columns={columns}
@@ -139,6 +156,14 @@ export default function CustomersPage() {
139
156
  searchFields={['name', 'email', 'company'] as (keyof Customer)[]}
140
157
  onRowClick={(row) => goTo(`customers/${row.id}`)}
141
158
  getRowId={(row) => row.id}
159
+ pageIndex={page}
160
+ pageSize={pageSize}
161
+ onPageChange={setPage}
162
+ onPageSizeChange={(size) => {
163
+ setPageSize(size);
164
+ setPage(0);
165
+ }}
166
+ hasNextPage={hasMore}
142
167
  emptyIllustration="empty-people"
143
168
  emptyTitle="No customers yet"
144
169
  emptyDescription="Add your first customer to start booking deliveries for them."