@burdenoff/microfe-movethewheels 2026.626.2 → 2026.703.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.
Files changed (120) hide show
  1. package/dist/{AIAssistantPage-AvsMbunM.js → AIAssistantPage-C3ovZ0Fh.js} +100 -73
  2. package/dist/AnalyticsPage-O8SpY-V4.js +215 -0
  3. package/dist/{CreateOrderPage-CjlnEtk3.js → CreateOrderPage-CB1_-_mN.js} +33 -35
  4. package/dist/{CustomerDetailsPage-I6cdDHUi.js → CustomerDetailsPage-BaeKEI1D.js} +42 -44
  5. package/dist/{CustomersPage-CGGfD4ki.js → CustomersPage-DRaLuHMC.js} +26 -28
  6. package/dist/{DashboardPage-D2HRqwg9.js → DashboardPage-DPjhNfKB.js} +85 -83
  7. package/dist/{DataTable-CD9ZhpLB.js → DataTable-DmciIKKM.js} +3 -3
  8. package/dist/{DriverDetailsPage-rg_pGNuP.js → DriverDetailsPage-CSSQTOzn.js} +52 -54
  9. package/dist/{DriversPage-DKiynISH.js → DriversPage-C2Bk0w4Q.js} +24 -26
  10. package/dist/{FinancePage-Vg8WPs38.js → FinancePage-lmp0Sdsw.js} +9 -9
  11. package/dist/FleetPage-DZn5pIqd.js +365 -0
  12. package/dist/ImportExportPage-BIUJZc7O.js +290 -0
  13. package/dist/InventoryPage-Cxh1zYJd.js +253 -0
  14. package/dist/{LiveTrackingPage-DQ1q4Id3.js → LiveTrackingPage-BY1yAkIt.js} +60 -56
  15. package/dist/MarketplacePage-RMuL17EA.js +247 -0
  16. package/dist/{MetricCard-B7yMyvMX.js → MetricCard-7aYgzZHK.js} +1 -1
  17. package/dist/{OrderDetailsPage-CFASgsYX.js → OrderDetailsPage-C2Mf4KSy.js} +83 -85
  18. package/dist/{OrdersListPage-BHeT8S5o.js → OrdersListPage-BrPBtnhp.js} +31 -33
  19. package/dist/{PageLayout-D3N8MCTm.js → PageLayout-D4LDHJJq.js} +483 -422
  20. package/dist/{ProductDetailsPage-CFGcbaHo.js → ProductDetailsPage-CezzzZcj.js} +27 -29
  21. package/dist/{ProductsPage--xEpbQ8A.js → ProductsPage-I5DilIoY.js} +21 -23
  22. package/dist/ReportsPage-BDXNmUng.js +284 -0
  23. package/dist/RouteDetailsPage-BcomL20M.js +263 -0
  24. package/dist/RoutesPage-lqX9ylHP.js +144 -0
  25. package/dist/SettingsPage-CQVIOaNs.js +421 -0
  26. package/dist/StatusBadge-NpTeHVlw.js +152 -0
  27. package/dist/{TrackingPage-CcRc_dva.js → TrackingPage-BXrjz7V8.js} +59 -61
  28. package/dist/{VehicleDetailsPage-3YDMPsPs.js → VehicleDetailsPage-DZjLcJgr.js} +40 -42
  29. package/dist/{VehiclesPage-UlxPWP9K.js → VehiclesPage-6W39XT5W.js} +19 -21
  30. package/dist/{WarehouseDetailsPage-Db7KeN-d.js → WarehouseDetailsPage-C10pgRGW.js} +38 -40
  31. package/dist/{WarehousesPage-CuTO-OPp.js → WarehousesPage-CHvBqNaQ.js} +15 -17
  32. package/dist/{arrow-left-CBpZKkpq.js → arrow-left-n48Y-Wis.js} +1 -1
  33. package/dist/{box-CAaiElsb.js → box-D9Pal4HM.js} +1 -1
  34. package/dist/{chart-column-D1Ym15-S.js → chart-column-BFIJysDR.js} +1 -1
  35. package/dist/{chevron-right-BIFGi0UA.js → chevron-right-EMIIyyJ1.js} +1 -1
  36. package/dist/{circle-alert-DjmNo5_L.js → circle-alert-iY24lSlp.js} +1 -1
  37. package/dist/{circle-check-big-CjuPVo3_.js → circle-check-big-w0TrLKMF.js} +1 -1
  38. package/dist/{clock-BjkdnddD.js → clock-DJZtdl8u.js} +1 -1
  39. package/dist/{dollar-sign-IB4O0wuQ.js → dollar-sign-CgKQRz_e.js} +1 -1
  40. package/dist/{download-Dpa9vqoG.js → download-DRXxoiQR.js} +1 -1
  41. package/dist/{file-text-C8Uioy11.js → file-text-DWWY6_f5.js} +1 -1
  42. package/dist/{filter-Da5OZaHI.js → filter-DloLC2eE.js} +1 -1
  43. package/dist/index.js +119 -111
  44. package/dist/{map-_Per4sT9.js → map-CkJaULb2.js} +1 -1
  45. package/dist/{map-pin-BSaf5IDL.js → map-pin-BGiCvyCs.js} +1 -1
  46. package/dist/{navigation-DaTbrhw2.js → navigation-D36prnAn.js} +1 -1
  47. package/dist/{package-BE7U0wQE.js → package-BChUzpJS.js} +1 -1
  48. package/dist/{phone-DV4StZiA.js → phone-Ac9s3mo_.js} +1 -1
  49. package/dist/{plus-Dl1rLeJR.js → plus-Z76q-FZm.js} +1 -1
  50. package/dist/{refresh-cw-CWfERAit.js → refresh-cw-DKt6f5T6.js} +1 -1
  51. package/dist/{save-XH8vydOn.js → save-CojywNhG.js} +1 -1
  52. package/dist/{search-OxoIiL_C.js → search-CDGefqb3.js} +1 -1
  53. package/dist/{square-pen-hLIbFQFV.js → square-pen-CopE9GmB.js} +1 -1
  54. package/dist/{star-YsVV3MNQ.js → star-nHlLjDKR.js} +1 -1
  55. package/dist/{store-RCBl7HB-.js → store-BR9Voi5E.js} +1 -1
  56. package/dist/{trending-up-C-85AwGT.js → trending-up-B7W-GL6P.js} +1 -1
  57. package/dist/{triangle-alert-BOhfGgmh.js → triangle-alert-BonHFFNz.js} +1 -1
  58. package/dist/{truck-CtDjNFbi.js → truck-DrlfSpRC.js} +1 -1
  59. package/dist/{useAnalytics-ph7eTIK6.js → useAnalytics-CgdzUfY_.js} +40 -40
  60. package/dist/useAssistant-D4v3oxD3.js +84 -0
  61. package/dist/{useCustomers-bS3a4ytk.js → useCustomers-QrlozybF.js} +24 -24
  62. package/dist/{useFleet-BdETplNE.js → useFleet-DiAC-IiH.js} +70 -67
  63. package/dist/useImportExport-BLMnAKCC.js +55 -0
  64. package/dist/{useInventory-Dwn18FPz.js → useInventory-DKoks37x.js} +33 -33
  65. package/dist/{useOrders-D_3_hGMp.js → useOrders-D3UZVy8Y.js} +42 -42
  66. package/dist/useReports-Puq4DCH9.js +78 -0
  67. package/dist/{useRoutes-v4aBaS-E.js → useRoutes-Do57LvRb.js} +31 -31
  68. package/dist/useSettings-CkDH_-gM.js +81 -0
  69. package/dist/{useTracking-De2KIUNu.js → useTracking-GJify4HN.js} +22 -22
  70. package/dist/{user-Dz5TJZFs.js → user-CjSoudeu.js} +1 -1
  71. package/dist/{users-g2N0eL-Z.js → users-BAM4gHTn.js} +1 -1
  72. package/dist/{warehouse-D3WWDeTX.js → warehouse-C7MSr47B.js} +1 -1
  73. package/dist/{wrench-CmxY1n4I.js → wrench-CANXWxF-.js} +1 -1
  74. package/package.json +1 -1
  75. package/src/movethewheels/MoveTheWheelsRoutes.tsx +27 -3
  76. package/src/movethewheels/components/StatusBadge.tsx +18 -37
  77. package/src/movethewheels/constants/index.ts +67 -26
  78. package/src/movethewheels/hooks/index.ts +12 -0
  79. package/src/movethewheels/hooks/useAnalytics.ts +20 -6
  80. package/src/movethewheels/hooks/useAssistant.ts +74 -0
  81. package/src/movethewheels/hooks/useFleet.ts +63 -59
  82. package/src/movethewheels/hooks/useImportExport.ts +101 -0
  83. package/src/movethewheels/hooks/useReports.ts +158 -0
  84. package/src/movethewheels/hooks/useSettings.ts +120 -0
  85. package/src/movethewheels/pages/AIAssistantPage.tsx +53 -15
  86. package/src/movethewheels/pages/AnalyticsPage.tsx +67 -64
  87. package/src/movethewheels/pages/CustomerDetailsPage.tsx +1 -1
  88. package/src/movethewheels/pages/CustomersPage.tsx +1 -1
  89. package/src/movethewheels/pages/DashboardPage.tsx +42 -22
  90. package/src/movethewheels/pages/DriverDetailsPage.tsx +1 -1
  91. package/src/movethewheels/pages/DriversPage.tsx +1 -1
  92. package/src/movethewheels/pages/FleetPage.tsx +96 -38
  93. package/src/movethewheels/pages/ImportExportPage.tsx +174 -68
  94. package/src/movethewheels/pages/InventoryPage.tsx +66 -28
  95. package/src/movethewheels/pages/LiveTrackingPage.tsx +12 -3
  96. package/src/movethewheels/pages/MarketplacePage.tsx +87 -45
  97. package/src/movethewheels/pages/OrderDetailsPage.tsx +2 -2
  98. package/src/movethewheels/pages/ReportsPage.tsx +194 -90
  99. package/src/movethewheels/pages/RouteDetailsPage.tsx +5 -11
  100. package/src/movethewheels/pages/RoutesPage.tsx +4 -9
  101. package/src/movethewheels/pages/SettingsPage.tsx +234 -56
  102. package/src/movethewheels/pages/TrackingPage.tsx +6 -6
  103. package/src/movethewheels/providers/MoveTheWheelsProvider.tsx +7 -6
  104. package/src/movethewheels/store/movethewheelsStore.ts +1 -1
  105. package/dist/AnalyticsPage-CCLfHgQZ.js +0 -212
  106. package/dist/FleetPage-DddXt8r8.js +0 -332
  107. package/dist/ImportExportPage-BW6qO3kc.js +0 -248
  108. package/dist/InventoryPage-rMGuaJN-.js +0 -222
  109. package/dist/MarketplacePage-DKrXAv62.js +0 -219
  110. package/dist/ReportsPage-Bb7_90rK.js +0 -235
  111. package/dist/RouteDetailsPage-CVtDVb1N.js +0 -282
  112. package/dist/RoutesPage-s57PtgcQ.js +0 -163
  113. package/dist/SettingsPage-BYNCBOpN.js +0 -298
  114. package/dist/StatusBadge-CijYQXtb.js +0 -209
  115. package/dist/graphqlClient-CdJyR_ed.js +0 -55
  116. package/dist/navigation-BgnOfsVd.js +0 -6
  117. package/dist/route-Dp9JiN_E.js +0 -51
  118. package/dist/settings-3Rw5NwFF.js +0 -28
  119. package/src/movethewheels/constants/mockData.ts +0 -1342
  120. /package/dist/{formatters-_vJlC-47.js → formatters-DhKZ-eA1.js} +0 -0
