@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
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
* Surfaces next-week demand forecasts by lane / customer alongside the
|
|
5
5
|
* most recent closed-period variance (BOFF-4393). Pure read-side view
|
|
6
6
|
* over `wspace-movethewheels-svc`'s DemandForecast surface.
|
|
7
|
+
*
|
|
8
|
+
* "Next week" is the first forecast week that has not begun yet: the current
|
|
9
|
+
* week's rows are kept server-side for the closed-period variance, but they
|
|
10
|
+
* are not upcoming demand. Cold-start rows (confidence 0, fewer than 4 weeks
|
|
11
|
+
* of history for that lane or customer) are not shown as forecasts.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import { Calendar, TrendingUp, TrendingDown, AlertCircle } from 'lucide-react';
|
|
10
15
|
|
|
11
16
|
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from './ui';
|
|
17
|
+
import { formatCurrency } from '../utils/formatters';
|
|
12
18
|
|
|
13
19
|
import type { ForecastPoint, ForecastVariance } from '../hooks/useDemandForecast';
|
|
14
20
|
|
|
@@ -28,20 +34,41 @@ const formatWeek = (iso: string): string => {
|
|
|
28
34
|
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
29
35
|
};
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
37
|
+
/** Rows for one week that rest on enough history to be a forecast. */
|
|
38
|
+
const forecastsFor = (rows: ForecastPoint[], weekStart: string): ForecastPoint[] =>
|
|
39
|
+
rows.filter((row) => row.periodStart === weekStart && row.confidence > 0);
|
|
35
40
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
const topByVolume = (rows: ForecastPoint[], weekStart: string): ForecastPoint[] =>
|
|
42
|
+
[...forecastsFor(rows, weekStart)]
|
|
43
|
+
.sort(
|
|
44
|
+
(a, b) => b.predictedVolume - a.predictedVolume || b.predictedRevenue - a.predictedRevenue
|
|
45
|
+
)
|
|
46
|
+
.slice(0, 3);
|
|
47
|
+
|
|
48
|
+
const topByRevenue = (rows: ForecastPoint[], weekStart: string): ForecastPoint[] =>
|
|
49
|
+
[...forecastsFor(rows, weekStart)]
|
|
50
|
+
.sort((a, b) => b.predictedRevenue - a.predictedRevenue)
|
|
51
|
+
.slice(0, 3);
|
|
40
52
|
|
|
41
53
|
const totalVolume = (rows: ForecastPoint[], weekStart: string): number =>
|
|
42
|
-
rows
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
forecastsFor(rows, weekStart).reduce((sum, row) => sum + row.predictedVolume, 0);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The first forecast week that has not started yet, else the latest week on
|
|
58
|
+
* offer (rows arrive sorted by period).
|
|
59
|
+
*/
|
|
60
|
+
const upcomingWeekStart = (rows: ForecastPoint[], now: Date = new Date()): string | null => {
|
|
61
|
+
const starts = [...new Set(rows.map((row) => row.periodStart))].sort(
|
|
62
|
+
(a, b) => new Date(a).getTime() - new Date(b).getTime()
|
|
63
|
+
);
|
|
64
|
+
return starts.find((start) => new Date(start).getTime() > now.getTime()) ?? starts.at(-1) ?? null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** `3 jobs`, `1 job`, or `<1 job` when demand is real but under one a week. */
|
|
68
|
+
const formatJobs = (volume: number, revenue: number): string => {
|
|
69
|
+
if (volume === 0 && revenue > 0) return '<1 job';
|
|
70
|
+
return volume === 1 ? '1 job' : `${volume} jobs`;
|
|
71
|
+
};
|
|
45
72
|
|
|
46
73
|
const formatConfidence = (confidence: number): string => {
|
|
47
74
|
if (confidence <= 0) return 'Not enough history';
|
|
@@ -90,9 +117,13 @@ export function UpcomingDemandPanel({
|
|
|
90
117
|
);
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
const nextWeekStart = laneForecast
|
|
120
|
+
const nextWeekStart = upcomingWeekStart([...laneForecast, ...customerForecast]);
|
|
121
|
+
const hasForecast =
|
|
122
|
+
nextWeekStart !== null &&
|
|
123
|
+
(forecastsFor(laneForecast, nextWeekStart).length > 0 ||
|
|
124
|
+
forecastsFor(customerForecast, nextWeekStart).length > 0);
|
|
94
125
|
|
|
95
|
-
if (!nextWeekStart ||
|
|
126
|
+
if (!nextWeekStart || !hasForecast) {
|
|
96
127
|
return (
|
|
97
128
|
<Card>
|
|
98
129
|
<CardHeader>
|
|
@@ -105,8 +136,8 @@ export function UpcomingDemandPanel({
|
|
|
105
136
|
<div className="flex items-start gap-2 rounded-lg border border-border/60 bg-muted/40 p-4 text-sm text-text-secondary">
|
|
106
137
|
<AlertCircle size={16} aria-hidden="true" className="mt-0.5 shrink-0" />
|
|
107
138
|
<p>
|
|
108
|
-
Not enough history to forecast next week yet. Once
|
|
109
|
-
|
|
139
|
+
Not enough history to forecast next week yet. Once a lane or customer has at least 4
|
|
140
|
+
weeks of delivery jobs, this panel will surface its demand.
|
|
110
141
|
</p>
|
|
111
142
|
</div>
|
|
112
143
|
</CardContent>
|
|
@@ -117,7 +148,18 @@ export function UpcomingDemandPanel({
|
|
|
117
148
|
const topLanes = topByVolume(laneForecast, nextWeekStart);
|
|
118
149
|
const topLanesByRevenue = topByRevenue(laneForecast, nextWeekStart);
|
|
119
150
|
const topCustomers = topByVolume(customerForecast, nextWeekStart);
|
|
120
|
-
|
|
151
|
+
// The headline total comes from lane forecasts when any lane clears the
|
|
152
|
+
// history threshold. A customer can have enough history while each of its
|
|
153
|
+
// lanes does not, so fall back to the customer total rather than show a
|
|
154
|
+
// lane total of 0 beside valid customer forecasts.
|
|
155
|
+
const hasLaneForecast = forecastsFor(laneForecast, nextWeekStart).length > 0;
|
|
156
|
+
const total = hasLaneForecast
|
|
157
|
+
? totalVolume(laneForecast, nextWeekStart)
|
|
158
|
+
: totalVolume(customerForecast, nextWeekStart);
|
|
159
|
+
const totalLabel = hasLaneForecast
|
|
160
|
+
? 'Predicted jobs (all lanes)'
|
|
161
|
+
: 'Predicted jobs (all customers)';
|
|
162
|
+
const headlineRow = hasLaneForecast ? topLanes[0] : topCustomers[0];
|
|
121
163
|
|
|
122
164
|
// Most-recent variance row per scope key — show the lane that overshot /
|
|
123
165
|
// undershot the most in absolute volume terms.
|
|
@@ -136,13 +178,11 @@ export function UpcomingDemandPanel({
|
|
|
136
178
|
<CardContent className="space-y-4">
|
|
137
179
|
<div className="flex items-baseline justify-between rounded-lg border border-border/60 bg-muted/40 px-4 py-3">
|
|
138
180
|
<div>
|
|
139
|
-
<p className="text-xs uppercase tracking-wide text-text-secondary">
|
|
140
|
-
Predicted jobs (all lanes)
|
|
141
|
-
</p>
|
|
181
|
+
<p className="text-xs uppercase tracking-wide text-text-secondary">{totalLabel}</p>
|
|
142
182
|
<p className="text-2xl font-bold">{total}</p>
|
|
143
183
|
</div>
|
|
144
184
|
<p className="text-xs text-text-secondary">
|
|
145
|
-
{
|
|
185
|
+
{headlineRow ? formatConfidence(headlineRow.confidence) : ''}
|
|
146
186
|
</p>
|
|
147
187
|
</div>
|
|
148
188
|
|
|
@@ -180,14 +220,12 @@ function ForecastList({ title, rows }: ForecastListProps) {
|
|
|
180
220
|
key={`${row.scopeType}-${row.scopeKey}`}
|
|
181
221
|
className="flex items-center justify-between text-sm"
|
|
182
222
|
>
|
|
183
|
-
<span className="truncate font-medium"
|
|
223
|
+
<span className="truncate font-medium" title={row.scopeLabel}>
|
|
224
|
+
{row.scopeLabel}
|
|
225
|
+
</span>
|
|
184
226
|
<span className="ml-2 shrink-0 text-text-secondary">
|
|
185
|
-
{row.predictedVolume}
|
|
186
|
-
{
|
|
187
|
-
style: 'currency',
|
|
188
|
-
currency: 'USD',
|
|
189
|
-
maximumFractionDigits: 0,
|
|
190
|
-
}).format(row.predictedRevenue)}
|
|
227
|
+
{formatJobs(row.predictedVolume, row.predictedRevenue)} ·{' '}
|
|
228
|
+
{formatCurrency(row.predictedRevenue)}
|
|
191
229
|
</span>
|
|
192
230
|
</li>
|
|
193
231
|
))}
|
|
@@ -219,11 +257,15 @@ function VarianceList({ rows }: VarianceListProps) {
|
|
|
219
257
|
>
|
|
220
258
|
<span className="flex items-center gap-2 truncate">
|
|
221
259
|
<Icon size={14} className={tone} aria-hidden="true" />
|
|
222
|
-
<span className="truncate font-medium"
|
|
260
|
+
<span className="truncate font-medium" title={row.scopeLabel}>
|
|
261
|
+
{row.scopeLabel}
|
|
262
|
+
</span>
|
|
223
263
|
</span>
|
|
224
264
|
<span className={`ml-2 shrink-0 ${tone}`}>
|
|
225
265
|
{isOvershoot ? '+' : ''}
|
|
226
|
-
{row.volumeVariance
|
|
266
|
+
{Math.abs(row.volumeVariance) === 1
|
|
267
|
+
? `${row.volumeVariance} job`
|
|
268
|
+
: `${row.volumeVariance} jobs`}
|
|
227
269
|
</span>
|
|
228
270
|
</li>
|
|
229
271
|
);
|
|
@@ -35,6 +35,8 @@ interface ApiCustomer {
|
|
|
35
35
|
businessLicense?: string | null;
|
|
36
36
|
taxId?: string | null;
|
|
37
37
|
rating?: number | null;
|
|
38
|
+
notifyByEmail?: boolean | null;
|
|
39
|
+
notifyBySms?: boolean | null;
|
|
38
40
|
createdAt: string;
|
|
39
41
|
updatedAt: string;
|
|
40
42
|
deletedAt?: string | null;
|
|
@@ -53,6 +55,8 @@ const LIST_CUSTOMERS_QUERY = `
|
|
|
53
55
|
businessLicense
|
|
54
56
|
taxId
|
|
55
57
|
rating
|
|
58
|
+
notifyByEmail
|
|
59
|
+
notifyBySms
|
|
56
60
|
createdAt
|
|
57
61
|
updatedAt
|
|
58
62
|
deletedAt
|
|
@@ -74,6 +78,8 @@ const GET_CUSTOMER_QUERY = `
|
|
|
74
78
|
businessLicense
|
|
75
79
|
taxId
|
|
76
80
|
rating
|
|
81
|
+
notifyByEmail
|
|
82
|
+
notifyBySms
|
|
77
83
|
createdAt
|
|
78
84
|
updatedAt
|
|
79
85
|
deletedAt
|
|
@@ -94,6 +100,8 @@ const CREATE_CUSTOMER_MUTATION = `
|
|
|
94
100
|
businessLicense
|
|
95
101
|
taxId
|
|
96
102
|
rating
|
|
103
|
+
notifyByEmail
|
|
104
|
+
notifyBySms
|
|
97
105
|
createdAt
|
|
98
106
|
updatedAt
|
|
99
107
|
deletedAt
|
|
@@ -114,6 +122,8 @@ const UPDATE_CUSTOMER_MUTATION = `
|
|
|
114
122
|
businessLicense
|
|
115
123
|
taxId
|
|
116
124
|
rating
|
|
125
|
+
notifyByEmail
|
|
126
|
+
notifyBySms
|
|
117
127
|
createdAt
|
|
118
128
|
updatedAt
|
|
119
129
|
deletedAt
|
|
@@ -144,6 +154,9 @@ function mapCustomer(customer: ApiCustomer): Customer {
|
|
|
144
154
|
businessLicense: customer.businessLicense ?? undefined,
|
|
145
155
|
taxId: customer.taxId ?? undefined,
|
|
146
156
|
rating: customer.rating ?? undefined,
|
|
157
|
+
// Absent only from an older service; match its defaults (email on, SMS off).
|
|
158
|
+
notifyByEmail: customer.notifyByEmail ?? true,
|
|
159
|
+
notifyBySms: customer.notifyBySms ?? false,
|
|
147
160
|
createdAt: new Date(customer.createdAt),
|
|
148
161
|
updatedAt: new Date(customer.updatedAt),
|
|
149
162
|
};
|
|
@@ -263,6 +276,8 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
263
276
|
businessLicense: customerData.businessLicense,
|
|
264
277
|
taxId: customerData.taxId,
|
|
265
278
|
rating: customerData.rating,
|
|
279
|
+
notifyByEmail: customerData.notifyByEmail,
|
|
280
|
+
notifyBySms: customerData.notifyBySms,
|
|
266
281
|
},
|
|
267
282
|
});
|
|
268
283
|
|
|
@@ -289,6 +304,8 @@ export function useCustomers(options: UseCustomersOptions = {}): UseCustomersRes
|
|
|
289
304
|
businessLicense: updates.businessLicense,
|
|
290
305
|
taxId: updates.taxId,
|
|
291
306
|
rating: updates.rating,
|
|
307
|
+
notifyByEmail: updates.notifyByEmail,
|
|
308
|
+
notifyBySms: updates.notifyBySms,
|
|
292
309
|
},
|
|
293
310
|
});
|
|
294
311
|
|
|
@@ -12,6 +12,8 @@ import { executeGraphQL } from '../utils/graphqlClient';
|
|
|
12
12
|
export interface ForecastPoint {
|
|
13
13
|
scopeType: 'LANE' | 'CUSTOMER';
|
|
14
14
|
scopeKey: string;
|
|
15
|
+
/** Customer name or `Origin → Destination`; falls back to the key. */
|
|
16
|
+
scopeLabel: string;
|
|
15
17
|
periodStart: string;
|
|
16
18
|
periodEnd: string;
|
|
17
19
|
predictedVolume: number;
|
|
@@ -24,6 +26,7 @@ export interface ForecastPoint {
|
|
|
24
26
|
export interface ForecastVariance {
|
|
25
27
|
scopeType: 'LANE' | 'CUSTOMER';
|
|
26
28
|
scopeKey: string;
|
|
29
|
+
scopeLabel: string;
|
|
27
30
|
periodStart: string;
|
|
28
31
|
periodEnd: string;
|
|
29
32
|
predictedVolume: number;
|
|
@@ -49,6 +52,7 @@ const FORECAST_QUERY = `
|
|
|
49
52
|
demandForecastByLane(weeks: $weeks) {
|
|
50
53
|
scopeType
|
|
51
54
|
scopeKey
|
|
55
|
+
scopeLabel
|
|
52
56
|
periodStart
|
|
53
57
|
periodEnd
|
|
54
58
|
predictedVolume
|
|
@@ -60,6 +64,7 @@ const FORECAST_QUERY = `
|
|
|
60
64
|
demandForecastByCustomer(weeks: $weeks) {
|
|
61
65
|
scopeType
|
|
62
66
|
scopeKey
|
|
67
|
+
scopeLabel
|
|
63
68
|
periodStart
|
|
64
69
|
periodEnd
|
|
65
70
|
predictedVolume
|
|
@@ -71,6 +76,7 @@ const FORECAST_QUERY = `
|
|
|
71
76
|
laneVariance: demandForecastVariance(scopeType: LANE, limit: $varianceLimit) {
|
|
72
77
|
scopeType
|
|
73
78
|
scopeKey
|
|
79
|
+
scopeLabel
|
|
74
80
|
periodStart
|
|
75
81
|
periodEnd
|
|
76
82
|
predictedVolume
|
|
@@ -83,6 +89,7 @@ const FORECAST_QUERY = `
|
|
|
83
89
|
customerVariance: demandForecastVariance(scopeType: CUSTOMER, limit: $varianceLimit) {
|
|
84
90
|
scopeType
|
|
85
91
|
scopeKey
|
|
92
|
+
scopeLabel
|
|
86
93
|
periodStart
|
|
87
94
|
periodEnd
|
|
88
95
|
predictedVolume
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
Package,
|
|
19
19
|
X,
|
|
20
20
|
Trash2,
|
|
21
|
+
BellRing,
|
|
21
22
|
} from 'lucide-react';
|
|
22
23
|
import { PageLayout, LoadingState, ErrorState } from '../components/PageLayout';
|
|
23
24
|
import { TypeBadge, StatusBadge } from '../components/StatusBadge';
|
|
@@ -51,6 +52,10 @@ import { useOrders } from '../hooks/useOrders';
|
|
|
51
52
|
import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
|
|
52
53
|
import { formatAddress, formatCurrency, formatDate } from '../utils/formatters';
|
|
53
54
|
import { joinPath, buildOrderCustomerPath, getSafeCustomerReturnPath } from '../utils/navigation';
|
|
55
|
+
import {
|
|
56
|
+
CustomerDeliveryUpdatesFields,
|
|
57
|
+
deliveryUpdatesSummary,
|
|
58
|
+
} from '../components/CustomerDeliveryUpdatesFields';
|
|
54
59
|
import { FormError } from '../components/FormError';
|
|
55
60
|
import { Notice, useNotice } from '../components/Notice';
|
|
56
61
|
|
|
@@ -68,6 +73,8 @@ interface CustomerFormState {
|
|
|
68
73
|
businessLicense: string;
|
|
69
74
|
taxId: string;
|
|
70
75
|
rating: string;
|
|
76
|
+
notifyByEmail: boolean;
|
|
77
|
+
notifyBySms: boolean;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
const emptyCustomerForm: CustomerFormState = {
|
|
@@ -84,6 +91,8 @@ const emptyCustomerForm: CustomerFormState = {
|
|
|
84
91
|
businessLicense: '',
|
|
85
92
|
taxId: '',
|
|
86
93
|
rating: '',
|
|
94
|
+
notifyByEmail: true,
|
|
95
|
+
notifyBySms: false,
|
|
87
96
|
};
|
|
88
97
|
|
|
89
98
|
function NewCustomerForm() {
|
|
@@ -125,6 +134,8 @@ function NewCustomerForm() {
|
|
|
125
134
|
businessLicense: form.businessLicense.trim() || undefined,
|
|
126
135
|
taxId: form.taxId.trim() || undefined,
|
|
127
136
|
rating: form.rating ? Number(form.rating) : undefined,
|
|
137
|
+
notifyByEmail: form.notifyByEmail,
|
|
138
|
+
notifyBySms: form.notifyBySms,
|
|
128
139
|
});
|
|
129
140
|
const destination = returnTo
|
|
130
141
|
? buildOrderCustomerPath(returnTo, created.id)
|
|
@@ -320,6 +331,13 @@ function NewCustomerForm() {
|
|
|
320
331
|
/>
|
|
321
332
|
</div>
|
|
322
333
|
</div>
|
|
334
|
+
<div className="mt-6">
|
|
335
|
+
<CustomerDeliveryUpdatesFields
|
|
336
|
+
idPrefix="new-customer"
|
|
337
|
+
value={{ notifyByEmail: form.notifyByEmail, notifyBySms: form.notifyBySms }}
|
|
338
|
+
onChange={(next) => setForm((prev) => ({ ...prev, ...next }))}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
323
341
|
</CardContent>
|
|
324
342
|
</Card>
|
|
325
343
|
</form>
|
|
@@ -343,6 +361,8 @@ function customerToForm(customer: {
|
|
|
343
361
|
businessLicense?: string;
|
|
344
362
|
taxId?: string;
|
|
345
363
|
rating?: number;
|
|
364
|
+
notifyByEmail: boolean;
|
|
365
|
+
notifyBySms: boolean;
|
|
346
366
|
}): CustomerFormState {
|
|
347
367
|
return {
|
|
348
368
|
name: customer.name,
|
|
@@ -358,6 +378,8 @@ function customerToForm(customer: {
|
|
|
358
378
|
businessLicense: customer.businessLicense ?? '',
|
|
359
379
|
taxId: customer.taxId ?? '',
|
|
360
380
|
rating: customer.rating ? String(customer.rating) : '',
|
|
381
|
+
notifyByEmail: customer.notifyByEmail,
|
|
382
|
+
notifyBySms: customer.notifyBySms,
|
|
361
383
|
};
|
|
362
384
|
}
|
|
363
385
|
|
|
@@ -424,6 +446,8 @@ export default function CustomerDetailsPage() {
|
|
|
424
446
|
businessLicense: form.businessLicense.trim() || undefined,
|
|
425
447
|
taxId: form.taxId.trim() || undefined,
|
|
426
448
|
rating: form.rating ? Number(form.rating) : undefined,
|
|
449
|
+
notifyByEmail: form.notifyByEmail,
|
|
450
|
+
notifyBySms: form.notifyBySms,
|
|
427
451
|
});
|
|
428
452
|
notify('Customer updated.');
|
|
429
453
|
setEditing(false);
|
|
@@ -662,6 +686,12 @@ export default function CustomerDetailsPage() {
|
|
|
662
686
|
/>
|
|
663
687
|
</div>
|
|
664
688
|
</div>
|
|
689
|
+
<CustomerDeliveryUpdatesFields
|
|
690
|
+
idPrefix="edit-customer"
|
|
691
|
+
value={{ notifyByEmail: form.notifyByEmail, notifyBySms: form.notifyBySms }}
|
|
692
|
+
onChange={(next) => setForm((prev) => ({ ...prev, ...next }))}
|
|
693
|
+
disabled={saving}
|
|
694
|
+
/>
|
|
665
695
|
</div>
|
|
666
696
|
) : (
|
|
667
697
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
@@ -691,6 +721,15 @@ export default function CustomerDetailsPage() {
|
|
|
691
721
|
</div>
|
|
692
722
|
</div>
|
|
693
723
|
)}
|
|
724
|
+
<div className="flex items-center gap-3">
|
|
725
|
+
<BellRing size={18} className="text-text-secondary" />
|
|
726
|
+
<div>
|
|
727
|
+
<p className="text-sm text-text-secondary">Delivery updates</p>
|
|
728
|
+
<p className="font-medium" data-testid="customer-delivery-updates">
|
|
729
|
+
{deliveryUpdatesSummary(customer)}
|
|
730
|
+
</p>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
694
733
|
</div>
|
|
695
734
|
</div>
|
|
696
735
|
)}
|