@burdenoff/microfe-movethewheels 2026.1101.3 → 2026.1102.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.
- package/dist/components/CustomerDeliveryUpdatesFields.js +57 -0
- package/dist/components/CustomerNotificationSettingsCard.js +368 -0
- package/dist/components/UpcomingDemandPanel.js +75 -77
- package/dist/generated/tracking-quality-operations.d.ts +275 -0
- package/dist/generated/wspace-operations.d.ts +3 -0
- package/dist/hooks/useCustomers.js +9 -3
- package/dist/hooks/useDemandForecast.js +1 -1
- package/dist/movethewheels/components/CustomerDeliveryUpdatesFields.d.ts +12 -0
- package/dist/movethewheels/components/CustomerNotificationSettingsCard.d.ts +3 -0
- package/dist/movethewheels/hooks/useDemandForecast.d.ts +3 -0
- package/dist/movethewheels/types/index.d.ts +4 -0
- package/dist/movethewheels/utils/customerNotifications.d.ts +66 -0
- package/dist/node_modules/lucide-react/dist/esm/icons/bell-ring.js +22 -0
- package/dist/pages/CustomerDetailsPage.js +279 -231
- package/dist/pages/SettingsPage.js +242 -238
- package/dist/utils/customerNotifications.js +83 -0
- package/package.json +3 -2
- package/src/generated/tracking-quality-operations.ts +304 -0
- package/src/generated/wspace-operations.ts +6 -6
- package/src/movethewheels/components/CustomerDeliveryUpdatesFields.tsx +67 -0
- package/src/movethewheels/components/CustomerNotificationSettingsCard.tsx +465 -0
- package/src/movethewheels/components/UpcomingDemandPanel.tsx +71 -29
- package/src/movethewheels/hooks/useCustomers.ts +17 -0
- package/src/movethewheels/hooks/useDemandForecast.ts +7 -0
- package/src/movethewheels/pages/CustomerDetailsPage.tsx +39 -0
- package/src/movethewheels/pages/SettingsPage.tsx +221 -208
- package/src/movethewheels/types/index.ts +4 -0
- package/src/movethewheels/utils/customerNotifications.ts +209 -0
- package/src/operations/wspace/movethewheels/queries/forecast.graphql +3 -0
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* the priority and delivery window new orders start from. Saving applies them
|
|
7
7
|
* to every MoveTheWheels screen straight away.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* It also holds the customer delivery notification settings (BOFF-4402): whether
|
|
10
|
+
* and how the workspace tells its customers about their shipments.
|
|
11
|
+
*
|
|
12
|
+
* Company profile, your own notification preferences, two-factor
|
|
13
|
+
* authentication and integrations are platform settings; this page links to
|
|
14
|
+
* them instead of duplicating them.
|
|
11
15
|
*/
|
|
12
16
|
import { useEffect, useMemo, useState, type ChangeEvent, type FormEvent } from 'react';
|
|
13
17
|
import {
|
|
@@ -23,6 +27,7 @@ import {
|
|
|
23
27
|
} from 'lucide-react';
|
|
24
28
|
import { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';
|
|
25
29
|
import { PermissionProvider, usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
30
|
+
import { CustomerNotificationSettingsCard } from '../components/CustomerNotificationSettingsCard';
|
|
26
31
|
import { PageLayout } from '../components/PageLayout';
|
|
27
32
|
import { FormError } from '../components/FormError';
|
|
28
33
|
import { Notice, useNotice } from '../components/Notice';
|
|
@@ -204,7 +209,7 @@ function SettingsWorkbench() {
|
|
|
204
209
|
return (
|
|
205
210
|
<PageLayout
|
|
206
211
|
title="Settings"
|
|
207
|
-
subtitle="Workspace defaults
|
|
212
|
+
subtitle="Workspace defaults, new-order defaults and customer delivery notifications"
|
|
208
213
|
icon={<Settings size={24} />}
|
|
209
214
|
maxWidth="xl"
|
|
210
215
|
>
|
|
@@ -222,223 +227,231 @@ function SettingsWorkbench() {
|
|
|
222
227
|
<Notice message={notice} />
|
|
223
228
|
|
|
224
229
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
|
225
|
-
<
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
{settings.isCustomized ? '
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
|
-
{!isLoaded ? (
|
|
236
|
-
<div className="space-y-4" aria-busy="true">
|
|
237
|
-
<Skeleton className="h-10 w-full" />
|
|
238
|
-
<Skeleton className="h-10 w-full" />
|
|
239
|
-
<Skeleton className="h-10 w-full" />
|
|
240
|
-
</div>
|
|
241
|
-
) : (
|
|
242
|
-
<form
|
|
243
|
-
className="space-y-6"
|
|
244
|
-
onSubmit={(event) => void onSubmit(event)}
|
|
245
|
-
aria-label="MoveTheWheels settings"
|
|
246
|
-
>
|
|
247
|
-
{readOnly && (
|
|
248
|
-
<p className="rounded-lg border border-border-default bg-surface-secondary px-4 py-3 text-sm text-text-secondary">
|
|
249
|
-
You can view these settings. Ask a workspace admin or the Logistics Director to
|
|
250
|
-
change them.
|
|
251
|
-
</p>
|
|
230
|
+
<div className="min-w-0 space-y-6 xl:col-span-2">
|
|
231
|
+
<EmphasisPanel className="space-y-6">
|
|
232
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
233
|
+
<h2 className="text-base font-semibold text-text-primary">Operational defaults</h2>
|
|
234
|
+
{isLoaded && (
|
|
235
|
+
<Badge variant={settings.isCustomized ? 'default' : 'outline'}>
|
|
236
|
+
{settings.isCustomized ? 'Customized for this workspace' : 'Platform defaults'}
|
|
237
|
+
</Badge>
|
|
252
238
|
)}
|
|
239
|
+
</div>
|
|
253
240
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
))}
|
|
273
|
-
</SelectContent>
|
|
274
|
-
</Select>
|
|
275
|
-
<p className="text-xs text-text-secondary">
|
|
276
|
-
Used for order totals, revenue and costs that have no currency of their own.
|
|
277
|
-
</p>
|
|
278
|
-
</div>
|
|
241
|
+
{!isLoaded ? (
|
|
242
|
+
<div className="space-y-4" aria-busy="true">
|
|
243
|
+
<Skeleton className="h-10 w-full" />
|
|
244
|
+
<Skeleton className="h-10 w-full" />
|
|
245
|
+
<Skeleton className="h-10 w-full" />
|
|
246
|
+
</div>
|
|
247
|
+
) : (
|
|
248
|
+
<form
|
|
249
|
+
className="space-y-6"
|
|
250
|
+
onSubmit={(event) => void onSubmit(event)}
|
|
251
|
+
aria-label="MoveTheWheels settings"
|
|
252
|
+
>
|
|
253
|
+
{readOnly && (
|
|
254
|
+
<p className="rounded-lg border border-border-default bg-surface-secondary px-4 py-3 text-sm text-text-secondary">
|
|
255
|
+
You can view these settings. Ask a workspace admin or the Logistics Director to
|
|
256
|
+
change them.
|
|
257
|
+
</p>
|
|
258
|
+
)}
|
|
279
259
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
260
|
+
<fieldset className="space-y-4" disabled={readOnly || isSaving}>
|
|
261
|
+
<legend className="text-sm font-semibold text-text-primary">Display</legend>
|
|
262
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
263
|
+
<div className="min-w-0 space-y-2">
|
|
264
|
+
<Label htmlFor="mtw-settings-currency">Default currency</Label>
|
|
265
|
+
<Select
|
|
266
|
+
value={draft.defaultCurrency}
|
|
267
|
+
onValueChange={(value: string) => update('defaultCurrency', value)}
|
|
268
|
+
disabled={readOnly || isSaving}
|
|
269
|
+
>
|
|
270
|
+
<SelectTrigger id="mtw-settings-currency" aria-label="Default currency">
|
|
271
|
+
<SelectValue />
|
|
272
|
+
</SelectTrigger>
|
|
273
|
+
<SelectContent className="max-h-72">
|
|
274
|
+
{currencies.map((code) => (
|
|
275
|
+
<SelectItem key={code} value={code}>
|
|
276
|
+
{currencyLabel(code)}
|
|
277
|
+
</SelectItem>
|
|
278
|
+
))}
|
|
279
|
+
</SelectContent>
|
|
280
|
+
</Select>
|
|
281
|
+
<p className="text-xs text-text-secondary">
|
|
282
|
+
Used for order totals, revenue and costs that have no currency of their own.
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
305
285
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
<SelectContent>
|
|
320
|
-
{DISTANCE_UNIT_OPTIONS.map((option) => (
|
|
321
|
-
<SelectItem key={option.value} value={option.value}>
|
|
322
|
-
{option.label}
|
|
286
|
+
<div className="min-w-0 space-y-2">
|
|
287
|
+
<Label htmlFor="mtw-settings-timezone">Time zone</Label>
|
|
288
|
+
<Select
|
|
289
|
+
value={draft.timezone}
|
|
290
|
+
onValueChange={(value: string) => update('timezone', value)}
|
|
291
|
+
disabled={readOnly || isSaving}
|
|
292
|
+
>
|
|
293
|
+
<SelectTrigger id="mtw-settings-timezone" aria-label="Time zone">
|
|
294
|
+
<SelectValue />
|
|
295
|
+
</SelectTrigger>
|
|
296
|
+
<SelectContent className="max-h-72">
|
|
297
|
+
<SelectItem value={LOCAL_TIME}>
|
|
298
|
+
{`Each viewer's local time (yours: ${localZone})`}
|
|
323
299
|
</SelectItem>
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
300
|
+
{zones.map((name) => (
|
|
301
|
+
<SelectItem key={name} value={name}>
|
|
302
|
+
{name.replace(/_/g, ' ')}
|
|
303
|
+
</SelectItem>
|
|
304
|
+
))}
|
|
305
|
+
</SelectContent>
|
|
306
|
+
</Select>
|
|
307
|
+
<p className="text-xs text-text-secondary">
|
|
308
|
+
Dates and times on every MoveTheWheels screen are shown in this zone.
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
328
311
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
312
|
+
<div className="min-w-0 space-y-2">
|
|
313
|
+
<Label htmlFor="mtw-settings-distance">Distance unit</Label>
|
|
314
|
+
<Select
|
|
315
|
+
value={draft.distanceUnit}
|
|
316
|
+
onValueChange={(value: string) => {
|
|
317
|
+
const unit = DISTANCE_UNIT_OPTIONS.find(
|
|
318
|
+
(option) => option.value === value
|
|
319
|
+
);
|
|
320
|
+
if (unit) update('distanceUnit', unit.value);
|
|
321
|
+
}}
|
|
322
|
+
disabled={readOnly || isSaving}
|
|
323
|
+
>
|
|
324
|
+
<SelectTrigger id="mtw-settings-distance" aria-label="Distance unit">
|
|
325
|
+
<SelectValue />
|
|
326
|
+
</SelectTrigger>
|
|
327
|
+
<SelectContent>
|
|
328
|
+
{DISTANCE_UNIT_OPTIONS.map((option) => (
|
|
329
|
+
<SelectItem key={option.value} value={option.value}>
|
|
330
|
+
{option.label}
|
|
331
|
+
</SelectItem>
|
|
332
|
+
))}
|
|
333
|
+
</SelectContent>
|
|
334
|
+
</Select>
|
|
335
|
+
</div>
|
|
353
336
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
</SelectItem>
|
|
377
|
-
))}
|
|
378
|
-
</SelectContent>
|
|
379
|
-
</Select>
|
|
380
|
-
<p className="text-xs text-text-secondary">
|
|
381
|
-
New orders start with this priority.
|
|
382
|
-
</p>
|
|
337
|
+
<div className="min-w-0 space-y-2">
|
|
338
|
+
<Label htmlFor="mtw-settings-weight">Weight unit</Label>
|
|
339
|
+
<Select
|
|
340
|
+
value={draft.weightUnit}
|
|
341
|
+
onValueChange={(value: string) => {
|
|
342
|
+
const unit = WEIGHT_UNIT_OPTIONS.find((option) => option.value === value);
|
|
343
|
+
if (unit) update('weightUnit', unit.value);
|
|
344
|
+
}}
|
|
345
|
+
disabled={readOnly || isSaving}
|
|
346
|
+
>
|
|
347
|
+
<SelectTrigger id="mtw-settings-weight" aria-label="Weight unit">
|
|
348
|
+
<SelectValue />
|
|
349
|
+
</SelectTrigger>
|
|
350
|
+
<SelectContent>
|
|
351
|
+
{WEIGHT_UNIT_OPTIONS.map((option) => (
|
|
352
|
+
<SelectItem key={option.value} value={option.value}>
|
|
353
|
+
{option.label}
|
|
354
|
+
</SelectItem>
|
|
355
|
+
))}
|
|
356
|
+
</SelectContent>
|
|
357
|
+
</Select>
|
|
358
|
+
</div>
|
|
383
359
|
</div>
|
|
360
|
+
</fieldset>
|
|
361
|
+
|
|
362
|
+
<fieldset className="space-y-4" disabled={readOnly || isSaving}>
|
|
363
|
+
<legend className="text-sm font-semibold text-text-primary">New orders</legend>
|
|
364
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
365
|
+
<div className="min-w-0 space-y-2">
|
|
366
|
+
<Label htmlFor="mtw-settings-priority">Default priority</Label>
|
|
367
|
+
<Select
|
|
368
|
+
value={draft.defaultOrderPriority}
|
|
369
|
+
onValueChange={(value: string) => {
|
|
370
|
+
const priority = ORDER_PRIORITY_OPTIONS.find(
|
|
371
|
+
(option) => option.value === value
|
|
372
|
+
);
|
|
373
|
+
if (priority) update('defaultOrderPriority', priority.value);
|
|
374
|
+
}}
|
|
375
|
+
disabled={readOnly || isSaving}
|
|
376
|
+
>
|
|
377
|
+
<SelectTrigger id="mtw-settings-priority" aria-label="Default priority">
|
|
378
|
+
<SelectValue />
|
|
379
|
+
</SelectTrigger>
|
|
380
|
+
<SelectContent>
|
|
381
|
+
{ORDER_PRIORITY_OPTIONS.map((option) => (
|
|
382
|
+
<SelectItem key={option.value} value={option.value}>
|
|
383
|
+
{option.label}
|
|
384
|
+
</SelectItem>
|
|
385
|
+
))}
|
|
386
|
+
</SelectContent>
|
|
387
|
+
</Select>
|
|
388
|
+
<p className="text-xs text-text-secondary">
|
|
389
|
+
New orders start with this priority.
|
|
390
|
+
</p>
|
|
391
|
+
</div>
|
|
384
392
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
393
|
+
<div className="min-w-0 space-y-2">
|
|
394
|
+
<Label htmlFor="mtw-settings-window">Default delivery window (hours)</Label>
|
|
395
|
+
<Input
|
|
396
|
+
id="mtw-settings-window"
|
|
397
|
+
type="number"
|
|
398
|
+
inputMode="numeric"
|
|
399
|
+
min={DELIVERY_WINDOW_MIN_HOURS}
|
|
400
|
+
max={DELIVERY_WINDOW_MAX_HOURS}
|
|
401
|
+
step={1}
|
|
402
|
+
value={draft.defaultDeliveryWindowHours}
|
|
403
|
+
aria-invalid={windowError ? true : undefined}
|
|
404
|
+
aria-describedby="mtw-settings-window-help"
|
|
405
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
|
406
|
+
update('defaultDeliveryWindowHours', event.target.value)
|
|
407
|
+
}
|
|
408
|
+
/>
|
|
409
|
+
<p
|
|
410
|
+
id="mtw-settings-window-help"
|
|
411
|
+
className={
|
|
412
|
+
windowError
|
|
413
|
+
? 'text-xs text-status-error-text'
|
|
414
|
+
: 'text-xs text-text-secondary'
|
|
415
|
+
}
|
|
416
|
+
>
|
|
417
|
+
{windowError ??
|
|
418
|
+
'The order form suggests a scheduled delivery this many hours ahead.'}
|
|
419
|
+
</p>
|
|
420
|
+
</div>
|
|
412
421
|
</div>
|
|
413
|
-
</
|
|
414
|
-
</fieldset>
|
|
422
|
+
</fieldset>
|
|
415
423
|
|
|
416
|
-
|
|
424
|
+
<FormError message={formError} />
|
|
417
425
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
426
|
+
<div className="flex flex-wrap items-center justify-end gap-3">
|
|
427
|
+
<Button
|
|
428
|
+
type="button"
|
|
429
|
+
variant="outline"
|
|
430
|
+
disabled={!isDirty || isSaving}
|
|
431
|
+
onClick={() => {
|
|
432
|
+
setFormError(null);
|
|
433
|
+
setDraft(toDraft(settings));
|
|
434
|
+
}}
|
|
435
|
+
>
|
|
436
|
+
<RotateCcw size={16} className="mr-2" /> Discard changes
|
|
437
|
+
</Button>
|
|
438
|
+
<Button type="submit" disabled={!canSave}>
|
|
439
|
+
{isSaving ? (
|
|
440
|
+
<Loader2 size={16} className="mr-2 animate-spin" />
|
|
441
|
+
) : (
|
|
442
|
+
<Save size={16} className="mr-2" />
|
|
443
|
+
)}
|
|
444
|
+
Save settings
|
|
445
|
+
</Button>
|
|
446
|
+
</div>
|
|
447
|
+
</form>
|
|
448
|
+
)}
|
|
449
|
+
</EmphasisPanel>
|
|
450
|
+
|
|
451
|
+
{/* Keyed by workspace: a workspace switch remounts the card, so no
|
|
452
|
+
settings, draft or log from the previous workspace survive it. */}
|
|
453
|
+
<CustomerNotificationSettingsCard key={workspaceId ?? 'none'} readOnly={readOnly} />
|
|
454
|
+
</div>
|
|
442
455
|
|
|
443
456
|
<div className="space-y-6">
|
|
444
457
|
<Card>
|
|
@@ -180,6 +180,10 @@ export interface Customer {
|
|
|
180
180
|
businessLicense?: string;
|
|
181
181
|
taxId?: string;
|
|
182
182
|
rating?: number;
|
|
183
|
+
/** Delivery notifications by email (BOFF-4402). */
|
|
184
|
+
notifyByEmail: boolean;
|
|
185
|
+
/** Delivery notifications by SMS; needs a phone number with its country code. */
|
|
186
|
+
notifyBySms: boolean;
|
|
183
187
|
createdAt: Date;
|
|
184
188
|
updatedAt: Date;
|
|
185
189
|
}
|