@@ -0,0 +1,158 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
3
+ import { executeGraphQL } from '../utils/graphqlClient';
4
+
5
+ export interface MovethewheelsReport {
6
+ id: string;
7
+ name: string;
8
+ description?: string | null;
9
+ type: string;
10
+ status: string;
11
+ schedule?: string | null;
12
+ lastGenerated?: string | null;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ }
16
+
17
+ export interface GeneratedReport {
18
+ generatedAt: string;
19
+ result?: Record<string, unknown> | null;
20
+ report: MovethewheelsReport;
21
+ }
22
+
23
+ const REPORT_FIELDS = `
24
+ id
25
+ name
26
+ description
27
+ type
28
+ status
29
+ schedule
30
+ lastGenerated
31
+ createdAt
32
+ updatedAt
33
+ `;
34
+
35
+ const LIST_REPORTS_QUERY = `
36
+ query MovethewheelsReports($filter: MovethewheelsReportFilterInput, $pagination: PaginationInput) {
37
+ movethewheelsReports(filter: $filter, pagination: $pagination) {
38
+ ${REPORT_FIELDS}
39
+ }
40
+ }
41
+ `;
42
+
43
+ const CREATE_REPORT_MUTATION = `
44
+ mutation CreateMovethewheelsReport($input: CreateMovethewheelsReportInput!) {
45
+ createMovethewheelsReport(input: $input) {
46
+ ${REPORT_FIELDS}
47
+ }
48
+ }
49
+ `;
50
+
51
+ const GENERATE_REPORT_MUTATION = `
52
+ mutation GenerateMovethewheelsReport($input: GenerateMovethewheelsReportInput!) {
53
+ generateMovethewheelsReport(input: $input) {
54
+ generatedAt
55
+ result
56
+ report {
57
+ ${REPORT_FIELDS}
58
+ }
59
+ }
60
+ }
61
+ `;
62
+
63
+ export interface CreateReportInput {
64
+ name: string;
65
+ type: string;
66
+ description?: string;
67
+ schedule?: string;
68
+ status?: string;
69
+ }
70
+
71
+ interface UseReportsResult {
72
+ reports: MovethewheelsReport[];
73
+ isLoading: boolean;
74
+ error: Error | null;
75
+ refetch: () => void;
76
+ createReport: (input: CreateReportInput) => Promise<MovethewheelsReport>;
77
+ generateReport: (
78
+ reportId: string,
79
+ parameters?: Record<string, unknown>
80
+ ) => Promise<GeneratedReport>;
81
+ }
82
+
83
+ export function useReports(): UseReportsResult {
84
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
85
+ useMoveTheWheels();
86
+ const [reports, setReports] = useState<MovethewheelsReport[]>([]);
87
+ const [isLoading, setIsLoading] = useState(false);
88
+ const [error, setError] = useState<Error | null>(null);
89
+
90
+ const graphQLOptions = useMemo(
91
+ () => ({
92
+ apiGatewayUrl,
93
+ authToken,
94
+ workspaceToken,
95
+ workspaceId,
96
+ tenantId,
97
+ actorId: currentUser?.id,
98
+ }),
99
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
100
+ );
101
+
102
+ const load = useCallback(async () => {
103
+ setIsLoading(true);
104
+ setError(null);
105
+ try {
106
+ const data = await executeGraphQL<{ movethewheelsReports: MovethewheelsReport[] }>(
107
+ graphQLOptions,
108
+ LIST_REPORTS_QUERY,
109
+ { pagination: { limit: 100, offset: 0 } }
110
+ );
111
+ setReports(data.movethewheelsReports);
112
+ } catch (fetchError) {
113
+ setError(fetchError instanceof Error ? fetchError : new Error('Failed to load reports'));
114
+ } finally {
115
+ setIsLoading(false);
116
+ }
117
+ }, [graphQLOptions]);
118
+
119
+ useEffect(() => {
120
+ void load();
121
+ }, [load]);
122
+
123
+ const refetch = useCallback(() => {
124
+ void load();
125
+ }, [load]);
126
+
127
+ const createReport = useCallback(
128
+ async (input: CreateReportInput): Promise<MovethewheelsReport> => {
129
+ const data = await executeGraphQL<{ createMovethewheelsReport: MovethewheelsReport }>(
130
+ graphQLOptions,
131
+ CREATE_REPORT_MUTATION,
132
+ { input }
133
+ );
134
+ const created = data.createMovethewheelsReport;
135
+ setReports((previous) => [created, ...previous]);
136
+ return created;
137
+ },
138
+ [graphQLOptions]
139
+ );
140
+
141
+ const generateReport = useCallback(
142
+ async (reportId: string, parameters?: Record<string, unknown>): Promise<GeneratedReport> => {
143
+ const data = await executeGraphQL<{ generateMovethewheelsReport: GeneratedReport }>(
144
+ graphQLOptions,
145
+ GENERATE_REPORT_MUTATION,
146
+ { input: { reportId, parameters } }
147
+ );
148
+ const generated = data.generateMovethewheelsReport;
149
+ setReports((previous) =>
150
+ previous.map((item) => (item.id === reportId ? generated.report : item))
151
+ );
152
+ return generated;
153
+ },
154
+ [graphQLOptions]
155
+ );
156
+
157
+ return { reports, isLoading, error, refetch, createReport, generateReport };
158
+ }
@@ -0,0 +1,120 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
3
+ import { executeGraphQL } from '../utils/graphqlClient';
4
+
5
+ export type ModulePreferences = Record<string, unknown>;
6
+
7
+ export interface ModuleSettings {
8
+ preferences: ModulePreferences;
9
+ updatedAt: string;
10
+ workspaceId: string;
11
+ }
12
+
13
+ const MODULE_SETTINGS_QUERY = `
14
+ query MovethewheelsModuleSettings {
15
+ moduleSettings {
16
+ preferences
17
+ updatedAt
18
+ workspaceId
19
+ }
20
+ }
21
+ `;
22
+
23
+ const UPDATE_MODULE_SETTINGS_MUTATION = `
24
+ mutation UpdateMovethewheelsModuleSettings($input: UpdateMovethewheelsModuleSettingsInput!) {
25
+ updateModuleSettings(input: $input) {
26
+ preferences
27
+ updatedAt
28
+ workspaceId
29
+ }
30
+ }
31
+ `;
32
+
33
+ interface UseModuleSettingsResult {
34
+ settings: ModuleSettings | null;
35
+ preferences: ModulePreferences;
36
+ isLoading: boolean;
37
+ isSaving: boolean;
38
+ error: Error | null;
39
+ refetch: () => void;
40
+ savePreferences: (preferences: ModulePreferences) => Promise<ModuleSettings>;
41
+ }
42
+
43
+ export function useModuleSettings(): UseModuleSettingsResult {
44
+ const { apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser } =
45
+ useMoveTheWheels();
46
+ const [settings, setSettings] = useState<ModuleSettings | null>(null);
47
+ const [isLoading, setIsLoading] = useState(false);
48
+ const [isSaving, setIsSaving] = useState(false);
49
+ const [error, setError] = useState<Error | null>(null);
50
+
51
+ const graphQLOptions = useMemo(
52
+ () => ({
53
+ apiGatewayUrl,
54
+ authToken,
55
+ workspaceToken,
56
+ workspaceId,
57
+ tenantId,
58
+ actorId: currentUser?.id,
59
+ }),
60
+ [apiGatewayUrl, authToken, workspaceToken, workspaceId, tenantId, currentUser?.id]
61
+ );
62
+
63
+ const load = useCallback(async () => {
64
+ setIsLoading(true);
65
+ setError(null);
66
+ try {
67
+ const data = await executeGraphQL<{ moduleSettings: ModuleSettings }>(
68
+ graphQLOptions,
69
+ MODULE_SETTINGS_QUERY
70
+ );
71
+ setSettings(data.moduleSettings);
72
+ } catch (fetchError) {
73
+ setError(fetchError instanceof Error ? fetchError : new Error('Failed to load settings'));
74
+ } finally {
75
+ setIsLoading(false);
76
+ }
77
+ }, [graphQLOptions]);
78
+
79
+ useEffect(() => {
80
+ void load();
81
+ }, [load]);
82
+
83
+ const refetch = useCallback(() => {
84
+ void load();
85
+ }, [load]);
86
+
87
+ const savePreferences = useCallback(
88
+ async (preferences: ModulePreferences): Promise<ModuleSettings> => {
89
+ setIsSaving(true);
90
+ setError(null);
91
+ try {
92
+ const data = await executeGraphQL<{ updateModuleSettings: ModuleSettings }>(
93
+ graphQLOptions,
94
+ UPDATE_MODULE_SETTINGS_MUTATION,
95
+ { input: { preferences } }
96
+ );
97
+ setSettings(data.updateModuleSettings);
98
+ return data.updateModuleSettings;
99
+ } catch (fetchError) {
100
+ const normalized =
101
+ fetchError instanceof Error ? fetchError : new Error('Failed to save settings');
102
+ setError(normalized);
103
+ throw normalized;
104
+ } finally {
105
+ setIsSaving(false);
106
+ }
107
+ },
108
+ [graphQLOptions]
109
+ );
110
+
111
+ return {
112
+ settings,
113
+ preferences: settings?.preferences ?? {},
114
+ isLoading,
115
+ isSaving,
116
+ error,
117
+ refetch,
118
+ savePreferences,
119
+ };
120
+ }
@@ -16,14 +16,22 @@ import {
16
16
  ScrollArea,
17
17
  } from '../components/ui';
