@burdenoff/microfe-movethewheels 2026.1031.3 → 2026.1031.4
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/MoveTheWheelsRoot.js +136 -127
- package/dist/MoveTheWheelsRoutes.js +140 -135
- package/dist/components/DocumentsCard.js +11 -11
- package/dist/components/ProofOfDeliveryCard.js +13 -13
- package/dist/components/ShipmentRiskDialog.js +24 -24
- package/dist/components/ShippingQuoteResults.js +46 -46
- package/dist/components/TrackingAlertList.js +17 -17
- package/dist/generated/tracking-quality-operations.d.ts +56 -0
- package/dist/hooks/useOrderDispatch.js +17 -17
- package/dist/movethewheels/pages/SettingsPage.d.ts +1 -0
- package/dist/movethewheels/providers/ModuleSettingsProvider.d.ts +17 -0
- package/dist/movethewheels/utils/formatters.d.ts +23 -3
- package/dist/movethewheels/utils/moduleSettings.d.ts +52 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/bell.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/key-round.js +14 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/plug.js +22 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/rotate-ccw.js +11 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/settings.js +13 -0
- package/dist/pages/AnalyticsPage.js +27 -27
- package/dist/pages/CreateOrderPage.js +340 -317
- package/dist/pages/CustomerDetailsPage.js +246 -246
- package/dist/pages/CustomersPage.js +19 -19
- package/dist/pages/DashboardPage.js +24 -24
- package/dist/pages/DeliveryCapturePage.js +74 -74
- package/dist/pages/DriverDetailsPage.js +337 -337
- package/dist/pages/DriversPage.js +26 -26
- package/dist/pages/FinancePage.js +26 -26
- package/dist/pages/InventoryPage.js +28 -28
- package/dist/pages/LiveTrackingPage.js +108 -108
- package/dist/pages/MarketplacePage.js +44 -44
- package/dist/pages/OrderDetailsPage.js +306 -306
- package/dist/pages/OrdersListPage.js +27 -27
- package/dist/pages/ProductDetailsPage.js +89 -89
- package/dist/pages/ProductsPage.js +73 -73
- package/dist/pages/RouteDetailsPage.js +282 -285
- package/dist/pages/RoutesPage.js +78 -78
- package/dist/pages/SettingsPage.js +459 -0
- package/dist/pages/TrackingPage.js +41 -41
- package/dist/pages/VehicleDetailsPage.js +207 -207
- package/dist/pages/VehiclesPage.js +17 -16
- package/dist/pages/WarehouseDetailsPage.js +132 -132
- package/dist/providers/ModuleSettingsProvider.js +74 -0
- package/dist/utils/formatters.js +53 -26
- package/dist/utils/moduleSettings.js +132 -0
- package/package.json +3 -2
- package/src/generated/tracking-quality-operations.ts +62 -0
- package/src/movethewheels/MoveTheWheelsRoot.tsx +14 -3
- package/src/movethewheels/MoveTheWheelsRoutes.tsx +9 -0
- package/src/movethewheels/pages/CreateOrderPage.tsx +38 -6
- package/src/movethewheels/pages/ProductsPage.tsx +2 -2
- package/src/movethewheels/pages/RouteDetailsPage.tsx +3 -6
- package/src/movethewheels/pages/RoutesPage.tsx +2 -2
- package/src/movethewheels/pages/SettingsPage.tsx +516 -0
- package/src/movethewheels/pages/VehicleDetailsPage.tsx +4 -2
- package/src/movethewheels/pages/VehiclesPage.tsx +2 -1
- package/src/movethewheels/providers/ModuleSettingsProvider.tsx +133 -0
- package/src/movethewheels/utils/formatters.ts +69 -10
- package/src/movethewheels/utils/moduleSettings.ts +224 -0
|
@@ -57,7 +57,7 @@ import { useRefetchOnLeaveEdit } from '../hooks/useRefetchOnLeaveEdit';
|
|
|
57
57
|
import { AssignmentSuggestions } from '../components/AssignmentSuggestions';
|
|
58
58
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
59
59
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
60
|
-
import { formatDate, formatAddress, formatCurrency } from '../utils/formatters';
|
|
60
|
+
import { formatDate, formatAddress, formatCurrency, formatDistance } from '../utils/formatters';
|
|
61
61
|
import { joinPath } from '../utils/navigation';
|
|
62
62
|
import { ROUTE_STATUS_COLORS } from '../constants';
|
|
63
63
|
import { FormError } from '../components/FormError';
|
|
@@ -526,7 +526,7 @@ export default function RouteDetailsPage() {
|
|
|
526
526
|
return (
|
|
527
527
|
<PageLayout
|
|
528
528
|
title={route.name}
|
|
529
|
-
subtitle={`${route.stops.length} stops - ${route.distance
|
|
529
|
+
subtitle={`${route.stops.length} stops - ${formatDistance(route.distance)}`}
|
|
530
530
|
icon={<Navigation size={24} />}
|
|
531
531
|
headerActions={
|
|
532
532
|
<div className="flex gap-2">
|
|
@@ -720,10 +720,7 @@ export default function RouteDetailsPage() {
|
|
|
720
720
|
<h2 className="text-base font-semibold text-text-primary">Route Info</h2>
|
|
721
721
|
<div>
|
|
722
722
|
<p className="text-sm text-text-secondary">Total Distance</p>
|
|
723
|
-
<p className="text-2xl font-bold">
|
|
724
|
-
{route.distance.toFixed(1)}
|
|
725
|
-
<span className="text-sm font-normal"> km</span>
|
|
726
|
-
</p>
|
|
723
|
+
<p className="text-2xl font-bold">{formatDistance(route.distance)}</p>
|
|
727
724
|
</div>
|
|
728
725
|
<div>
|
|
729
726
|
<p className="text-sm text-text-secondary">Est. Duration</p>
|
|
@@ -22,7 +22,7 @@ import { CTAOverflowMenu, EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/
|
|
|
22
22
|
import { useRoutes, useRouteStats } from '../hooks/useRoutes';
|
|
23
23
|
import { useDebouncedValue } from '../hooks/useDebouncedValue';
|
|
24
24
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
25
|
-
import { formatDate } from '../utils/formatters';
|
|
25
|
+
import { formatDate, formatDistance } from '../utils/formatters';
|
|
26
26
|
import { joinPath } from '../utils/navigation';
|
|
27
27
|
import { ROUTE_STATUS_COLORS } from '../constants';
|
|
28
28
|
import type { Route } from '../types';
|
|
@@ -81,7 +81,7 @@ export default function RoutesPage() {
|
|
|
81
81
|
header: 'Distance',
|
|
82
82
|
accessorKey: 'distance',
|
|
83
83
|
sortable: true,
|
|
84
|
-
cell: (row) =>
|
|
84
|
+
cell: (row) => formatDistance(row.distance),
|
|
85
85
|
},
|
|
86
86
|
{ id: 'duration', header: 'Est. Duration', cell: (row) => `${row.estimatedDuration} min` },
|
|
87
87
|
{
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Page (BOFF-10488)
|
|
3
|
+
*
|
|
4
|
+
* The workspace's MoveTheWheels operational defaults: the currency amounts are
|
|
5
|
+
* shown in, distance and weight units, the time zone dates are shown in, and
|
|
6
|
+
* the priority and delivery window new orders start from. Saving applies them
|
|
7
|
+
* to every MoveTheWheels screen straight away.
|
|
8
|
+
*
|
|
9
|
+
* Company profile, notifications, two-factor authentication and integrations
|
|
10
|
+
* are platform settings; this page links to them instead of duplicating them.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useMemo, useState, type ChangeEvent, type FormEvent } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
Bell,
|
|
15
|
+
Building2,
|
|
16
|
+
ExternalLink,
|
|
17
|
+
KeyRound,
|
|
18
|
+
Loader2,
|
|
19
|
+
Plug,
|
|
20
|
+
RotateCcw,
|
|
21
|
+
Save,
|
|
22
|
+
Settings,
|
|
23
|
+
} from 'lucide-react';
|
|
24
|
+
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
25
|
+
import { PermissionProvider, usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
26
|
+
import { PageLayout } from '../components/PageLayout';
|
|
27
|
+
import { FormError } from '../components/FormError';
|
|
28
|
+
import { Notice, useNotice } from '../components/Notice';
|
|
29
|
+
import {
|
|
30
|
+
Badge,
|
|
31
|
+
Button,
|
|
32
|
+
Card,
|
|
33
|
+
CardContent,
|
|
34
|
+
CardDescription,
|
|
35
|
+
CardHeader,
|
|
36
|
+
CardTitle,
|
|
37
|
+
Input,
|
|
38
|
+
Label,
|
|
39
|
+
Select,
|
|
40
|
+
SelectContent,
|
|
41
|
+
SelectItem,
|
|
42
|
+
SelectTrigger,
|
|
43
|
+
SelectValue,
|
|
44
|
+
Skeleton,
|
|
45
|
+
} from '../components/ui';
|
|
46
|
+
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
47
|
+
import { useModuleSettings } from '../providers/ModuleSettingsProvider';
|
|
48
|
+
import { formatCurrency, formatDateTime, formatDistance, formatWeight } from '../utils/formatters';
|
|
49
|
+
import {
|
|
50
|
+
DELIVERY_WINDOW_MAX_HOURS,
|
|
51
|
+
DELIVERY_WINDOW_MIN_HOURS,
|
|
52
|
+
DISTANCE_UNIT_OPTIONS,
|
|
53
|
+
ORDER_PRIORITY_OPTIONS,
|
|
54
|
+
WEIGHT_UNIT_OPTIONS,
|
|
55
|
+
browserTimeZone,
|
|
56
|
+
changedSettings,
|
|
57
|
+
currencyLabel,
|
|
58
|
+
currencyOptions,
|
|
59
|
+
deliveryWindowError,
|
|
60
|
+
timeZoneOptions,
|
|
61
|
+
type MtwDistanceUnit,
|
|
62
|
+
type MtwModuleSettings,
|
|
63
|
+
type MtwOrderPriority,
|
|
64
|
+
type MtwWeightUnit,
|
|
65
|
+
} from '../utils/moduleSettings';
|
|
66
|
+
|
|
67
|
+
/** Select value standing for "no workspace time zone". */
|
|
68
|
+
const LOCAL_TIME = '__viewer_local__';
|
|
69
|
+
|
|
70
|
+
interface Draft {
|
|
71
|
+
defaultCurrency: string;
|
|
72
|
+
distanceUnit: MtwDistanceUnit;
|
|
73
|
+
weightUnit: MtwWeightUnit;
|
|
74
|
+
timezone: string;
|
|
75
|
+
defaultOrderPriority: MtwOrderPriority;
|
|
76
|
+
defaultDeliveryWindowHours: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toDraft(settings: MtwModuleSettings): Draft {
|
|
80
|
+
return {
|
|
81
|
+
defaultCurrency: settings.defaultCurrency,
|
|
82
|
+
distanceUnit: settings.distanceUnit,
|
|
83
|
+
weightUnit: settings.weightUnit,
|
|
84
|
+
timezone: settings.timezone ?? LOCAL_TIME,
|
|
85
|
+
defaultOrderPriority: settings.defaultOrderPriority,
|
|
86
|
+
defaultDeliveryWindowHours: String(settings.defaultDeliveryWindowHours),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const PLATFORM_LINKS = [
|
|
91
|
+
{
|
|
92
|
+
id: 'company',
|
|
93
|
+
title: 'Company profile',
|
|
94
|
+
description: 'Organization name, address and workspace details',
|
|
95
|
+
path: '/organizations',
|
|
96
|
+
icon: Building2,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'notifications',
|
|
100
|
+
title: 'Notification preferences',
|
|
101
|
+
description: 'Which alerts you receive and where',
|
|
102
|
+
path: '/notifications/preferences',
|
|
103
|
+
icon: Bell,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'mfa',
|
|
107
|
+
title: 'Two-factor authentication',
|
|
108
|
+
description: 'Protect your sign-in with a second factor',
|
|
109
|
+
path: '/auth/mfa',
|
|
110
|
+
icon: KeyRound,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'integrations',
|
|
114
|
+
title: 'Integrations',
|
|
115
|
+
description: 'Carrier, ERP and telematics connections',
|
|
116
|
+
path: '/integrations/overview',
|
|
117
|
+
icon: Plug,
|
|
118
|
+
},
|
|
119
|
+
] as const;
|
|
120
|
+
|
|
121
|
+
export default function SettingsPage() {
|
|
122
|
+
const { workspaceId } = useMoveTheWheels();
|
|
123
|
+
return (
|
|
124
|
+
<PermissionProvider scopeId={workspaceId ?? null} gateway="workspace">
|
|
125
|
+
<SettingsWorkbench />
|
|
126
|
+
</PermissionProvider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SettingsWorkbench() {
|
|
131
|
+
const { navigate, workspaceId, currentUser } = useMoveTheWheels();
|
|
132
|
+
const { settings, isLoaded, error: loadError, save, reload } = useModuleSettings();
|
|
133
|
+
const permissions = usePermissions();
|
|
134
|
+
const [draft, setDraft] = useState<Draft>(() => toDraft(settings));
|
|
135
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
136
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
137
|
+
const { message: notice, notify: showNotice } = useNotice();
|
|
138
|
+
|
|
139
|
+
// Re-seed the form whenever the saved settings change (first load, save).
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setDraft(toDraft(settings));
|
|
142
|
+
}, [settings]);
|
|
143
|
+
|
|
144
|
+
const permissionsReady = Boolean(
|
|
145
|
+
workspaceId &&
|
|
146
|
+
currentUser?.id &&
|
|
147
|
+
permissions.scopeId === workspaceId &&
|
|
148
|
+
!permissions.isLoading &&
|
|
149
|
+
!permissions.error
|
|
150
|
+
);
|
|
151
|
+
// Only lock the form when the effective permissions positively say no; the
|
|
152
|
+
// service enforces mtw-settings:update either way.
|
|
153
|
+
const readOnly =
|
|
154
|
+
permissionsReady &&
|
|
155
|
+
!permissions.isSuperAdmin &&
|
|
156
|
+
!permissions.hasPermission('mtw-settings:update');
|
|
157
|
+
|
|
158
|
+
const windowError = deliveryWindowError(draft.defaultDeliveryWindowHours);
|
|
159
|
+
const changes = useMemo(
|
|
160
|
+
() =>
|
|
161
|
+
changedSettings(settings, {
|
|
162
|
+
defaultCurrency: draft.defaultCurrency,
|
|
163
|
+
distanceUnit: draft.distanceUnit,
|
|
164
|
+
weightUnit: draft.weightUnit,
|
|
165
|
+
timezone: draft.timezone === LOCAL_TIME ? null : draft.timezone,
|
|
166
|
+
defaultOrderPriority: draft.defaultOrderPriority,
|
|
167
|
+
defaultDeliveryWindowHours: windowError
|
|
168
|
+
? undefined
|
|
169
|
+
: Number(draft.defaultDeliveryWindowHours),
|
|
170
|
+
}),
|
|
171
|
+
[settings, draft, windowError]
|
|
172
|
+
);
|
|
173
|
+
const isDirty = Object.keys(changes).length > 0;
|
|
174
|
+
const canSave = isLoaded && !readOnly && !isSaving && isDirty && !windowError;
|
|
175
|
+
|
|
176
|
+
const currencies = useMemo(() => currencyOptions(draft.defaultCurrency), [draft.defaultCurrency]);
|
|
177
|
+
const zones = useMemo(() => timeZoneOptions(settings.timezone), [settings.timezone]);
|
|
178
|
+
const localZone = useMemo(() => browserTimeZone(), []);
|
|
179
|
+
|
|
180
|
+
const update = <K extends keyof Draft>(key: K, value: Draft[K]) => {
|
|
181
|
+
setFormError(null);
|
|
182
|
+
setDraft((previous) => ({ ...previous, [key]: value }));
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
if (!canSave) return;
|
|
188
|
+
setIsSaving(true);
|
|
189
|
+
setFormError(null);
|
|
190
|
+
try {
|
|
191
|
+
await save(changes);
|
|
192
|
+
showNotice('Settings saved. MoveTheWheels now uses these defaults everywhere.');
|
|
193
|
+
} catch (caught) {
|
|
194
|
+
setFormError(caught instanceof Error ? caught.message : 'Could not save the settings');
|
|
195
|
+
} finally {
|
|
196
|
+
setIsSaving(false);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Live preview of what the saved settings will look like.
|
|
201
|
+
const previewSample = new Date('2026-03-14T15:30:00Z');
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<PageLayout
|
|
205
|
+
title="Settings"
|
|
206
|
+
subtitle="Workspace defaults for currency, units, time zone and new orders"
|
|
207
|
+
icon={<Settings size={24} />}
|
|
208
|
+
maxWidth="xl"
|
|
209
|
+
>
|
|
210
|
+
<PagePurpose icon={<Settings size={18} />}>
|
|
211
|
+
Set how MoveTheWheels shows money, distances, weights and times for everyone in this
|
|
212
|
+
workspace, and what new orders start with. Changes apply to every MoveTheWheels screen as
|
|
213
|
+
soon as you save.
|
|
214
|
+
</PagePurpose>
|
|
215
|
+
|
|
216
|
+
{loadError && (
|
|
217
|
+
<FormError
|
|
218
|
+
message={`Could not load this workspace's settings (${loadError.message}). Showing the platform defaults.`}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
<Notice message={notice} />
|
|
222
|
+
|
|
223
|
+
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
|
224
|
+
<EmphasisPanel className="space-y-6 xl:col-span-2">
|
|
225
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
226
|
+
<h2 className="text-base font-semibold text-text-primary">Operational defaults</h2>
|
|
227
|
+
{isLoaded && (
|
|
228
|
+
<Badge variant={settings.isCustomized ? 'default' : 'outline'}>
|
|
229
|
+
{settings.isCustomized ? 'Customized for this workspace' : 'Platform defaults'}
|
|
230
|
+
</Badge>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{!isLoaded ? (
|
|
235
|
+
<div className="space-y-4" aria-busy="true">
|
|
236
|
+
<Skeleton className="h-10 w-full" />
|
|
237
|
+
<Skeleton className="h-10 w-full" />
|
|
238
|
+
<Skeleton className="h-10 w-full" />
|
|
239
|
+
</div>
|
|
240
|
+
) : (
|
|
241
|
+
<form
|
|
242
|
+
className="space-y-6"
|
|
243
|
+
onSubmit={(event) => void onSubmit(event)}
|
|
244
|
+
aria-label="MoveTheWheels settings"
|
|
245
|
+
>
|
|
246
|
+
{readOnly && (
|
|
247
|
+
<p className="rounded-lg border border-border-default bg-surface-secondary px-4 py-3 text-sm text-text-secondary">
|
|
248
|
+
You can view these settings. Ask a workspace admin or the Logistics Director to
|
|
249
|
+
change them.
|
|
250
|
+
</p>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
<fieldset className="space-y-4" disabled={readOnly || isSaving}>
|
|
254
|
+
<legend className="text-sm font-semibold text-text-primary">Display</legend>
|
|
255
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
256
|
+
<div className="min-w-0 space-y-2">
|
|
257
|
+
<Label htmlFor="mtw-settings-currency">Default currency</Label>
|
|
258
|
+
<Select
|
|
259
|
+
value={draft.defaultCurrency}
|
|
260
|
+
onValueChange={(value: string) => update('defaultCurrency', value)}
|
|
261
|
+
disabled={readOnly || isSaving}
|
|
262
|
+
>
|
|
263
|
+
<SelectTrigger id="mtw-settings-currency" aria-label="Default currency">
|
|
264
|
+
<SelectValue />
|
|
265
|
+
</SelectTrigger>
|
|
266
|
+
<SelectContent className="max-h-72">
|
|
267
|
+
{currencies.map((code) => (
|
|
268
|
+
<SelectItem key={code} value={code}>
|
|
269
|
+
{currencyLabel(code)}
|
|
270
|
+
</SelectItem>
|
|
271
|
+
))}
|
|
272
|
+
</SelectContent>
|
|
273
|
+
</Select>
|
|
274
|
+
<p className="text-xs text-text-secondary">
|
|
275
|
+
Used for order totals, revenue and costs that have no currency of their own.
|
|
276
|
+
</p>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="min-w-0 space-y-2">
|
|
280
|
+
<Label htmlFor="mtw-settings-timezone">Time zone</Label>
|
|
281
|
+
<Select
|
|
282
|
+
value={draft.timezone}
|
|
283
|
+
onValueChange={(value: string) => update('timezone', value)}
|
|
284
|
+
disabled={readOnly || isSaving}
|
|
285
|
+
>
|
|
286
|
+
<SelectTrigger id="mtw-settings-timezone" aria-label="Time zone">
|
|
287
|
+
<SelectValue />
|
|
288
|
+
</SelectTrigger>
|
|
289
|
+
<SelectContent className="max-h-72">
|
|
290
|
+
<SelectItem value={LOCAL_TIME}>
|
|
291
|
+
{`Each viewer's local time (yours: ${localZone})`}
|
|
292
|
+
</SelectItem>
|
|
293
|
+
{zones.map((name) => (
|
|
294
|
+
<SelectItem key={name} value={name}>
|
|
295
|
+
{name.replace(/_/g, ' ')}
|
|
296
|
+
</SelectItem>
|
|
297
|
+
))}
|
|
298
|
+
</SelectContent>
|
|
299
|
+
</Select>
|
|
300
|
+
<p className="text-xs text-text-secondary">
|
|
301
|
+
Dates and times on every MoveTheWheels screen are shown in this zone.
|
|
302
|
+
</p>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div className="min-w-0 space-y-2">
|
|
306
|
+
<Label htmlFor="mtw-settings-distance">Distance unit</Label>
|
|
307
|
+
<Select
|
|
308
|
+
value={draft.distanceUnit}
|
|
309
|
+
onValueChange={(value: string) => {
|
|
310
|
+
const unit = DISTANCE_UNIT_OPTIONS.find((option) => option.value === value);
|
|
311
|
+
if (unit) update('distanceUnit', unit.value);
|
|
312
|
+
}}
|
|
313
|
+
disabled={readOnly || isSaving}
|
|
314
|
+
>
|
|
315
|
+
<SelectTrigger id="mtw-settings-distance" aria-label="Distance unit">
|
|
316
|
+
<SelectValue />
|
|
317
|
+
</SelectTrigger>
|
|
318
|
+
<SelectContent>
|
|
319
|
+
{DISTANCE_UNIT_OPTIONS.map((option) => (
|
|
320
|
+
<SelectItem key={option.value} value={option.value}>
|
|
321
|
+
{option.label}
|
|
322
|
+
</SelectItem>
|
|
323
|
+
))}
|
|
324
|
+
</SelectContent>
|
|
325
|
+
</Select>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div className="min-w-0 space-y-2">
|
|
329
|
+
<Label htmlFor="mtw-settings-weight">Weight unit</Label>
|
|
330
|
+
<Select
|
|
331
|
+
value={draft.weightUnit}
|
|
332
|
+
onValueChange={(value: string) => {
|
|
333
|
+
const unit = WEIGHT_UNIT_OPTIONS.find((option) => option.value === value);
|
|
334
|
+
if (unit) update('weightUnit', unit.value);
|
|
335
|
+
}}
|
|
336
|
+
disabled={readOnly || isSaving}
|
|
337
|
+
>
|
|
338
|
+
<SelectTrigger id="mtw-settings-weight" aria-label="Weight unit">
|
|
339
|
+
<SelectValue />
|
|
340
|
+
</SelectTrigger>
|
|
341
|
+
<SelectContent>
|
|
342
|
+
{WEIGHT_UNIT_OPTIONS.map((option) => (
|
|
343
|
+
<SelectItem key={option.value} value={option.value}>
|
|
344
|
+
{option.label}
|
|
345
|
+
</SelectItem>
|
|
346
|
+
))}
|
|
347
|
+
</SelectContent>
|
|
348
|
+
</Select>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
</fieldset>
|
|
352
|
+
|
|
353
|
+
<fieldset className="space-y-4" disabled={readOnly || isSaving}>
|
|
354
|
+
<legend className="text-sm font-semibold text-text-primary">New orders</legend>
|
|
355
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
356
|
+
<div className="min-w-0 space-y-2">
|
|
357
|
+
<Label htmlFor="mtw-settings-priority">Default priority</Label>
|
|
358
|
+
<Select
|
|
359
|
+
value={draft.defaultOrderPriority}
|
|
360
|
+
onValueChange={(value: string) => {
|
|
361
|
+
const priority = ORDER_PRIORITY_OPTIONS.find(
|
|
362
|
+
(option) => option.value === value
|
|
363
|
+
);
|
|
364
|
+
if (priority) update('defaultOrderPriority', priority.value);
|
|
365
|
+
}}
|
|
366
|
+
disabled={readOnly || isSaving}
|
|
367
|
+
>
|
|
368
|
+
<SelectTrigger id="mtw-settings-priority" aria-label="Default priority">
|
|
369
|
+
<SelectValue />
|
|
370
|
+
</SelectTrigger>
|
|
371
|
+
<SelectContent>
|
|
372
|
+
{ORDER_PRIORITY_OPTIONS.map((option) => (
|
|
373
|
+
<SelectItem key={option.value} value={option.value}>
|
|
374
|
+
{option.label}
|
|
375
|
+
</SelectItem>
|
|
376
|
+
))}
|
|
377
|
+
</SelectContent>
|
|
378
|
+
</Select>
|
|
379
|
+
<p className="text-xs text-text-secondary">
|
|
380
|
+
New orders start with this priority.
|
|
381
|
+
</p>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div className="min-w-0 space-y-2">
|
|
385
|
+
<Label htmlFor="mtw-settings-window">Default delivery window (hours)</Label>
|
|
386
|
+
<Input
|
|
387
|
+
id="mtw-settings-window"
|
|
388
|
+
type="number"
|
|
389
|
+
inputMode="numeric"
|
|
390
|
+
min={DELIVERY_WINDOW_MIN_HOURS}
|
|
391
|
+
max={DELIVERY_WINDOW_MAX_HOURS}
|
|
392
|
+
step={1}
|
|
393
|
+
value={draft.defaultDeliveryWindowHours}
|
|
394
|
+
aria-invalid={windowError ? true : undefined}
|
|
395
|
+
aria-describedby="mtw-settings-window-help"
|
|
396
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
|
397
|
+
update('defaultDeliveryWindowHours', event.target.value)
|
|
398
|
+
}
|
|
399
|
+
/>
|
|
400
|
+
<p
|
|
401
|
+
id="mtw-settings-window-help"
|
|
402
|
+
className={
|
|
403
|
+
windowError
|
|
404
|
+
? 'text-xs text-status-error-text'
|
|
405
|
+
: 'text-xs text-text-secondary'
|
|
406
|
+
}
|
|
407
|
+
>
|
|
408
|
+
{windowError ??
|
|
409
|
+
'The order form suggests a scheduled delivery this many hours ahead.'}
|
|
410
|
+
</p>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</fieldset>
|
|
414
|
+
|
|
415
|
+
<FormError message={formError} />
|
|
416
|
+
|
|
417
|
+
<div className="flex flex-wrap items-center justify-end gap-3">
|
|
418
|
+
<Button
|
|
419
|
+
type="button"
|
|
420
|
+
variant="outline"
|
|
421
|
+
disabled={!isDirty || isSaving}
|
|
422
|
+
onClick={() => {
|
|
423
|
+
setFormError(null);
|
|
424
|
+
setDraft(toDraft(settings));
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
<RotateCcw size={16} className="mr-2" /> Discard changes
|
|
428
|
+
</Button>
|
|
429
|
+
<Button type="submit" disabled={!canSave}>
|
|
430
|
+
{isSaving ? (
|
|
431
|
+
<Loader2 size={16} className="mr-2 animate-spin" />
|
|
432
|
+
) : (
|
|
433
|
+
<Save size={16} className="mr-2" />
|
|
434
|
+
)}
|
|
435
|
+
Save settings
|
|
436
|
+
</Button>
|
|
437
|
+
</div>
|
|
438
|
+
</form>
|
|
439
|
+
)}
|
|
440
|
+
</EmphasisPanel>
|
|
441
|
+
|
|
442
|
+
<div className="space-y-6">
|
|
443
|
+
<Card>
|
|
444
|
+
<CardHeader>
|
|
445
|
+
<CardTitle className="text-base">How it looks now</CardTitle>
|
|
446
|
+
<CardDescription>Examples using the saved settings.</CardDescription>
|
|
447
|
+
</CardHeader>
|
|
448
|
+
<CardContent>
|
|
449
|
+
<dl className="space-y-2 text-sm" data-testid="mtw-settings-preview">
|
|
450
|
+
<div className="flex justify-between gap-4">
|
|
451
|
+
<dt className="text-text-secondary">Amount</dt>
|
|
452
|
+
<dd className="font-medium text-text-primary">{formatCurrency(1234.5)}</dd>
|
|
453
|
+
</div>
|
|
454
|
+
<div className="flex justify-between gap-4">
|
|
455
|
+
<dt className="text-text-secondary">Distance</dt>
|
|
456
|
+
<dd className="font-medium text-text-primary">{formatDistance(42.2)}</dd>
|
|
457
|
+
</div>
|
|
458
|
+
<div className="flex justify-between gap-4">
|
|
459
|
+
<dt className="text-text-secondary">Weight</dt>
|
|
460
|
+
<dd className="font-medium text-text-primary">{formatWeight(750)}</dd>
|
|
461
|
+
</div>
|
|
462
|
+
<div className="flex justify-between gap-4">
|
|
463
|
+
<dt className="text-text-secondary">Date and time</dt>
|
|
464
|
+
<dd className="text-right font-medium text-text-primary">
|
|
465
|
+
{formatDateTime(previewSample)}
|
|
466
|
+
</dd>
|
|
467
|
+
</div>
|
|
468
|
+
</dl>
|
|
469
|
+
{settings.updatedAt && (
|
|
470
|
+
<p className="mt-4 text-xs text-text-secondary">
|
|
471
|
+
Last saved {formatDateTime(settings.updatedAt)}
|
|
472
|
+
</p>
|
|
473
|
+
)}
|
|
474
|
+
{loadError && (
|
|
475
|
+
<Button variant="outline" size="sm" className="mt-4" onClick={reload}>
|
|
476
|
+
Try loading again
|
|
477
|
+
</Button>
|
|
478
|
+
)}
|
|
479
|
+
</CardContent>
|
|
480
|
+
</Card>
|
|
481
|
+
|
|
482
|
+
<Card>
|
|
483
|
+
<CardHeader>
|
|
484
|
+
<CardTitle className="text-base">Other settings</CardTitle>
|
|
485
|
+
<CardDescription>
|
|
486
|
+
These are managed by the platform for every product.
|
|
487
|
+
</CardDescription>
|
|
488
|
+
</CardHeader>
|
|
489
|
+
<CardContent className="space-y-2">
|
|
490
|
+
{PLATFORM_LINKS.map((link) => {
|
|
491
|
+
const Icon = link.icon;
|
|
492
|
+
return (
|
|
493
|
+
<button
|
|
494
|
+
key={link.id}
|
|
495
|
+
type="button"
|
|
496
|
+
className="flex w-full items-start gap-3 rounded-lg border border-border-default px-3 py-2 text-left transition-colors hover:bg-surface-secondary"
|
|
497
|
+
onClick={() => navigate(link.path)}
|
|
498
|
+
>
|
|
499
|
+
<Icon size={18} className="mt-0.5 shrink-0 text-text-secondary" />
|
|
500
|
+
<span className="min-w-0 flex-1">
|
|
501
|
+
<span className="block text-sm font-medium text-text-primary">
|
|
502
|
+
{link.title}
|
|
503
|
+
</span>
|
|
504
|
+
<span className="block text-xs text-text-secondary">{link.description}</span>
|
|
505
|
+
</span>
|
|
506
|
+
<ExternalLink size={14} className="mt-1 shrink-0 text-text-tertiary" />
|
|
507
|
+
</button>
|
|
508
|
+
);
|
|
509
|
+
})}
|
|
510
|
+
</CardContent>
|
|
511
|
+
</Card>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
</PageLayout>
|
|
515
|
+
);
|
|
516
|
+
}
|
|
@@ -53,7 +53,7 @@ import { useRecordVehicleService } from '../hooks/useFleetIntelligence';
|
|
|
53
53
|
import { useRefetchOnLeaveEdit } from '../hooks/useRefetchOnLeaveEdit';
|
|
54
54
|
import { VehicleServiceCard } from '../components/VehicleServiceCard';
|
|
55
55
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
56
|
-
import { formatDate, getInitials } from '../utils/formatters';
|
|
56
|
+
import { formatDate, formatWeight, getInitials } from '../utils/formatters';
|
|
57
57
|
import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';
|
|
58
58
|
import { joinPath } from '../utils/navigation';
|
|
59
59
|
import { FormError } from '../components/FormError';
|
|
@@ -614,7 +614,9 @@ export default function VehicleDetailsPage() {
|
|
|
614
614
|
<div>
|
|
615
615
|
<p className="text-sm text-text-secondary">Weight</p>
|
|
616
616
|
<p className="text-2xl font-bold">
|
|
617
|
-
{vehicle.capacity.weight !== undefined
|
|
617
|
+
{vehicle.capacity.weight !== undefined
|
|
618
|
+
? formatWeight(vehicle.capacity.weight)
|
|
619
|
+
: '—'}
|
|
618
620
|
</p>
|
|
619
621
|
</div>
|
|
620
622
|
<div>
|
|
@@ -38,6 +38,7 @@ import { useEntityExport } from '../hooks/useEntityExport';
|
|
|
38
38
|
import { ExportStatusBanner } from '../components/ExportStatusBanner';
|
|
39
39
|
import { VEHICLE_STATUS_LABELS } from '../constants';
|
|
40
40
|
import type { Vehicle, VehicleStatus } from '../types';
|
|
41
|
+
import { formatWeight } from '../utils/formatters';
|
|
41
42
|
|
|
42
43
|
const STATUS_OPTIONS: Array<{ value: VehicleStatus | 'all'; label: string }> = [
|
|
43
44
|
{ value: 'all', label: 'All statuses' },
|
|
@@ -121,7 +122,7 @@ export default function VehiclesPage() {
|
|
|
121
122
|
header: 'Capacity',
|
|
122
123
|
cell: (row) => (
|
|
123
124
|
<div className="text-sm">
|
|
124
|
-
<p>{row.capacity.weight !== undefined ?
|
|
125
|
+
<p>{row.capacity.weight !== undefined ? formatWeight(row.capacity.weight) : '—'}</p>
|
|
125
126
|
<p className="text-text-secondary">
|
|
126
127
|
{row.capacity.volume !== undefined ? `${row.capacity.volume} m³` : '—'}
|
|
127
128
|
</p>
|