@burdenoff/microfe-movethewheels 2026.626.2 → 2026.703.2
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-AvsMbunM.js → AIAssistantPage-C3cPsWt-.js} +100 -73
- package/dist/AnalyticsPage-Dz6g2MvK.js +215 -0
- package/dist/{CreateOrderPage-CjlnEtk3.js → CreateOrderPage-C5iwnjp4.js} +36 -67
- package/dist/CustomerDetailsPage-B6nwFyLO.js +406 -0
- package/dist/{CustomersPage-CGGfD4ki.js → CustomersPage-2Y05LVyh.js} +26 -28
- package/dist/{DashboardPage-D2HRqwg9.js → DashboardPage-BVjQi0EB.js} +85 -83
- package/dist/{DataTable-CD9ZhpLB.js → DataTable-DmciIKKM.js} +3 -3
- package/dist/DriverDetailsPage-CN3WQ0B9.js +507 -0
- package/dist/{DriversPage-DKiynISH.js → DriversPage-B0Zpx8OQ.js} +25 -27
- package/dist/{FinancePage-Vg8WPs38.js → FinancePage-DEDykSOP.js} +9 -9
- package/dist/FleetPage-DqkFi3s3.js +365 -0
- package/dist/FormError-BlOjnXOq.js +15 -0
- package/dist/ImportExportPage-DKJRCezl.js +290 -0
- package/dist/InventoryPage-DwfgCUQq.js +253 -0
- package/dist/{LiveTrackingPage-DQ1q4Id3.js → LiveTrackingPage-DCX-K-LR.js} +60 -56
- package/dist/MarketplacePage-CIQ97_zp.js +247 -0
- package/dist/{MetricCard-B7yMyvMX.js → MetricCard-BY0QrMD4.js} +1 -1
- package/dist/{OrderDetailsPage-CFASgsYX.js → OrderDetailsPage-DYiZ-9UK.js} +83 -85
- package/dist/{OrdersListPage-BHeT8S5o.js → OrdersListPage-BjDlCKJd.js} +31 -33
- package/dist/{PageLayout-D3N8MCTm.js → PageLayout-D4LDHJJq.js} +483 -422
- package/dist/ProductDetailsPage-C9uOiYLp.js +350 -0
- package/dist/{ProductsPage--xEpbQ8A.js → ProductsPage-C1G9h7Xd.js} +22 -24
- package/dist/ReportsPage-Zp53q3_8.js +284 -0
- package/dist/RouteDetailsPage-DUStXSf9.js +531 -0
- package/dist/RoutesPage-DMfoicM1.js +144 -0
- package/dist/SettingsPage-Bn-v0mbQ.js +421 -0
- package/dist/StatusBadge-BZurYHMy.js +152 -0
- package/dist/{TrackingPage-CcRc_dva.js → TrackingPage-W-jtdM3Y.js} +59 -61
- package/dist/VehicleDetailsPage-Bceo5oz3.js +410 -0
- package/dist/{VehiclesPage-UlxPWP9K.js → VehiclesPage-VBtOep91.js} +20 -22
- package/dist/WarehouseDetailsPage-L_6sWigM.js +493 -0
- package/dist/{WarehousesPage-CuTO-OPp.js → WarehousesPage-DCbXXXWE.js} +16 -18
- package/dist/{arrow-left-CBpZKkpq.js → arrow-left-n48Y-Wis.js} +1 -1
- package/dist/{box-CAaiElsb.js → box-D9Pal4HM.js} +1 -1
- package/dist/{chart-column-D1Ym15-S.js → chart-column-BFIJysDR.js} +1 -1
- package/dist/{chevron-right-BIFGi0UA.js → chevron-right-EMIIyyJ1.js} +1 -1
- package/dist/{circle-alert-DjmNo5_L.js → circle-alert-iY24lSlp.js} +1 -1
- package/dist/{circle-check-big-CjuPVo3_.js → circle-check-big-w0TrLKMF.js} +1 -1
- package/dist/{clock-BjkdnddD.js → clock-DJZtdl8u.js} +1 -1
- package/dist/{dollar-sign-IB4O0wuQ.js → dollar-sign-CgKQRz_e.js} +1 -1
- package/dist/{download-Dpa9vqoG.js → download-DRXxoiQR.js} +1 -1
- package/dist/{file-text-C8Uioy11.js → file-text-DWWY6_f5.js} +1 -1
- package/dist/{filter-Da5OZaHI.js → filter-DloLC2eE.js} +1 -1
- package/dist/index.js +186 -162
- package/dist/{map-_Per4sT9.js → map-CkJaULb2.js} +1 -1
- package/dist/{map-pin-BSaf5IDL.js → map-pin-BGiCvyCs.js} +1 -1
- package/dist/{navigation-DaTbrhw2.js → navigation-D36prnAn.js} +1 -1
- package/dist/{package-BE7U0wQE.js → package-BChUzpJS.js} +1 -1
- package/dist/{phone-DV4StZiA.js → phone-Ac9s3mo_.js} +1 -1
- package/dist/{plus-Dl1rLeJR.js → plus-Z76q-FZm.js} +1 -1
- package/dist/{refresh-cw-CWfERAit.js → refresh-cw-DKt6f5T6.js} +1 -1
- package/dist/{save-XH8vydOn.js → save-CojywNhG.js} +1 -1
- package/dist/{search-OxoIiL_C.js → search-CDGefqb3.js} +1 -1
- package/dist/{square-pen-hLIbFQFV.js → square-pen-CopE9GmB.js} +1 -1
- package/dist/{star-YsVV3MNQ.js → star-nHlLjDKR.js} +1 -1
- package/dist/{store-RCBl7HB-.js → store-BR9Voi5E.js} +1 -1
- package/dist/trash-2-BKhar6Hs.js +32 -0
- package/dist/{trending-up-C-85AwGT.js → trending-up-Dohgd4t_.js} +1 -1
- package/dist/{triangle-alert-BOhfGgmh.js → triangle-alert-i-XQL9Rd.js} +1 -1
- package/dist/{truck-CtDjNFbi.js → truck-C0v_hbLR.js} +1 -1
- package/dist/{useAnalytics-ph7eTIK6.js → useAnalytics-DS8n0xgg.js} +40 -40
- package/dist/useAssistant-D4v3oxD3.js +84 -0
- package/dist/{useCustomers-bS3a4ytk.js → useCustomers-BmTNGxiI.js} +24 -24
- package/dist/{useFleet-BdETplNE.js → useFleet-DKqXZ16m.js} +133 -109
- package/dist/useImportExport-DHr-SWSw.js +55 -0
- package/dist/{useInventory-Dwn18FPz.js → useInventory-DkyiVY_A.js} +95 -75
- package/dist/{useOrders-D_3_hGMp.js → useOrders-Cfk1GLiq.js} +42 -42
- package/dist/useReports-CTqZr-AB.js +78 -0
- package/dist/{useRoutes-v4aBaS-E.js → useRoutes-BQWO607N.js} +31 -31
- package/dist/useSettings-CkDH_-gM.js +81 -0
- package/dist/{useTracking-De2KIUNu.js → useTracking-B-va1kO3.js} +22 -22
- package/dist/{user-Dz5TJZFs.js → user-jyFU3lXk.js} +1 -1
- package/dist/{users-g2N0eL-Z.js → users-DZ-ilS6B.js} +1 -1
- package/dist/{warehouse-D3WWDeTX.js → warehouse-6m5kXMLu.js} +1 -1
- package/dist/{wrench-CmxY1n4I.js → wrench-CANXWxF-.js} +1 -1
- package/package.json +1 -1
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +31 -3
- package/src/movethewheels/components/FormError.tsx +25 -0
- package/src/movethewheels/components/StatusBadge.tsx +18 -37
- package/src/movethewheels/constants/index.ts +67 -26
- package/src/movethewheels/hooks/index.ts +12 -0
- package/src/movethewheels/hooks/useAnalytics.ts +20 -6
- package/src/movethewheels/hooks/useAssistant.ts +74 -0
- package/src/movethewheels/hooks/useFleet.ts +170 -59
- package/src/movethewheels/hooks/useImportExport.ts +101 -0
- package/src/movethewheels/hooks/useInventory.ts +107 -0
- package/src/movethewheels/hooks/useReports.ts +158 -0
- package/src/movethewheels/hooks/useSettings.ts +120 -0
- package/src/movethewheels/pages/AIAssistantPage.tsx +53 -15
- package/src/movethewheels/pages/AnalyticsPage.tsx +67 -64
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +154 -21
- package/src/movethewheels/pages/CustomersPage.tsx +1 -1
- package/src/movethewheels/pages/DashboardPage.tsx +42 -22
- package/src/movethewheels/pages/DriverDetailsPage.tsx +160 -3
- package/src/movethewheels/pages/DriversPage.tsx +6 -2
- package/src/movethewheels/pages/FleetPage.tsx +96 -38
- package/src/movethewheels/pages/ImportExportPage.tsx +174 -68
- package/src/movethewheels/pages/InventoryPage.tsx +66 -28
- package/src/movethewheels/pages/LiveTrackingPage.tsx +12 -3
- package/src/movethewheels/pages/MarketplacePage.tsx +87 -45
- package/src/movethewheels/pages/OrderDetailsPage.tsx +2 -2
- package/src/movethewheels/pages/ProductDetailsPage.tsx +146 -3
- package/src/movethewheels/pages/ProductsPage.tsx +5 -1
- package/src/movethewheels/pages/ReportsPage.tsx +194 -90
- package/src/movethewheels/pages/RouteDetailsPage.tsx +290 -36
- package/src/movethewheels/pages/RoutesPage.tsx +4 -9
- package/src/movethewheels/pages/SettingsPage.tsx +234 -56
- package/src/movethewheels/pages/TrackingPage.tsx +6 -6
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +158 -2
- package/src/movethewheels/pages/VehiclesPage.tsx +5 -1
- package/src/movethewheels/pages/WarehouseDetailsPage.tsx +224 -2
- package/src/movethewheels/pages/WarehousesPage.tsx +5 -1
- package/src/movethewheels/providers/MoveTheWheelsProvider.tsx +7 -6
- package/src/movethewheels/store/movethewheelsStore.ts +1 -1
- package/dist/AnalyticsPage-CCLfHgQZ.js +0 -212
- package/dist/CustomerDetailsPage-I6cdDHUi.js +0 -265
- package/dist/DriverDetailsPage-rg_pGNuP.js +0 -330
- package/dist/FleetPage-DddXt8r8.js +0 -332
- package/dist/ImportExportPage-BW6qO3kc.js +0 -248
- package/dist/InventoryPage-rMGuaJN-.js +0 -222
- package/dist/MarketplacePage-DKrXAv62.js +0 -219
- package/dist/ProductDetailsPage-CFGcbaHo.js +0 -195
- package/dist/ReportsPage-Bb7_90rK.js +0 -235
- package/dist/RouteDetailsPage-CVtDVb1N.js +0 -282
- package/dist/RoutesPage-s57PtgcQ.js +0 -163
- package/dist/SettingsPage-BYNCBOpN.js +0 -298
- package/dist/StatusBadge-CijYQXtb.js +0 -209
- package/dist/VehicleDetailsPage-3YDMPsPs.js +0 -220
- package/dist/WarehouseDetailsPage-Db7KeN-d.js +0 -238
- package/dist/graphqlClient-CdJyR_ed.js +0 -55
- package/dist/navigation-BgnOfsVd.js +0 -6
- package/dist/route-Dp9JiN_E.js +0 -51
- package/dist/settings-3Rw5NwFF.js +0 -28
- package/src/movethewheels/constants/mockData.ts +0 -1342
- /package/dist/{formatters-_vJlC-47.js → formatters-CF2S2l1m.js} +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings Page
|
|
3
|
+
*
|
|
4
|
+
* Loads persisted module settings (Query.moduleSettings.preferences) and saves
|
|
5
|
+
* changes via updateModuleSettings. Integrations read "Not configured" until a
|
|
6
|
+
* real connection is recorded — no fabricated "Connected" statuses.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import { useState } from 'react';
|
|
9
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
6
10
|
import { Settings, User, Bell, Shield, Globe, Webhook, Save } from 'lucide-react';
|
|
7
11
|
import { PageLayout } from '../components/PageLayout';
|
|
8
12
|
import {
|
|
@@ -26,9 +30,116 @@ import {
|
|
|
26
30
|
TabsContent,
|
|
27
31
|
} from '../components/ui';
|
|
28
32
|
import { EmphasisPanel, PagePurpose, NextSteps } from '@burdenoff/fe-libs/ui';
|
|
33
|
+
import { useModuleSettings, type ModulePreferences } from '../hooks/useSettings';
|
|
34
|
+
|
|
35
|
+
interface CompanyPrefs {
|
|
36
|
+
name: string;
|
|
37
|
+
email: string;
|
|
38
|
+
phone: string;
|
|
39
|
+
website: string;
|
|
40
|
+
}
|
|
41
|
+
interface LocalizationPrefs {
|
|
42
|
+
timezone: string;
|
|
43
|
+
currency: string;
|
|
44
|
+
language: string;
|
|
45
|
+
}
|
|
46
|
+
interface NotificationPrefs {
|
|
47
|
+
orderUpdates: boolean;
|
|
48
|
+
deliveryAlerts: boolean;
|
|
49
|
+
systemNotifications: boolean;
|
|
50
|
+
weeklyReports: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface SettingsForm {
|
|
53
|
+
company: CompanyPrefs;
|
|
54
|
+
localization: LocalizationPrefs;
|
|
55
|
+
notifications: NotificationPrefs;
|
|
56
|
+
twoFactorEnabled: boolean;
|
|
57
|
+
integrations: Record<string, boolean>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const INTEGRATIONS = [
|
|
61
|
+
{ key: 'googleMaps', name: 'Google Maps' },
|
|
62
|
+
{ key: 'stripe', name: 'Stripe' },
|
|
63
|
+
{ key: 'twilio', name: 'Twilio' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const NOTIFICATION_ITEMS: { key: keyof NotificationPrefs; label: string; desc: string }[] = [
|
|
67
|
+
{
|
|
68
|
+
key: 'orderUpdates',
|
|
69
|
+
label: 'Order updates',
|
|
70
|
+
desc: 'Receive notifications for order status changes',
|
|
71
|
+
},
|
|
72
|
+
{ key: 'deliveryAlerts', label: 'Delivery alerts', desc: 'Get notified about delivery issues' },
|
|
73
|
+
{
|
|
74
|
+
key: 'systemNotifications',
|
|
75
|
+
label: 'System notifications',
|
|
76
|
+
desc: 'Important system updates and maintenance',
|
|
77
|
+
},
|
|
78
|
+
{ key: 'weeklyReports', label: 'Weekly reports', desc: 'Receive weekly performance summary' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
function toForm(preferences: ModulePreferences): SettingsForm {
|
|
82
|
+
const company = (preferences.company as Partial<CompanyPrefs> | undefined) ?? {};
|
|
83
|
+
const localization = (preferences.localization as Partial<LocalizationPrefs> | undefined) ?? {};
|
|
84
|
+
const notifications = (preferences.notifications as Partial<NotificationPrefs> | undefined) ?? {};
|
|
85
|
+
const security = (preferences.security as { twoFactorEnabled?: boolean } | undefined) ?? {};
|
|
86
|
+
const integrations = (preferences.integrations as Record<string, boolean> | undefined) ?? {};
|
|
87
|
+
return {
|
|
88
|
+
company: {
|
|
89
|
+
name: company.name ?? '',
|
|
90
|
+
email: company.email ?? '',
|
|
91
|
+
phone: company.phone ?? '',
|
|
92
|
+
website: company.website ?? '',
|
|
93
|
+
},
|
|
94
|
+
localization: {
|
|
95
|
+
timezone: localization.timezone ?? 'utc',
|
|
96
|
+
currency: localization.currency ?? 'usd',
|
|
97
|
+
language: localization.language ?? 'en',
|
|
98
|
+
},
|
|
99
|
+
notifications: {
|
|
100
|
+
orderUpdates: notifications.orderUpdates ?? true,
|
|
101
|
+
deliveryAlerts: notifications.deliveryAlerts ?? true,
|
|
102
|
+
systemNotifications: notifications.systemNotifications ?? true,
|
|
103
|
+
weeklyReports: notifications.weeklyReports ?? false,
|
|
104
|
+
},
|
|
105
|
+
twoFactorEnabled: security.twoFactorEnabled ?? false,
|
|
106
|
+
integrations,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
29
109
|
|
|
30
110
|
export default function SettingsPage() {
|
|
111
|
+
const { preferences, isLoading, isSaving, error, savePreferences } = useModuleSettings();
|
|
31
112
|
const [tab, setTab] = useState('general');
|
|
113
|
+
const [form, setForm] = useState<SettingsForm>(() => toForm({}));
|
|
114
|
+
const [saved, setSaved] = useState(false);
|
|
115
|
+
|
|
116
|
+
const loadedForm = useMemo(() => toForm(preferences), [preferences]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
setForm(loadedForm);
|
|
119
|
+
}, [loadedForm]);
|
|
120
|
+
|
|
121
|
+
const patchCompany = (patch: Partial<CompanyPrefs>) =>
|
|
122
|
+
setForm((prev) => ({ ...prev, company: { ...prev.company, ...patch } }));
|
|
123
|
+
const patchLocalization = (patch: Partial<LocalizationPrefs>) =>
|
|
124
|
+
setForm((prev) => ({ ...prev, localization: { ...prev.localization, ...patch } }));
|
|
125
|
+
const toggleNotification = (key: keyof NotificationPrefs, value: boolean) =>
|
|
126
|
+
setForm((prev) => ({ ...prev, notifications: { ...prev.notifications, [key]: value } }));
|
|
127
|
+
|
|
128
|
+
const handleSave = async () => {
|
|
129
|
+
setSaved(false);
|
|
130
|
+
try {
|
|
131
|
+
await savePreferences({
|
|
132
|
+
company: form.company,
|
|
133
|
+
localization: form.localization,
|
|
134
|
+
notifications: form.notifications,
|
|
135
|
+
security: { twoFactorEnabled: form.twoFactorEnabled },
|
|
136
|
+
integrations: form.integrations,
|
|
137
|
+
});
|
|
138
|
+
setSaved(true);
|
|
139
|
+
} catch {
|
|
140
|
+
// error surfaced via the hook's `error`
|
|
141
|
+
}
|
|
142
|
+
};
|
|
32
143
|
|
|
33
144
|
return (
|
|
34
145
|
<PageLayout
|
|
@@ -43,6 +154,19 @@ export default function SettingsPage() {
|
|
|
43
154
|
expect.
|
|
44
155
|
</PagePurpose>
|
|
45
156
|
|
|
157
|
+
{(saved || error) && (
|
|
158
|
+
<div
|
|
159
|
+
role="status"
|
|
160
|
+
className={`rounded-lg p-3 text-sm ${
|
|
161
|
+
error
|
|
162
|
+
? 'bg-status-error-bg-subtle text-status-error-text'
|
|
163
|
+
: 'bg-status-success-bg-subtle text-status-success-text'
|
|
164
|
+
}`}
|
|
165
|
+
>
|
|
166
|
+
{error ? error.message : 'Settings saved.'}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
46
170
|
<NextSteps
|
|
47
171
|
storageKey="movethewheels-settings"
|
|
48
172
|
steps={[
|
|
@@ -101,19 +225,40 @@ export default function SettingsPage() {
|
|
|
101
225
|
<div className="grid grid-cols-2 gap-4">
|
|
102
226
|
<div>
|
|
103
227
|
<Label>Company Name</Label>
|
|
104
|
-
<Input
|
|
228
|
+
<Input
|
|
229
|
+
value={form.company.name}
|
|
230
|
+
placeholder="Your company name"
|
|
231
|
+
disabled={isLoading}
|
|
232
|
+
onChange={(e) => patchCompany({ name: e.target.value })}
|
|
233
|
+
/>
|
|
105
234
|
</div>
|
|
106
235
|
<div>
|
|
107
236
|
<Label>Contact Email</Label>
|
|
108
|
-
<Input
|
|
237
|
+
<Input
|
|
238
|
+
type="email"
|
|
239
|
+
value={form.company.email}
|
|
240
|
+
placeholder="contact@example.com"
|
|
241
|
+
disabled={isLoading}
|
|
242
|
+
onChange={(e) => patchCompany({ email: e.target.value })}
|
|
243
|
+
/>
|
|
109
244
|
</div>
|
|
110
245
|
<div>
|
|
111
246
|
<Label>Phone</Label>
|
|
112
|
-
<Input
|
|
247
|
+
<Input
|
|
248
|
+
value={form.company.phone}
|
|
249
|
+
placeholder="+1-555-0100"
|
|
250
|
+
disabled={isLoading}
|
|
251
|
+
onChange={(e) => patchCompany({ phone: e.target.value })}
|
|
252
|
+
/>
|
|
113
253
|
</div>
|
|
114
254
|
<div>
|
|
115
255
|
<Label>Website</Label>
|
|
116
|
-
<Input
|
|
256
|
+
<Input
|
|
257
|
+
value={form.company.website}
|
|
258
|
+
placeholder="https://example.com"
|
|
259
|
+
disabled={isLoading}
|
|
260
|
+
onChange={(e) => patchCompany({ website: e.target.value })}
|
|
261
|
+
/>
|
|
117
262
|
</div>
|
|
118
263
|
</div>
|
|
119
264
|
</EmphasisPanel>
|
|
@@ -128,7 +273,10 @@ export default function SettingsPage() {
|
|
|
128
273
|
<div className="grid grid-cols-3 gap-4">
|
|
129
274
|
<div>
|
|
130
275
|
<Label>Timezone</Label>
|
|
131
|
-
<Select
|
|
276
|
+
<Select
|
|
277
|
+
value={form.localization.timezone}
|
|
278
|
+
onValueChange={(value) => patchLocalization({ timezone: value })}
|
|
279
|
+
>
|
|
132
280
|
<SelectTrigger>
|
|
133
281
|
<SelectValue />
|
|
134
282
|
</SelectTrigger>
|
|
@@ -141,7 +289,10 @@ export default function SettingsPage() {
|
|
|
141
289
|
</div>
|
|
142
290
|
<div>
|
|
143
291
|
<Label>Currency</Label>
|
|
144
|
-
<Select
|
|
292
|
+
<Select
|
|
293
|
+
value={form.localization.currency}
|
|
294
|
+
onValueChange={(value) => patchLocalization({ currency: value })}
|
|
295
|
+
>
|
|
145
296
|
<SelectTrigger>
|
|
146
297
|
<SelectValue />
|
|
147
298
|
</SelectTrigger>
|
|
@@ -154,7 +305,10 @@ export default function SettingsPage() {
|
|
|
154
305
|
</div>
|
|
155
306
|
<div>
|
|
156
307
|
<Label>Language</Label>
|
|
157
|
-
<Select
|
|
308
|
+
<Select
|
|
309
|
+
value={form.localization.language}
|
|
310
|
+
onValueChange={(value) => patchLocalization({ language: value })}
|
|
311
|
+
>
|
|
158
312
|
<SelectTrigger>
|
|
159
313
|
<SelectValue />
|
|
160
314
|
</SelectTrigger>
|
|
@@ -176,18 +330,17 @@ export default function SettingsPage() {
|
|
|
176
330
|
<CardTitle>Email Notifications</CardTitle>
|
|
177
331
|
</CardHeader>
|
|
178
332
|
<CardContent className="space-y-4">
|
|
179
|
-
{
|
|
180
|
-
{
|
|
181
|
-
{ label: 'Delivery alerts', desc: 'Get notified about delivery issues' },
|
|
182
|
-
{ label: 'System notifications', desc: 'Important system updates and maintenance' },
|
|
183
|
-
{ label: 'Weekly reports', desc: 'Receive weekly performance summary' },
|
|
184
|
-
].map((item) => (
|
|
185
|
-
<div key={item.label} className="flex items-center justify-between">
|
|
333
|
+
{NOTIFICATION_ITEMS.map((item) => (
|
|
334
|
+
<div key={item.key} className="flex items-center justify-between">
|
|
186
335
|
<div>
|
|
187
336
|
<p className="font-medium">{item.label}</p>
|
|
188
337
|
<p className="text-sm text-text-secondary">{item.desc}</p>
|
|
189
338
|
</div>
|
|
190
|
-
<Switch
|
|
339
|
+
<Switch
|
|
340
|
+
checked={form.notifications[item.key]}
|
|
341
|
+
disabled={isLoading}
|
|
342
|
+
onCheckedChange={(value) => toggleNotification(item.key, value)}
|
|
343
|
+
/>
|
|
191
344
|
</div>
|
|
192
345
|
))}
|
|
193
346
|
</CardContent>
|
|
@@ -198,23 +351,26 @@ export default function SettingsPage() {
|
|
|
198
351
|
<Card>
|
|
199
352
|
<CardHeader>
|
|
200
353
|
<CardTitle>Password</CardTitle>
|
|
354
|
+
<CardDescription>
|
|
355
|
+
Passwords are managed by your workspace identity provider.
|
|
356
|
+
</CardDescription>
|
|
201
357
|
</CardHeader>
|
|
202
358
|
<CardContent className="space-y-4">
|
|
203
359
|
<div>
|
|
204
360
|
<Label>Current Password</Label>
|
|
205
|
-
<Input type="password" />
|
|
361
|
+
<Input type="password" autoComplete="current-password" />
|
|
206
362
|
</div>
|
|
207
363
|
<div className="grid grid-cols-2 gap-4">
|
|
208
364
|
<div>
|
|
209
365
|
<Label>New Password</Label>
|
|
210
|
-
<Input type="password" />
|
|
366
|
+
<Input type="password" autoComplete="new-password" />
|
|
211
367
|
</div>
|
|
212
368
|
<div>
|
|
213
369
|
<Label>Confirm Password</Label>
|
|
214
|
-
<Input type="password" />
|
|
370
|
+
<Input type="password" autoComplete="new-password" />
|
|
215
371
|
</div>
|
|
216
372
|
</div>
|
|
217
|
-
<Button>Update Password</Button>
|
|
373
|
+
<Button variant="outline">Update Password</Button>
|
|
218
374
|
</CardContent>
|
|
219
375
|
</Card>
|
|
220
376
|
<Card>
|
|
@@ -224,59 +380,81 @@ export default function SettingsPage() {
|
|
|
224
380
|
</CardHeader>
|
|
225
381
|
<CardContent>
|
|
226
382
|
<div className="flex items-center justify-between">
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
383
|
+
<p>
|
|
384
|
+
Status:{' '}
|
|
385
|
+
<span
|
|
386
|
+
className={`font-medium ${
|
|
387
|
+
form.twoFactorEnabled
|
|
388
|
+
? 'text-status-success-text'
|
|
389
|
+
: 'text-status-warning-text'
|
|
390
|
+
}`}
|
|
391
|
+
>
|
|
392
|
+
{form.twoFactorEnabled ? 'Enabled' : 'Disabled'}
|
|
393
|
+
</span>
|
|
394
|
+
</p>
|
|
395
|
+
<Button
|
|
396
|
+
variant="outline"
|
|
397
|
+
disabled={isSaving}
|
|
398
|
+
onClick={() =>
|
|
399
|
+
setForm((prev) => ({ ...prev, twoFactorEnabled: !prev.twoFactorEnabled }))
|
|
400
|
+
}
|
|
401
|
+
>
|
|
402
|
+
{form.twoFactorEnabled ? 'Disable 2FA' : 'Enable 2FA'}
|
|
403
|
+
</Button>
|
|
233
404
|
</div>
|
|
234
405
|
</CardContent>
|
|
235
406
|
</Card>
|
|
236
407
|
</TabsContent>
|
|
237
408
|
|
|
238
409
|
<TabsContent value="integrations" className="space-y-6">
|
|
239
|
-
<Card>
|
|
240
|
-
<CardHeader>
|
|
241
|
-
<CardTitle>API Keys</CardTitle>
|
|
242
|
-
<CardDescription>Manage your API access</CardDescription>
|
|
243
|
-
</CardHeader>
|
|
244
|
-
<CardContent className="space-y-4">
|
|
245
|
-
<div className="p-4 bg-bg-sunken rounded-lg">
|
|
246
|
-
<p className="font-medium">Production API Key</p>
|
|
247
|
-
<code className="text-sm">pk_live_••••••••••••••••</code>
|
|
248
|
-
</div>
|
|
249
|
-
<Button variant="outline">Generate New Key</Button>
|
|
250
|
-
</CardContent>
|
|
251
|
-
</Card>
|
|
252
410
|
<Card>
|
|
253
411
|
<CardHeader>
|
|
254
412
|
<CardTitle>Connected Services</CardTitle>
|
|
413
|
+
<CardDescription>
|
|
414
|
+
Connect the providers that power maps, payments and messaging.
|
|
415
|
+
</CardDescription>
|
|
255
416
|
</CardHeader>
|
|
256
417
|
<CardContent className="space-y-3">
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
418
|
+
{INTEGRATIONS.map((service) => {
|
|
419
|
+
const connected = form.integrations[service.key] === true;
|
|
420
|
+
return (
|
|
421
|
+
<div
|
|
422
|
+
key={service.key}
|
|
423
|
+
className="flex items-center justify-between p-3 border rounded-lg"
|
|
424
|
+
>
|
|
425
|
+
<div>
|
|
426
|
+
<span className="font-medium">{service.name}</span>
|
|
427
|
+
<p className="text-sm text-text-secondary">
|
|
428
|
+
{connected ? 'Connected' : 'Not configured'}
|
|
429
|
+
</p>
|
|
430
|
+
</div>
|
|
431
|
+
<Button
|
|
432
|
+
variant={connected ? 'outline' : 'default'}
|
|
433
|
+
size="sm"
|
|
434
|
+
disabled={isSaving}
|
|
435
|
+
onClick={() =>
|
|
436
|
+
setForm((prev) => ({
|
|
437
|
+
...prev,
|
|
438
|
+
integrations: {
|
|
439
|
+
...prev.integrations,
|
|
440
|
+
[service.key]: !connected,
|
|
441
|
+
},
|
|
442
|
+
}))
|
|
443
|
+
}
|
|
444
|
+
>
|
|
445
|
+
{connected ? 'Disconnect' : 'Connect'}
|
|
446
|
+
</Button>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
})}
|
|
272
450
|
</CardContent>
|
|
273
451
|
</Card>
|
|
274
452
|
</TabsContent>
|
|
275
453
|
</Tabs>
|
|
276
454
|
|
|
277
455
|
<div className="flex justify-end">
|
|
278
|
-
<Button>
|
|
279
|
-
<Save size={16} /> Save Changes
|
|
456
|
+
<Button onClick={() => void handleSave()} disabled={isSaving || isLoading}>
|
|
457
|
+
<Save size={16} /> {isSaving ? 'Saving…' : 'Save Changes'}
|
|
280
458
|
</Button>
|
|
281
459
|
</div>
|
|
282
460
|
</PageLayout>
|
|
@@ -308,20 +308,20 @@ export default function TrackingPage() {
|
|
|
308
308
|
key={alert.id}
|
|
309
309
|
className={`flex items-start gap-3 p-3 rounded-lg ${
|
|
310
310
|
alert.severity === 'critical'
|
|
311
|
-
? 'bg-
|
|
311
|
+
? 'bg-status-error-bg-subtle'
|
|
312
312
|
: alert.severity === 'high'
|
|
313
|
-
? 'bg-
|
|
314
|
-
: 'bg-
|
|
313
|
+
? 'bg-status-warning-bg-subtle'
|
|
314
|
+
: 'bg-status-warning-bg-subtle'
|
|
315
315
|
}`}
|
|
316
316
|
>
|
|
317
317
|
<AlertCircle
|
|
318
318
|
size={16}
|
|
319
319
|
className={
|
|
320
320
|
alert.severity === 'critical'
|
|
321
|
-
? 'text-
|
|
321
|
+
? 'text-status-error-text'
|
|
322
322
|
: alert.severity === 'high'
|
|
323
|
-
? 'text-
|
|
324
|
-
: 'text-
|
|
323
|
+
? 'text-status-warning-text'
|
|
324
|
+
: 'text-status-warning-text'
|
|
325
325
|
}
|
|
326
326
|
/>
|
|
327
327
|
<div>
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Vehicle Details Page
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { useParams } from 'react-router-dom';
|
|
6
|
-
import { Truck, ArrowLeft, MapPin, Fuel, Wrench, FileText, Edit } from 'lucide-react';
|
|
7
|
+
import { Truck, ArrowLeft, MapPin, Fuel, Wrench, FileText, Edit, Save } from 'lucide-react';
|
|
7
8
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
8
9
|
import { StatusBadge, VehicleTypeBadge } from '../components/StatusBadge';
|
|
9
10
|
import {
|
|
@@ -14,12 +15,166 @@ import {
|
|
|
14
15
|
Button,
|
|
15
16
|
Badge,
|
|
16
17
|
Progress,
|
|
18
|
+
Input,
|
|
19
|
+
Label,
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
17
25
|
} from '../components/ui';
|
|
18
26
|
import { EmphasisPanel, NextSteps, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
19
|
-
import { useVehicle } from '../hooks/useFleet';
|
|
27
|
+
import { useVehicle, useFleet } from '../hooks/useFleet';
|
|
20
28
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
21
29
|
import { formatDate } from '../utils/formatters';
|
|
22
30
|
import { joinPath } from '../utils/navigation';
|
|
31
|
+
import { FormError } from '../components/FormError';
|
|
32
|
+
|
|
33
|
+
const VEHICLE_TYPE_OPTIONS = [
|
|
34
|
+
{ value: 'TRUCK', label: 'Truck' },
|
|
35
|
+
{ value: 'VAN', label: 'Van' },
|
|
36
|
+
{ value: 'MOTORCYCLE', label: 'Motorcycle' },
|
|
37
|
+
{ value: 'BICYCLE', label: 'Bicycle' },
|
|
38
|
+
{ value: 'FLATBED', label: 'Flatbed' },
|
|
39
|
+
{ value: 'REFRIGERATED', label: 'Refrigerated' },
|
|
40
|
+
{ value: 'SEMI_TRAILER', label: 'Semi-Trailer' },
|
|
41
|
+
{ value: 'DRONE', label: 'Drone' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function NewVehicleForm() {
|
|
45
|
+
const { basePath, navigate } = useMoveTheWheels();
|
|
46
|
+
const { createVehicle } = useFleet();
|
|
47
|
+
const [form, setForm] = useState({
|
|
48
|
+
vehicleNumber: '',
|
|
49
|
+
type: 'VAN',
|
|
50
|
+
make: '',
|
|
51
|
+
model: '',
|
|
52
|
+
licensePlate: '',
|
|
53
|
+
});
|
|
54
|
+
const [submitting, setSubmitting] = useState(false);
|
|
55
|
+
const [error, setError] = useState<string | null>(null);
|
|
56
|
+
|
|
57
|
+
const goBack = () => navigate?.(joinPath(basePath, 'fleet/vehicles'));
|
|
58
|
+
|
|
59
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
if (!form.vehicleNumber.trim()) {
|
|
62
|
+
setError('Vehicle number is required.');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setSubmitting(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
try {
|
|
68
|
+
const created = await createVehicle({
|
|
69
|
+
vehicleNumber: form.vehicleNumber.trim(),
|
|
70
|
+
type: form.type,
|
|
71
|
+
make: form.make.trim() || undefined,
|
|
72
|
+
model: form.model.trim() || undefined,
|
|
73
|
+
licensePlate: form.licensePlate.trim() || undefined,
|
|
74
|
+
});
|
|
75
|
+
navigate?.(joinPath(basePath, `fleet/vehicles/${created.id}`));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Failed to create vehicle:', err);
|
|
78
|
+
setError(err instanceof Error ? err.message : 'Failed to create vehicle. Please try again.');
|
|
79
|
+
setSubmitting(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<PageLayout
|
|
85
|
+
title="Add Vehicle"
|
|
86
|
+
subtitle="Register a new vehicle in your fleet"
|
|
87
|
+
icon={<Truck size={24} />}
|
|
88
|
+
headerActions={
|
|
89
|
+
<div className="flex gap-2">
|
|
90
|
+
<Button variant="outline" onClick={goBack} disabled={submitting}>
|
|
91
|
+
<ArrowLeft size={16} /> Cancel
|
|
92
|
+
</Button>
|
|
93
|
+
<Button onClick={handleSubmit} disabled={submitting}>
|
|
94
|
+
<Save size={16} /> {submitting ? 'Saving...' : 'Add Vehicle'}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
}
|
|
98
|
+
>
|
|
99
|
+
<PagePurpose icon={<Truck size={18} />}>
|
|
100
|
+
Add a vehicle so it can be assigned to deliveries and routes. Give it a fleet number and type
|
|
101
|
+
to start — make, model and plate help identify it on the road.
|
|
102
|
+
</PagePurpose>
|
|
103
|
+
|
|
104
|
+
<form onSubmit={handleSubmit} className="max-w-2xl space-y-6">
|
|
105
|
+
<FormError message={error} />
|
|
106
|
+
<EmphasisPanel>
|
|
107
|
+
<h2 className="text-base font-semibold text-text-primary mb-4">Vehicle Details</h2>
|
|
108
|
+
<div className="space-y-4">
|
|
109
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
110
|
+
<div className="space-y-2">
|
|
111
|
+
<Label htmlFor="vehicleNumber">
|
|
112
|
+
Vehicle Number <span className="text-status-error-text">*</span>
|
|
113
|
+
</Label>
|
|
114
|
+
<Input
|
|
115
|
+
id="vehicleNumber"
|
|
116
|
+
value={form.vehicleNumber}
|
|
117
|
+
onChange={(e) => setForm((prev) => ({ ...prev, vehicleNumber: e.target.value }))}
|
|
118
|
+
placeholder="MTW-001"
|
|
119
|
+
required
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="space-y-2">
|
|
123
|
+
<Label htmlFor="type">
|
|
124
|
+
Type <span className="text-status-error-text">*</span>
|
|
125
|
+
</Label>
|
|
126
|
+
<Select
|
|
127
|
+
value={form.type}
|
|
128
|
+
onValueChange={(value) => setForm((prev) => ({ ...prev, type: value }))}
|
|
129
|
+
>
|
|
130
|
+
<SelectTrigger id="type">
|
|
131
|
+
<SelectValue />
|
|
132
|
+
</SelectTrigger>
|
|
133
|
+
<SelectContent>
|
|
134
|
+
{VEHICLE_TYPE_OPTIONS.map((option) => (
|
|
135
|
+
<SelectItem key={option.value} value={option.value}>
|
|
136
|
+
{option.label}
|
|
137
|
+
</SelectItem>
|
|
138
|
+
))}
|
|
139
|
+
</SelectContent>
|
|
140
|
+
</Select>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
<Label htmlFor="make">Make</Label>
|
|
146
|
+
<Input
|
|
147
|
+
id="make"
|
|
148
|
+
value={form.make}
|
|
149
|
+
onChange={(e) => setForm((prev) => ({ ...prev, make: e.target.value }))}
|
|
150
|
+
placeholder="Ford"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="space-y-2">
|
|
154
|
+
<Label htmlFor="model">Model</Label>
|
|
155
|
+
<Input
|
|
156
|
+
id="model"
|
|
157
|
+
value={form.model}
|
|
158
|
+
onChange={(e) => setForm((prev) => ({ ...prev, model: e.target.value }))}
|
|
159
|
+
placeholder="Transit"
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="space-y-2">
|
|
163
|
+
<Label htmlFor="licensePlate">License Plate</Label>
|
|
164
|
+
<Input
|
|
165
|
+
id="licensePlate"
|
|
166
|
+
value={form.licensePlate}
|
|
167
|
+
onChange={(e) => setForm((prev) => ({ ...prev, licensePlate: e.target.value }))}
|
|
168
|
+
placeholder="ABC-1234"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</EmphasisPanel>
|
|
174
|
+
</form>
|
|
175
|
+
</PageLayout>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
23
178
|
|
|
24
179
|
export default function VehicleDetailsPage() {
|
|
25
180
|
const { vehicleId } = useParams<{ vehicleId: string }>();
|
|
@@ -29,6 +184,7 @@ export default function VehicleDetailsPage() {
|
|
|
29
184
|
const goBack = () => navigate?.(joinPath(basePath, 'fleet/vehicles'));
|
|
30
185
|
const goTo = (path: string) => navigate?.(joinPath(basePath, path));
|
|
31
186
|
|
|
187
|
+
if (!vehicleId) return <NewVehicleForm />;
|
|
32
188
|
if (isLoading) return <LoadingState message="Loading vehicle details..." />;
|
|
33
189
|
if (error || !vehicle) return <ErrorState title="Vehicle not found" onRetry={refetch} />;
|
|
34
190
|
|
|
@@ -95,7 +95,11 @@ export default function VehiclesPage() {
|
|
|
95
95
|
icon={<Truck size={24} />}
|
|
96
96
|
headerActions={
|
|
97
97
|
<CTAOverflowMenu
|
|
98
|
-
primary={{
|
|
98
|
+
primary={{
|
|
99
|
+
label: 'Add Vehicle',
|
|
100
|
+
icon: <Plus size={16} />,
|
|
101
|
+
onSelect: () => goTo('fleet/vehicles/new'),
|
|
102
|
+
}}
|
|
99
103
|
actions={[
|
|
100
104
|
{ label: 'Refresh', icon: <RefreshCw size={16} />, onSelect: () => refetch() },
|
|
101
105
|
{ label: 'Export', icon: <Download size={16} />, onSelect: () => {} },
|