18
18
  import { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';
19
+ import { useAssistant } from '../hooks/useAssistant';
20
+
21
+ interface ChatMessage {
22
+ role: 'assistant' | 'user';
23
+ content: string;
24
+ isError?: boolean;
25
+ }
19
26
 
20
27
  export default function AIAssistantPage() {
28
+ const { ask, isLoading } = useAssistant();
21
29
  const [message, setMessage] = useState('');
22
- const [messages, setMessages] = useState([
30
+ const [messages, setMessages] = useState<ChatMessage[]>([
23
31
  {
24
32
  role: 'assistant',
25
33
  content:
26
- "Hello! I'm your logistics AI assistant. I can help you with route optimization, demand forecasting, fleet management insights, and more. How can I help you today?",
34
+ "Hello! I'm your logistics assistant. Ask me about your orders, deliveries, fleet or inventory and I'll answer from your live operational data.",
27
35
  },
28
36
  ]);
29
37
 
@@ -42,17 +50,31 @@ export default function AIAssistantPage() {
42
50
  'Natural Language Queries - Ask questions in plain English',
43
51
  ];
44
52
 
45
- const handleSend = () => {
46
- if (!message.trim()) return;
47
- setMessages([
48
- ...messages,
49
- { role: 'user', content: message },
50
- {
51
- role: 'assistant',
52
- content: `I understand you're asking about "${message}". Let me analyze that for you...\n\nBased on your current data, here are my findings:\n\n1. Your fleet utilization is currently at 78%, which is within the optimal range.\n2. There are 5 pending orders that could benefit from route consolidation.\n3. Estimated fuel savings from optimization: $120/day.\n\nWould you like me to generate a detailed report or take any action?`,
53
- },
54
- ]);
53
+ const submit = async (question: string) => {
54
+ const trimmed = question.trim();
55
+ if (!trimmed || isLoading) return;
55
56
  setMessage('');
57
+ setMessages((previous) => [...previous, { role: 'user', content: trimmed }]);
58
+ try {
59
+ const result = await ask(trimmed);
60
+ setMessages((previous) => [...previous, { role: 'assistant', content: result.answer }]);
61
+ } catch (fetchError) {
62
+ setMessages((previous) => [
63
+ ...previous,
64
+ {
65
+ role: 'assistant',
66
+ content:
67
+ fetchError instanceof Error
68
+ ? `Sorry, I couldn't answer that: ${fetchError.message}`
69
+ : "Sorry, I couldn't reach the assistant. Please try again.",
70
+ isError: true,
71
+ },
72
+ ]);
73
+ }
74
+ };
75
+
76
+ const handleSend = () => {
77
+ void submit(message);
56
78
  };
57
79
 
58
80
  return (
@@ -84,12 +106,26 @@ export default function AIAssistantPage() {
84
106
  className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
85
107
  >
86
108
  <div
87
- className={`max-w-[80%] p-3 rounded-lg ${msg.role === 'user' ? 'bg-action-primary-bg text-action-primary-text' : 'bg-bg-sunken'}`}
109
+ className={`max-w-[80%] p-3 rounded-lg ${
110
+ msg.role === 'user'
111
+ ? 'bg-action-primary-bg text-action-primary-text'
112
+ : msg.isError
113
+ ? 'bg-status-error-bg-subtle text-status-error-text'
114
+ : 'bg-bg-sunken'
115
+ }`}
88
116
  >
89
117
  <p className="whitespace-pre-line text-sm">{msg.content}</p>
90
118
  </div>
91
119
  </div>
92
120
  ))}
121
+ {isLoading && (
122
+ <div className="flex justify-start" aria-live="polite">
123
+ <div className="max-w-[80%] p-3 rounded-lg bg-bg-sunken flex items-center gap-2">
124
+ <Sparkles size={14} className="text-primary motion-safe:animate-pulse" />
125
+ <span className="text-sm text-text-secondary">Analyzing your data…</span>
126
+ </div>
127
+ </div>
128
+ )}
93
129
  </div>
94
130
  </ScrollArea>
95
131
  <div className="p-4 border-t">
@@ -98,9 +134,10 @@ export default function AIAssistantPage() {
98
134
  value={message}
99
135
  onChange={(e) => setMessage(e.target.value)}
100
136
  placeholder="Ask me anything about your logistics operations..."
137
+ disabled={isLoading}
101
138
  onKeyPress={(e) => e.key === 'Enter' && handleSend()}
102
139
  />
103
- <Button onClick={handleSend}>
140
+ <Button onClick={handleSend} disabled={isLoading || !message.trim()}>
104
141
  <Send size={16} />
105
142
  </Button>
106
143
  </div>
@@ -119,7 +156,8 @@ export default function AIAssistantPage() {
119
156
  key={i}
120
157
  variant="outline"
121
158
  className="w-full justify-start text-left h-auto py-3"
122
- onClick={() => setMessage(s.text)}
159
+ disabled={isLoading}
160
+ onClick={() => void submit(s.text)}
123
161
  >
124
162
  <s.icon size={16} className="mr-2 flex-shrink-0" />
125
163
  <span className="flex-1">{s.text}</span>
@@ -4,14 +4,14 @@
4
4
 
5
5
  import { BarChart3, TrendingUp, Package, Truck, DollarSign, Users } from 'lucide-react';
6
6
  import { PageLayout } from '../components/PageLayout';
7
- import { MetricCard, MetricGrid } from '../components/MetricCard';
8
- import { Card, CardContent, CardHeader, CardTitle, Progress } from '../components/ui';
7
+ import { MetricCard, MetricGrid, MetricCardSkeleton } from '../components/MetricCard';
8
+ import { Card, CardContent, CardHeader, CardTitle, Progress, Button } from '../components/ui';
9
9
  import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
10
10
  import { useAnalytics, useOrderAnalytics, useFleetAnalytics } from '../hooks/useAnalytics';
11
11
  import { formatCurrency } from '../utils/formatters';
12
12
 
13
13
  export default function AnalyticsPage() {
14
- const { analytics } = useAnalytics();
14
+ const { analytics, isLoading, error, refetch } = useAnalytics();
15
15
  const { orderData } = useOrderAnalytics();
16
16
  const { fleetData } = useFleetAnalytics();
17
17
 
@@ -27,43 +27,55 @@ export default function AnalyticsPage() {
27
27
  to invest — whether that&apos;s more vehicles, better routes, or new customers.
28
28
  </PagePurpose>
29
29
 
30
- <MetricGrid columns={5}>
31
- <MetricCard
32
- label="Total Orders"
33
- value={analytics.overview.totalOrders.toLocaleString()}
34
- icon={<Package size={20} />}
35
- change="+8.2%"
36
- changeType="positive"
37
- trend="up"
38
- />
39
- <MetricCard
40
- treatment="glass"
41
- glow
42
- label="Revenue"
43
- value={formatCurrency(analytics.overview.totalRevenue)}
44
- icon={<DollarSign size={20} />}
45
- change="+12.5%"
46
- changeType="positive"
47
- trend="up"
48
- />
49
- <MetricCard
50
- label="Active Vehicles"
51
- value={analytics.overview.activeVehicles}
52
- icon={<Truck size={20} />}
53
- />
54
- <MetricCard
55
- label="On-Time Rate"
56
- value={`${analytics.overview.onTimeDeliveryRate}%`}
57
- icon={<TrendingUp size={20} />}
58
- changeType={analytics.overview.onTimeDeliveryRate >= 90 ? 'positive' : 'negative'}
59
- />
60
- <MetricCard
61
- label="Satisfaction"
62
- value={analytics.overview.customerSatisfaction.toFixed(1)}
63
- icon={<Users size={20} />}
64
- subtitle="out of 5"
65
- />
66
- </MetricGrid>
30
+ {error && !isLoading ? (
31
+ <Card className="border-status-error-border">
32
+ <CardContent className="p-6 text-center space-y-3">
33
+ <p className="text-status-error-text font-medium">Couldn&apos;t load analytics</p>
34
+ <p className="text-sm text-text-secondary">{error.message}</p>
35
+ <Button variant="outline" onClick={refetch}>
36
+ Retry
37
+ </Button>
38
+ </CardContent>
39
+ </Card>
40
+ ) : isLoading ? (
41
+ <MetricGrid columns={5}>
42
+ {Array.from({ length: 5 }).map((_, i) => (
43
+ <MetricCardSkeleton key={i} />
44
+ ))}
45
+ </MetricGrid>
46
+ ) : (
47
+ <MetricGrid columns={5}>
48
+ <MetricCard
49
+ label="Total Orders"
50
+ value={analytics.overview.totalOrders.toLocaleString()}
51
+ icon={<Package size={20} />}
52
+ />
53
+ <MetricCard
54
+ treatment="glass"
55
+ glow
56
+ label="Revenue"
57
+ value={formatCurrency(analytics.overview.totalRevenue)}
58
+ icon={<DollarSign size={20} />}
59
+ />
60
+ <MetricCard
61
+ label="Active Vehicles"
62
+ value={analytics.overview.activeVehicles}
63
+ icon={<Truck size={20} />}
64
+ />
65
+ <MetricCard
66
+ label="On-Time Rate"
67
+ value={`${analytics.overview.onTimeDeliveryRate.toFixed(1)}%`}
68
+ icon={<TrendingUp size={20} />}
69
+ changeType={analytics.overview.onTimeDeliveryRate >= 90 ? 'positive' : 'neutral'}
70
+ />
71
+ <MetricCard
72
+ label="Satisfaction"
73
+ value={analytics.overview.customerSatisfaction.toFixed(1)}
74
+ icon={<Users size={20} />}
75
+ subtitle="out of 5"
76
+ />
77
+ </MetricGrid>
78
+ )}
67
79
 
68
80
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
69
81
  <Card>
@@ -149,45 +161,36 @@ export default function AnalyticsPage() {
149
161
  </div>
150
162
  <Progress value={fleetData.vehicleUtilization} />
151
163
  </div>
152
- <div>
153
- <div className="flex justify-between text-sm mb-1">
154
- <span>Avg Fuel Level</span>
155
- <span>{fleetData.avgFuelLevel.toFixed(0)}%</span>
164
+ {fleetData.vehiclesByType.length > 0 && (
165
+ <div className="pt-2">
166
+ <p className="text-sm text-text-secondary mb-2">Vehicles by Type</p>
167
+ {fleetData.vehiclesByType.map(({ type, count }) => (
168
+ <div key={type} className="flex justify-between text-sm py-1">
169
+ <span className="capitalize">{type}</span>
170
+ <span className="font-medium">{count}</span>
171
+ </div>
172
+ ))}
156
173
  </div>
157
- <Progress value={fleetData.avgFuelLevel} />
158
- </div>
159
- <div className="pt-2">
160
- <p className="text-sm text-text-secondary mb-2">Vehicles by Type</p>
161
- {fleetData.vehiclesByType.map(({ type, count }) => (
162
- <div key={type} className="flex justify-between text-sm py-1">
163
- <span className="capitalize">{type}</span>
164
- <span className="font-medium">{count}</span>
165
- </div>
166
- ))}
167
- </div>
174
+ )}
168
175
  </CardContent>
169
176
  </Card>
170
177
 
171
178
  {/* Performance Metrics — the page's single emphasis zone */}
172
179
  <EmphasisPanel className="space-y-4">
173
180
  <h2 className="text-base font-semibold text-text-primary">Performance Metrics</h2>
174
- <div>
175
- <p className="text-sm text-text-secondary">On-Time Pickup</p>
176
- <p className="text-2xl font-bold">{analytics.performance.onTimePickup}%</p>
177
- </div>
178
181
  <div>
179
182
  <p className="text-sm text-text-secondary">On-Time Delivery</p>
180
- <p className="text-2xl font-bold">{analytics.performance.onTimeDelivery}%</p>
183
+ <p className="text-2xl font-bold">{analytics.performance.onTimeDelivery.toFixed(1)}%</p>
181
184
  </div>
182
185
  <div>
183
- <p className="text-sm text-text-secondary">Damaged Items</p>
184
- <p className="text-2xl font-bold text-status-success-text">
185
- {analytics.performance.damagedItems}%
186
+ <p className="text-sm text-text-secondary">Customer Satisfaction</p>
187
+ <p className="text-2xl font-bold">
188
+ {analytics.overview.customerSatisfaction.toFixed(1)}/5
186
189
  </p>
187
190
  </div>
188
191
  <div>
189
192
  <p className="text-sm text-text-secondary">Driver Ratings</p>
190
- <p className="text-2xl font-bold">{analytics.performance.driverRatings}/5</p>
193
+ <p className="text-2xl font-bold">{analytics.performance.driverRatings.toFixed(1)}/5</p>
191
194
  </div>
192
195
  </EmphasisPanel>
193
196
  </div>
@@ -78,7 +78,7 @@ export default function CustomerDetailsPage() {
78
78
  {customer.isActive ? 'Active' : 'Inactive'}
79
79
  </Badge>
80
80
  <div className="flex items-center gap-1">
81
- <Star size={16} className="text-yellow-500 fill-yellow-500" />
81
+ <Star size={16} className="text-status-warning-text fill-current" />
82
82
  <span className="font-medium">{customer.rating.toFixed(1)}</span>
83
83
  </div>
84
84
  </div>
@@ -59,7 +59,7 @@ export default function CustomersPage() {
59
59
  header: 'Rating',
60
60
  cell: (row) => (
61
61
  <div className="flex items-center gap-1">
62
- <Star size={14} className="text-yellow-500 fill-yellow-500" />
62
+ <Star size={14} className="text-status-warning-text fill-current" />
63
63
  <span>{row.rating.toFixed(1)}</span>
64
64
  </div>
65
65
  ),