@burdenoff/microfe-movethewheels 2026.1101.4 → 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.
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Customer delivery notifications — client side of wspace-movethewheels-svc's
3
+ * customer notification settings, history and previews (BOFF-4402).
4
+ *
5
+ * The service decides what to send and when; this module holds the GraphQL
6
+ * documents, the labels and the pure draft helpers the Settings page uses.
7
+ * Kept free of React so the contract can be unit-tested.
8
+ */
9
+
10
+ export type MtwCustomerNotificationEvent =
11
+ 'BOOKED' | 'PICKED_UP' | 'OUT_FOR_DELIVERY' | 'DELIVERED' | 'DELAYED' | 'FAILED';
12
+ export type MtwCustomerNotificationChannel = 'EMAIL' | 'SMS';
13
+ export type MtwCustomerNotificationStatus = 'PENDING' | 'SENT' | 'FAILED' | 'SKIPPED';
14
+
15
+ export interface MtwCustomerNotificationSettings {
16
+ workspaceId: string;
17
+ enabled: boolean;
18
+ enabledAt: string | null;
19
+ emailEnabled: boolean;
20
+ smsEnabled: boolean;
21
+ events: MtwCustomerNotificationEvent[];
22
+ isCustomized: boolean;
23
+ updatedAt: string | null;
24
+ updatedBy: string | null;
25
+ }
26
+
27
+ export type MtwCustomerNotificationSettingsInput = Partial<
28
+ Pick<MtwCustomerNotificationSettings, 'enabled' | 'emailEnabled' | 'smsEnabled' | 'events'>
29
+ >;
30
+
31
+ export interface MtwCustomerNotification {
32
+ id: string;
33
+ shipmentId: string;
34
+ shipmentNumber: string | null;
35
+ customerId: string | null;
36
+ event: MtwCustomerNotificationEvent;
37
+ channel: MtwCustomerNotificationChannel;
38
+ status: MtwCustomerNotificationStatus;
39
+ recipient: string | null;
40
+ subject: string | null;
41
+ attempts: number;
42
+ lastError: string | null;
43
+ sentAt: string | null;
44
+ createdAt: string;
45
+ }
46
+
47
+ export interface MtwCustomerNotificationPreview {
48
+ event: MtwCustomerNotificationEvent;
49
+ subject: string;
50
+ emailText: string;
51
+ smsText: string;
52
+ }
53
+
54
+ /** Every event, in the order a shipment reaches them. */
55
+ export const CUSTOMER_NOTIFICATION_EVENTS: ReadonlyArray<{
56
+ value: MtwCustomerNotificationEvent;
57
+ label: string;
58
+ description: string;
59
+ }> = [
60
+ { value: 'BOOKED', label: 'Booked', description: 'A shipment is created for the customer' },
61
+ { value: 'PICKED_UP', label: 'Picked up', description: 'The shipment is collected' },
62
+ {
63
+ value: 'OUT_FOR_DELIVERY',
64
+ label: 'Out for delivery',
65
+ description: 'Tracking says the shipment is on its last leg',
66
+ },
67
+ { value: 'DELIVERED', label: 'Delivered', description: 'The shipment is delivered' },
68
+ {
69
+ value: 'DELAYED',
70
+ label: 'Running late',
71
+ description: 'The risk monitor expects it to miss the promised delivery (once per promise)',
72
+ },
73
+ { value: 'FAILED', label: 'Delivery failed', description: 'Delivery could not be completed' },
74
+ ];
75
+
76
+ export function eventLabel(event: MtwCustomerNotificationEvent): string {
77
+ return CUSTOMER_NOTIFICATION_EVENTS.find((option) => option.value === event)?.label ?? event;
78
+ }
79
+
80
+ export const STATUS_LABELS: Record<MtwCustomerNotificationStatus, string> = {
81
+ PENDING: 'Sending',
82
+ SENT: 'Sent',
83
+ FAILED: 'Failed',
84
+ SKIPPED: 'Not sent',
85
+ };
86
+
87
+ /** Off, email only, every event — what a workspace sees until it saves. */
88
+ export function defaultNotificationSettings(workspaceId = ''): MtwCustomerNotificationSettings {
89
+ return {
90
+ workspaceId,
91
+ enabled: false,
92
+ enabledAt: null,
93
+ emailEnabled: true,
94
+ smsEnabled: false,
95
+ events: CUSTOMER_NOTIFICATION_EVENTS.map((option) => option.value),
96
+ isCustomized: false,
97
+ updatedAt: null,
98
+ updatedBy: null,
99
+ };
100
+ }
101
+
102
+ /** Events in canonical order without duplicates. */
103
+ export function orderedEvents(
104
+ events: readonly MtwCustomerNotificationEvent[]
105
+ ): MtwCustomerNotificationEvent[] {
106
+ const wanted = new Set(events);
107
+ return CUSTOMER_NOTIFICATION_EVENTS.map((option) => option.value).filter((event) =>
108
+ wanted.has(event)
109
+ );
110
+ }
111
+
112
+ /** Add or remove one event from a selection. */
113
+ export function toggleEvent(
114
+ events: readonly MtwCustomerNotificationEvent[],
115
+ event: MtwCustomerNotificationEvent,
116
+ on: boolean
117
+ ): MtwCustomerNotificationEvent[] {
118
+ const next = new Set(events);
119
+ if (on) next.add(event);
120
+ else next.delete(event);
121
+ return orderedEvents([...next]);
122
+ }
123
+
124
+ /** Only the fields the draft changed, ready for the update mutation. */
125
+ export function changedNotificationSettings(
126
+ saved: MtwCustomerNotificationSettings,
127
+ draft: Pick<MtwCustomerNotificationSettings, 'enabled' | 'emailEnabled' | 'smsEnabled' | 'events'>
128
+ ): MtwCustomerNotificationSettingsInput {
129
+ const changes: MtwCustomerNotificationSettingsInput = {};
130
+ if (draft.enabled !== saved.enabled) changes.enabled = draft.enabled;
131
+ if (draft.emailEnabled !== saved.emailEnabled) changes.emailEnabled = draft.emailEnabled;
132
+ if (draft.smsEnabled !== saved.smsEnabled) changes.smsEnabled = draft.smsEnabled;
133
+ const draftEvents = orderedEvents(draft.events);
134
+ const savedEvents = orderedEvents(saved.events);
135
+ if (draftEvents.join(',') !== savedEvents.join(',')) changes.events = draftEvents;
136
+ return changes;
137
+ }
138
+
139
+ /** Why the draft cannot be saved, or null. Switching on needs a channel and an event. */
140
+ export function notificationDraftError(
141
+ draft: Pick<MtwCustomerNotificationSettings, 'enabled' | 'emailEnabled' | 'smsEnabled' | 'events'>
142
+ ): string | null {
143
+ if (!draft.enabled) return null;
144
+ if (!draft.emailEnabled && !draft.smsEnabled) return 'Choose email, SMS or both.';
145
+ if (draft.events.length === 0) return 'Choose at least one event to notify customers about.';
146
+ return null;
147
+ }
148
+
149
+ const SETTINGS_FIELDS = `
150
+ workspaceId
151
+ enabled
152
+ enabledAt
153
+ emailEnabled
154
+ smsEnabled
155
+ events
156
+ isCustomized
157
+ updatedAt
158
+ updatedBy
159
+ `;
160
+
161
+ export const MTW_CUSTOMER_NOTIFICATION_SETTINGS_QUERY = `
162
+ query MtwCustomerNotificationSettings {
163
+ mtwCustomerNotificationSettings {${SETTINGS_FIELDS}}
164
+ }
165
+ `;
166
+
167
+ export const UPDATE_MTW_CUSTOMER_NOTIFICATION_SETTINGS_MUTATION = `
168
+ mutation UpdateMtwCustomerNotificationSettings($input: UpdateMtwCustomerNotificationSettingsInput!) {
169
+ updateMtwCustomerNotificationSettings(input: $input) {${SETTINGS_FIELDS}}
170
+ }
171
+ `;
172
+
173
+ export const MTW_CUSTOMER_NOTIFICATIONS_QUERY = `
174
+ query MtwCustomerNotifications(
175
+ $filter: MtwCustomerNotificationFilterInput
176
+ $pagination: PaginationInput
177
+ ) {
178
+ mtwCustomerNotifications(filter: $filter, pagination: $pagination) {
179
+ total
180
+ hasMore
181
+ items {
182
+ id
183
+ shipmentId
184
+ shipmentNumber
185
+ customerId
186
+ event
187
+ channel
188
+ status
189
+ recipient
190
+ subject
191
+ attempts
192
+ lastError
193
+ sentAt
194
+ createdAt
195
+ }
196
+ }
197
+ }
198
+ `;
199
+
200
+ export const MTW_CUSTOMER_NOTIFICATION_PREVIEWS_QUERY = `
201
+ query MtwCustomerNotificationPreviews {
202
+ mtwCustomerNotificationPreviews {
203
+ event
204
+ subject
205
+ emailText
206
+ smsText
207
+ }
208
+ }
209
+ `;