@quanticjs/notification-ui 8.0.0

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/index.cjs ADDED
@@ -0,0 +1,4627 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ApiKeyManager: () => ApiKeyManager,
24
+ ApplicationRegistryPanel: () => ApplicationRegistryPanel,
25
+ BroadcastComposer: () => BroadcastComposer,
26
+ BroadcastList: () => BroadcastList,
27
+ BroadcastProgress: () => BroadcastProgress,
28
+ CatalogEditor: () => CatalogEditor,
29
+ DeliveryAnalyticsPage: () => DeliveryAnalyticsPage,
30
+ DeliveryLogExplorer: () => DeliveryLogExplorer,
31
+ DeliveryLogViewer: () => DeliveryLogViewer,
32
+ DlqConsole: () => DlqConsole,
33
+ FallbackReportPanel: () => FallbackReportPanel,
34
+ FrequencyCapTable: () => FrequencyCapTable,
35
+ FunnelStats: () => FunnelStats,
36
+ MissingTranslationsPanel: () => MissingTranslationsPanel,
37
+ NotificationBell: () => NotificationBell,
38
+ NotificationInbox: () => NotificationInbox,
39
+ NotificationPreferences: () => NotificationPreferences,
40
+ NotificationProvider: () => NotificationProvider,
41
+ NotificationRealtimeProvider: () => NotificationRealtimeProvider,
42
+ OperationsOverview: () => OperationsOverview,
43
+ QuietHoursForm: () => QuietHoursForm,
44
+ RecipientAdminPanel: () => RecipientAdminPanel,
45
+ SegmentBuilder: () => SegmentBuilder,
46
+ SegmentList: () => SegmentList,
47
+ SuppressionManager: () => SuppressionManager,
48
+ TemplateEditor: () => TemplateEditor,
49
+ TemplateList: () => TemplateList,
50
+ TemplatePreviewPane: () => TemplatePreviewPane,
51
+ TemplateStatusBadge: () => TemplateStatusBadge,
52
+ TemplateVersionHistory: () => TemplateVersionHistory,
53
+ TenantConfigForm: () => TenantConfigForm,
54
+ TrackingConfigForm: () => TrackingConfigForm,
55
+ TrendChart: () => TrendChart,
56
+ TypeTable: () => TypeTable,
57
+ WebhookEndpointManager: () => WebhookEndpointManager,
58
+ useBroadcasts: () => useBroadcasts,
59
+ useDeliveryAnalytics: () => useDeliveryAnalytics,
60
+ useDeliveryTypes: () => useDeliveryTypes,
61
+ useFunnelStats: () => useFunnelStats,
62
+ useNotificationConfig: () => useNotificationConfig,
63
+ useNotificationFeed: () => useNotificationFeed,
64
+ useRealtimeContext: () => useRealtimeContext,
65
+ useUnreadCount: () => useUnreadCount
66
+ });
67
+ module.exports = __toCommonJS(index_exports);
68
+
69
+ // src/notification-provider.tsx
70
+ var import_react2 = require("react");
71
+
72
+ // src/notification-realtime-provider.tsx
73
+ var import_react = require("react");
74
+ var import_react_query = require("@tanstack/react-query");
75
+ var import_socket = require("socket.io-client");
76
+ var import_jsx_runtime = require("react/jsx-runtime");
77
+ var RealtimeContext = (0, import_react.createContext)({ socket: null, status: "disabled" });
78
+ function NotificationRealtimeProvider({
79
+ serverUrl = "",
80
+ disabled = false,
81
+ appId,
82
+ children
83
+ }) {
84
+ const queryClient = (0, import_react_query.useQueryClient)();
85
+ const [socket, setSocket] = (0, import_react.useState)(null);
86
+ const [status, setStatus] = (0, import_react.useState)(disabled ? "disabled" : "disconnected");
87
+ const invalidateTimers = (0, import_react.useRef)(/* @__PURE__ */ new Map());
88
+ (0, import_react.useEffect)(() => {
89
+ if (disabled) {
90
+ setStatus("disabled");
91
+ return;
92
+ }
93
+ const debounceInvalidate = (key) => {
94
+ const cacheKey = JSON.stringify(key);
95
+ if (invalidateTimers.current.has(cacheKey)) return;
96
+ void queryClient.invalidateQueries({ queryKey: key });
97
+ const timer = setTimeout(() => invalidateTimers.current.delete(cacheKey), 200);
98
+ invalidateTimers.current.set(cacheKey, timer);
99
+ };
100
+ const s = (0, import_socket.io)(`${serverUrl}/realtime`, {
101
+ withCredentials: true,
102
+ transports: ["websocket"],
103
+ reconnectionDelay: 1e3,
104
+ reconnectionDelayMax: 3e4,
105
+ randomizationFactor: 0.5
106
+ });
107
+ setSocket(s);
108
+ s.on("connect", () => {
109
+ setStatus("connected");
110
+ void queryClient.invalidateQueries({ queryKey: ["notifications"] });
111
+ });
112
+ s.on("notification:new", (payload) => {
113
+ const eventAppId = payload?.appId ?? null;
114
+ if (appId && eventAppId && eventAppId !== appId) return;
115
+ debounceInvalidate(["notifications"]);
116
+ debounceInvalidate(["notifications", "unread-count"]);
117
+ });
118
+ s.on("unread-count:changed", () => {
119
+ debounceInvalidate(["notifications", "unread-count"]);
120
+ });
121
+ s.io.on("reconnect_attempt", () => setStatus("reconnecting"));
122
+ s.on("disconnect", (reason) => {
123
+ if (reason === "io server disconnect") {
124
+ s.io.opts.reconnection = false;
125
+ setStatus("disabled");
126
+ return;
127
+ }
128
+ setStatus("disconnected");
129
+ });
130
+ s.on("connect_error", () => setStatus("disconnected"));
131
+ const timers = invalidateTimers.current;
132
+ return () => {
133
+ for (const t of timers.values()) clearTimeout(t);
134
+ timers.clear();
135
+ s.disconnect();
136
+ setSocket(null);
137
+ setStatus("disconnected");
138
+ };
139
+ }, [disabled, serverUrl, appId]);
140
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RealtimeContext.Provider, { value: { socket, status }, children });
141
+ }
142
+ function useRealtimeContext() {
143
+ return (0, import_react.useContext)(RealtimeContext);
144
+ }
145
+
146
+ // src/notification-provider.tsx
147
+ var import_jsx_runtime2 = require("react/jsx-runtime");
148
+ var DEFAULT_CONFIG = {
149
+ appId: void 0,
150
+ basePath: "/notifications",
151
+ pollIntervalMs: 6e4
152
+ };
153
+ var NotificationConfigContext = (0, import_react2.createContext)(DEFAULT_CONFIG);
154
+ function useNotificationConfig() {
155
+ return (0, import_react2.useContext)(NotificationConfigContext);
156
+ }
157
+ function NotificationProvider({
158
+ appId,
159
+ basePath = "/notifications",
160
+ pollIntervalMs = 6e4,
161
+ serverUrl,
162
+ disabled,
163
+ children
164
+ }) {
165
+ const config = (0, import_react2.useMemo)(
166
+ () => ({ appId, basePath, pollIntervalMs }),
167
+ [appId, basePath, pollIntervalMs]
168
+ );
169
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NotificationConfigContext.Provider, { value: config, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NotificationRealtimeProvider, { appId, serverUrl, disabled, children }) });
170
+ }
171
+
172
+ // src/notification-preferences.tsx
173
+ var import_react3 = require("react");
174
+ var import_react_query2 = require("@quanticjs/react-query");
175
+ var import_react_ui = require("@quanticjs/react-ui");
176
+ var import_jsx_runtime3 = require("react/jsx-runtime");
177
+ function deriveLabel(type) {
178
+ const stripped = type.replace(/^wfl_/, "").replace(/_/g, " ");
179
+ return stripped.charAt(0).toUpperCase() + stripped.slice(1);
180
+ }
181
+ function toRows(prefs) {
182
+ const byType = /* @__PURE__ */ new Map();
183
+ for (const pref of prefs) {
184
+ const row = byType.get(pref.type) ?? {
185
+ type: pref.type,
186
+ inappEnabled: true,
187
+ emailEnabled: true,
188
+ digest: "none"
189
+ };
190
+ if (pref.channel === "inapp") {
191
+ row.inappEnabled = pref.enabled;
192
+ } else {
193
+ row.emailEnabled = pref.enabled;
194
+ row.digest = pref.digest;
195
+ }
196
+ byType.set(pref.type, row);
197
+ }
198
+ return [...byType.values()];
199
+ }
200
+ function toEntries(rows) {
201
+ return rows.flatMap((row) => [
202
+ { channel: "inapp", type: row.type, enabled: row.inappEnabled },
203
+ { channel: "email", type: row.type, enabled: row.emailEnabled, digest: row.digest }
204
+ ]);
205
+ }
206
+ function NotificationPreferences({
207
+ basePath = "/notifications",
208
+ typeLabels,
209
+ className
210
+ }) {
211
+ const toast = (0, import_react_ui.useToast)();
212
+ const { data, isLoading, isError, refetch } = (0, import_react_query2.useApiQuery)(
213
+ ["notifications", "preferences"],
214
+ (client) => client.get(`${basePath}/preferences`)
215
+ );
216
+ const serverRows = (0, import_react3.useMemo)(() => data ? toRows(data) : [], [data]);
217
+ const [rows, setRows] = (0, import_react3.useState)(serverRows);
218
+ (0, import_react3.useEffect)(() => setRows(serverRows), [serverRows]);
219
+ const mutation = (0, import_react_query2.useApiMutation)((client, entries) => client.put(`${basePath}/preferences`, { entries }), {
220
+ invalidates: [["notifications", "preferences"]],
221
+ onSuccess: () => {
222
+ toast.success("Notification preferences saved");
223
+ },
224
+ onError: (error) => {
225
+ toast.error(error.isServerError ? "Something went wrong" : error.title, {
226
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
227
+ });
228
+ }
229
+ });
230
+ const update = (type, patch) => {
231
+ setRows((prev) => prev.map((row) => row.type === type ? { ...row, ...patch } : row));
232
+ };
233
+ if (isLoading) {
234
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "status", "aria-label": "Loading notification preferences", className: (0, import_react_ui.cn)("space-y-2", className), children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "h-10 animate-pulse rounded-md bg-muted" }, i)) });
235
+ }
236
+ if (isError) {
237
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: (0, import_react_ui.cn)("rounded-md border border-border p-6 text-center", className), children: [
238
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-muted-foreground", children: "Failed to load notification preferences." }),
239
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
240
+ "button",
241
+ {
242
+ type: "button",
243
+ onClick: () => refetch(),
244
+ className: "mt-3 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground",
245
+ children: "Try again"
246
+ }
247
+ )
248
+ ] });
249
+ }
250
+ if (rows.length === 0) {
251
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: (0, import_react_ui.cn)("rounded-md border border-border p-6 text-center", className), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-muted-foreground", children: "No notification preferences available." }) });
252
+ }
253
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: (0, import_react_ui.cn)("space-y-4", className), children: [
254
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("table", { className: "w-full text-sm", children: [
255
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
256
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Notification" }),
257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "In-app" }),
258
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Email" }),
259
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Email digest" })
260
+ ] }) }),
261
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: rows.map((row) => {
262
+ const label = typeLabels?.[row.type] ?? deriveLabel(row.type);
263
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { className: "border-b border-border", children: [
264
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { className: "py-3 pe-4", children: label }),
265
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
266
+ "input",
267
+ {
268
+ type: "checkbox",
269
+ "aria-label": `${label} in-app notifications`,
270
+ checked: row.inappEnabled,
271
+ onChange: (e) => update(row.type, { inappEnabled: e.target.checked })
272
+ }
273
+ ) }),
274
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
275
+ "input",
276
+ {
277
+ type: "checkbox",
278
+ "aria-label": `${label} email notifications`,
279
+ checked: row.emailEnabled,
280
+ onChange: (e) => update(row.type, { emailEnabled: e.target.checked })
281
+ }
282
+ ) }),
283
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
284
+ "select",
285
+ {
286
+ "aria-label": `${label} email digest`,
287
+ value: row.digest,
288
+ disabled: !row.emailEnabled,
289
+ onChange: (e) => update(row.type, { digest: e.target.value }),
290
+ className: "rounded-md border border-border bg-background px-2 py-1",
291
+ children: [
292
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "none", children: "Immediately" }),
293
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "hourly", children: "Hourly digest" }),
294
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "daily", children: "Daily digest" })
295
+ ]
296
+ }
297
+ ) })
298
+ ] }, row.type);
299
+ }) })
300
+ ] }),
301
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
302
+ "button",
303
+ {
304
+ type: "button",
305
+ onClick: () => mutation.mutate(toEntries(rows)),
306
+ disabled: mutation.isPending,
307
+ className: "rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50",
308
+ children: mutation.isPending ? "Saving\u2026" : "Save preferences"
309
+ }
310
+ )
311
+ ] });
312
+ }
313
+
314
+ // src/use-unread-count.ts
315
+ var import_react_query3 = require("@quanticjs/react-query");
316
+ function useUnreadCount(options = {}) {
317
+ const config = useNotificationConfig();
318
+ const basePath = options.basePath ?? config.basePath;
319
+ const pollIntervalMs = options.pollIntervalMs ?? config.pollIntervalMs;
320
+ const appId = options.appId ?? config.appId;
321
+ const { status } = useRealtimeContext();
322
+ const socketUp = status === "connected";
323
+ const key = appId ? ["notifications", "unread-count", appId] : ["notifications", "unread-count"];
324
+ const url = appId ? `${basePath}/unread-count?appId=${encodeURIComponent(appId)}` : `${basePath}/unread-count`;
325
+ return (0, import_react_query3.useApiQuery)(key, (client) => client.get(url), {
326
+ refetchInterval: socketUp ? false : pollIntervalMs,
327
+ staleTime: socketUp ? Infinity : pollIntervalMs / 2
328
+ });
329
+ }
330
+
331
+ // src/use-notification-feed.ts
332
+ var import_react4 = require("react");
333
+ var import_react_query4 = require("@quanticjs/react-query");
334
+ var import_react_ui2 = require("@quanticjs/react-ui");
335
+ function useNotificationFeed(options = {}) {
336
+ const config = useNotificationConfig();
337
+ const basePath = options.basePath ?? config.basePath;
338
+ const pollIntervalMs = options.pollIntervalMs ?? config.pollIntervalMs;
339
+ const appId = options.appId ?? config.appId;
340
+ const limit = options.limit ?? 50;
341
+ const { socket, status } = useRealtimeContext();
342
+ const socketUp = status === "connected";
343
+ const toast = (0, import_react_ui2.useToast)();
344
+ const appParam = appId ? `&appId=${encodeURIComponent(appId)}` : "";
345
+ const readAllPath = appId ? `${basePath}/read-all?appId=${encodeURIComponent(appId)}` : `${basePath}/read-all`;
346
+ const query = (0, import_react_query4.useApiQuery)(
347
+ ["notifications", { limit, appId }],
348
+ (client) => client.get(`${basePath}?page=1&pageSize=${limit}${appParam}`),
349
+ {
350
+ refetchInterval: socketUp ? false : pollIntervalMs,
351
+ staleTime: socketUp ? Infinity : pollIntervalMs / 2
352
+ }
353
+ );
354
+ (0, import_react4.useEffect)(() => {
355
+ if (!socket) return;
356
+ const onConnect = () => {
357
+ const newest = query.data?.items?.[0]?.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString();
358
+ socket.emit("backfill:request", { since: newest });
359
+ };
360
+ socket.on("connect", onConnect);
361
+ return () => {
362
+ socket.off("connect", onConnect);
363
+ };
364
+ }, [socket, query.data]);
365
+ const markRead = (0, import_react_query4.useApiMutation)(
366
+ (client, id) => client.post(`${basePath}/${id}/read`, {}),
367
+ {
368
+ invalidates: [["notifications"], ["notifications", "unread-count"]],
369
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
370
+ description: `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
371
+ })
372
+ }
373
+ );
374
+ const markAllRead = (0, import_react_query4.useApiMutation)(
375
+ (client) => client.post(readAllPath, {}),
376
+ {
377
+ invalidates: [["notifications"], ["notifications", "unread-count"]],
378
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
379
+ description: `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
380
+ })
381
+ }
382
+ );
383
+ return { ...query, markRead, markAllRead };
384
+ }
385
+
386
+ // src/notification-bell.tsx
387
+ var import_react_ui3 = require("@quanticjs/react-ui");
388
+ var import_jsx_runtime4 = require("react/jsx-runtime");
389
+ function NotificationBell({
390
+ basePath,
391
+ pollIntervalMs,
392
+ appId,
393
+ onClick,
394
+ className
395
+ }) {
396
+ const { data } = useUnreadCount({ basePath, pollIntervalMs, appId });
397
+ const count = data?.count ?? 0;
398
+ const label = count === 0 ? "No unread notifications" : `${count} unread notifications`;
399
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
400
+ "button",
401
+ {
402
+ type: "button",
403
+ onClick,
404
+ "aria-label": "Notifications",
405
+ className: (0, import_react_ui3.cn)(
406
+ "relative inline-flex h-10 w-10 items-center justify-center rounded-full text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
407
+ className
408
+ ),
409
+ children: [
410
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
411
+ "svg",
412
+ {
413
+ "aria-hidden": "true",
414
+ viewBox: "0 0 24 24",
415
+ fill: "none",
416
+ stroke: "currentColor",
417
+ strokeWidth: "1.8",
418
+ className: "h-5 w-5",
419
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
420
+ "path",
421
+ {
422
+ strokeLinecap: "round",
423
+ strokeLinejoin: "round",
424
+ d: "M15 17h5l-1.4-1.4A2 2 0 0118 14.2V11a6 6 0 10-12 0v3.2a2 2 0 01-.6 1.4L4 17h5m6 0a3 3 0 11-6 0m6 0H9"
425
+ }
426
+ )
427
+ }
428
+ ),
429
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { role: "status", "aria-label": label, className: "sr-only", children: label }),
430
+ count > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
431
+ "span",
432
+ {
433
+ "aria-hidden": "true",
434
+ className: "absolute -end-0.5 -top-0.5 inline-flex min-w-5 items-center justify-center rounded-full bg-destructive px-1.5 text-xs font-semibold text-destructive-foreground",
435
+ children: count > 99 ? "99+" : count
436
+ }
437
+ )
438
+ ]
439
+ }
440
+ );
441
+ }
442
+
443
+ // src/notification-inbox.tsx
444
+ var import_react_ui4 = require("@quanticjs/react-ui");
445
+ var import_jsx_runtime5 = require("react/jsx-runtime");
446
+ function NotificationInbox({
447
+ basePath,
448
+ pollIntervalMs,
449
+ appId,
450
+ className
451
+ }) {
452
+ const { data, isLoading, isError, refetch, markRead, markAllRead } = useNotificationFeed({
453
+ basePath,
454
+ pollIntervalMs,
455
+ appId
456
+ });
457
+ if (isLoading) {
458
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
459
+ "div",
460
+ {
461
+ role: "status",
462
+ "aria-label": "Loading notifications",
463
+ className: (0, import_react_ui4.cn)("flex flex-col gap-2 p-4", className),
464
+ children: [
465
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "sr-only", children: "Loading notifications" }),
466
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "aria-hidden": "true", className: "h-12 animate-pulse rounded bg-muted" }, i))
467
+ ]
468
+ }
469
+ );
470
+ }
471
+ if (isError) {
472
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: (0, import_react_ui4.cn)("flex flex-col items-start gap-3 p-4", className), children: [
473
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load notifications" }),
474
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
475
+ "button",
476
+ {
477
+ type: "button",
478
+ onClick: () => void refetch(),
479
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
480
+ children: "Try again"
481
+ }
482
+ )
483
+ ] });
484
+ }
485
+ const items = data?.items ?? [];
486
+ if (items.length === 0) {
487
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: (0, import_react_ui4.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No notifications yet" });
488
+ }
489
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("section", { "aria-label": "Notifications", className: (0, import_react_ui4.cn)("flex flex-col", className), children: [
490
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("header", { className: "flex items-center justify-between border-b border-border px-4 py-2", children: [
491
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: "Notifications" }),
492
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
493
+ "button",
494
+ {
495
+ type: "button",
496
+ onClick: () => markAllRead.mutate(),
497
+ disabled: markAllRead.isPending,
498
+ className: "text-sm text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
499
+ children: "Mark all read"
500
+ }
501
+ )
502
+ ] }),
503
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { className: "divide-y divide-border", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InboxRow, { item, onMarkRead: () => markRead.mutate(item.id) }, item.id)) })
504
+ ] });
505
+ }
506
+ function InboxRow({
507
+ item,
508
+ onMarkRead
509
+ }) {
510
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
511
+ "button",
512
+ {
513
+ type: "button",
514
+ onClick: onMarkRead,
515
+ "aria-label": `${item.title}${item.isRead ? "" : " (unread)"}`,
516
+ className: (0, import_react_ui4.cn)(
517
+ "flex w-full flex-col items-start gap-0.5 px-4 py-3 text-start hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
518
+ item.isRead ? "text-muted-foreground" : "text-foreground"
519
+ ),
520
+ children: [
521
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex items-center gap-2 text-sm font-medium", children: [
522
+ !item.isRead && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { "aria-hidden": "true", className: "h-2 w-2 rounded-full bg-primary" }),
523
+ item.title
524
+ ] }),
525
+ item.body && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-xs text-muted-foreground", children: item.body })
526
+ ]
527
+ }
528
+ ) });
529
+ }
530
+
531
+ // src/delivery-analytics-page.tsx
532
+ var import_react5 = require("react");
533
+ var import_react_ui8 = require("@quanticjs/react-ui");
534
+
535
+ // src/use-delivery-analytics.ts
536
+ var import_react_query5 = require("@quanticjs/react-query");
537
+ function toQuery(filters) {
538
+ const params = new URLSearchParams();
539
+ params.set("from", filters.from);
540
+ params.set("to", filters.to);
541
+ if (filters.channel) params.set("channel", filters.channel);
542
+ if (filters.type) params.set("type", filters.type);
543
+ if (filters.organizationId) params.set("organizationId", filters.organizationId);
544
+ return params.toString();
545
+ }
546
+ function useDeliveryAnalytics({
547
+ basePath = "/analytics/notifications",
548
+ ...filters
549
+ }) {
550
+ return (0, import_react_query5.useApiQuery)(
551
+ ["delivery-analytics", "summary", filters],
552
+ (client) => client.get(`${basePath}/summary?${toQuery(filters)}`)
553
+ );
554
+ }
555
+
556
+ // src/use-funnel-stats.ts
557
+ var import_react_query6 = require("@quanticjs/react-query");
558
+ function useFunnelStats({
559
+ basePath = "/analytics/notifications",
560
+ ...filters
561
+ }) {
562
+ return (0, import_react_query6.useApiQuery)(
563
+ ["delivery-analytics", "funnel", filters],
564
+ (client) => client.get(`${basePath}/funnel?${toQuery(filters)}`)
565
+ );
566
+ }
567
+
568
+ // src/use-delivery-types.ts
569
+ var import_react_query7 = require("@quanticjs/react-query");
570
+ function useDeliveryTypes({
571
+ basePath = "/analytics/notifications",
572
+ ...filters
573
+ }) {
574
+ const params = new URLSearchParams();
575
+ params.set("from", filters.from);
576
+ params.set("to", filters.to);
577
+ if (filters.organizationId) params.set("organizationId", filters.organizationId);
578
+ return (0, import_react_query7.useApiQuery)(
579
+ ["delivery-analytics", "types", filters],
580
+ (client) => client.get(`${basePath}/types?${params.toString()}`)
581
+ );
582
+ }
583
+
584
+ // src/funnel-stats.tsx
585
+ var import_react_ui5 = require("@quanticjs/react-ui");
586
+ var import_jsx_runtime6 = require("react/jsx-runtime");
587
+ var pct = (n) => `${(n * 100).toFixed(1)}%`;
588
+ function FunnelStats({ funnel, className }) {
589
+ const cards = [
590
+ { label: "Sends", value: String(funnel.sends) },
591
+ { label: "Delivered", value: String(funnel.delivered) },
592
+ { label: "Opened", value: String(funnel.opened) },
593
+ { label: "Clicked", value: String(funnel.clicked) },
594
+ { label: "Open rate", value: pct(funnel.openRate) },
595
+ { label: "Click rate", value: pct(funnel.clickRate) },
596
+ { label: "Bounce rate", value: pct(funnel.bounceRate) },
597
+ { label: "Delivery rate", value: pct(funnel.deliveryRate) }
598
+ ];
599
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: (0, import_react_ui5.cn)("grid grid-cols-2 gap-3 sm:grid-cols-4", className), "aria-label": "Delivery funnel", children: cards.map((c) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
600
+ "li",
601
+ {
602
+ className: "flex flex-col gap-1 rounded-lg border border-border bg-card p-4",
603
+ children: [
604
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-xs font-medium text-muted-foreground", children: c.label }),
605
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: c.value })
606
+ ]
607
+ },
608
+ c.label
609
+ )) });
610
+ }
611
+
612
+ // src/trend-chart.tsx
613
+ var import_react_ui6 = require("@quanticjs/react-ui");
614
+ var import_recharts = require("recharts");
615
+ var import_jsx_runtime7 = require("react/jsx-runtime");
616
+ function TrendChart({ data, className }) {
617
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: (0, import_react_ui6.cn)("h-72 w-full", className), role: "img", "aria-label": "Delivery trend over time", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_recharts.LineChart, { data, margin: { top: 8, right: 16, bottom: 8, left: 0 }, children: [
618
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.CartesianGrid, { strokeDasharray: "3 3", stroke: "hsl(var(--border))" }),
619
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.XAxis, { dataKey: "day", stroke: "hsl(var(--muted-foreground))", fontSize: 12 }),
620
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.YAxis, { stroke: "hsl(var(--muted-foreground))", fontSize: 12, allowDecimals: false }),
621
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.Tooltip, {}),
622
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.Line, { type: "monotone", dataKey: "sends", stroke: "hsl(var(--primary))", dot: false }),
623
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.Line, { type: "monotone", dataKey: "delivered", stroke: "hsl(var(--chart-2, var(--primary)))", dot: false }),
624
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.Line, { type: "monotone", dataKey: "opened", stroke: "hsl(var(--chart-3, var(--primary)))", dot: false }),
625
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_recharts.Line, { type: "monotone", dataKey: "clicked", stroke: "hsl(var(--chart-4, var(--primary)))", dot: false })
626
+ ] }) }) });
627
+ }
628
+
629
+ // src/type-table.tsx
630
+ var import_react_ui7 = require("@quanticjs/react-ui");
631
+ var import_jsx_runtime8 = require("react/jsx-runtime");
632
+ var pct2 = (n) => `${(n * 100).toFixed(1)}%`;
633
+ function TypeTable({ rows, className }) {
634
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("table", { className: (0, import_react_ui7.cn)("w-full border-collapse text-sm", className), children: [
635
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("caption", { className: "sr-only", children: "Delivery breakdown by notification type" }),
636
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
637
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Type" }),
638
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Sends" }),
639
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Delivered" }),
640
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Open rate" }),
641
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("th", { scope: "col", className: "py-2 font-medium", children: "Click rate" })
642
+ ] }) }),
643
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("tbody", { children: rows.map((r) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("tr", { className: "border-b border-border/50", children: [
644
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "py-2 pe-4 font-medium text-foreground", children: r.type }),
645
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "py-2 pe-4 text-foreground", children: r.sends }),
646
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "py-2 pe-4 text-foreground", children: r.delivered }),
647
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "py-2 pe-4 text-foreground", children: pct2(r.openRate) }),
648
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("td", { className: "py-2 text-foreground", children: pct2(r.clickRate) })
649
+ ] }, r.type)) })
650
+ ] });
651
+ }
652
+
653
+ // src/delivery-analytics-page.tsx
654
+ var import_jsx_runtime9 = require("react/jsx-runtime");
655
+ var DEFAULT_FROM = "2026-06-01";
656
+ var DEFAULT_TO = "2026-06-30";
657
+ var CHANNELS = ["", "email", "inapp", "push", "sms"];
658
+ function DeliveryAnalyticsPage({
659
+ basePath = "/analytics/notifications",
660
+ organizationId,
661
+ className
662
+ }) {
663
+ const [channel, setChannel] = (0, import_react5.useState)("");
664
+ const [from, setFrom] = (0, import_react5.useState)(DEFAULT_FROM);
665
+ const [to, setTo] = (0, import_react5.useState)(DEFAULT_TO);
666
+ const filters = { from, to, channel: channel || void 0, organizationId, basePath };
667
+ const summary = useDeliveryAnalytics(filters);
668
+ const funnel = useFunnelStats(filters);
669
+ const types = useDeliveryTypes({ from, to, organizationId, basePath });
670
+ const isLoading = summary.isLoading || funnel.isLoading || types.isLoading;
671
+ const isError = summary.isError || funnel.isError || types.isError;
672
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("main", { className: (0, import_react_ui8.cn)("flex flex-col gap-6 p-4 sm:p-6", className), children: [
673
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("header", { className: "flex flex-col gap-1", children: [
674
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h1", { className: "text-xl font-semibold text-foreground", children: "Delivery Analytics" }),
675
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-sm text-muted-foreground", children: "Cross-channel send, open, and click performance." })
676
+ ] }),
677
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-wrap items-end gap-3", children: [
678
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex flex-col gap-1 text-sm", children: [
679
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "font-medium text-foreground", children: "Channel" }),
680
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
681
+ "select",
682
+ {
683
+ value: channel,
684
+ onChange: (e) => setChannel(e.target.value),
685
+ className: "rounded border border-border bg-background px-2 py-1.5 text-foreground",
686
+ children: CHANNELS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: c, children: c === "" ? "All channels" : c }, c || "all"))
687
+ }
688
+ )
689
+ ] }),
690
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex flex-col gap-1 text-sm", children: [
691
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "font-medium text-foreground", children: "From" }),
692
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
693
+ "input",
694
+ {
695
+ type: "date",
696
+ value: from,
697
+ onChange: (e) => setFrom(e.target.value),
698
+ className: "rounded border border-border bg-background px-2 py-1.5 text-foreground"
699
+ }
700
+ )
701
+ ] }),
702
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex flex-col gap-1 text-sm", children: [
703
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "font-medium text-foreground", children: "To" }),
704
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
705
+ "input",
706
+ {
707
+ type: "date",
708
+ value: to,
709
+ onChange: (e) => setTo(e.target.value),
710
+ className: "rounded border border-border bg-background px-2 py-1.5 text-foreground"
711
+ }
712
+ )
713
+ ] })
714
+ ] }),
715
+ isLoading ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { role: "status", "aria-label": "Loading delivery analytics", className: "flex flex-col gap-3", children: [
716
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "sr-only", children: "Loading delivery analytics" }),
717
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { "aria-hidden": "true", className: "h-24 animate-pulse rounded bg-muted" }),
718
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { "aria-hidden": "true", className: "h-72 animate-pulse rounded bg-muted" })
719
+ ] }) : isError ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col items-start gap-3", children: [
720
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load delivery analytics" }),
721
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
722
+ "button",
723
+ {
724
+ type: "button",
725
+ onClick: () => {
726
+ void summary.refetch();
727
+ void funnel.refetch();
728
+ void types.refetch();
729
+ },
730
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
731
+ children: "Try again"
732
+ }
733
+ )
734
+ ] }) : (summary.data?.length ?? 0) === 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "rounded border border-border p-6 text-center text-sm text-muted-foreground", children: "No delivery data for the selected range" }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col gap-6", children: [
735
+ funnel.data ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FunnelStats, { funnel: funnel.data }) : null,
736
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Delivery trend", className: "rounded-lg border border-border p-4", children: [
737
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: "Trend" }),
738
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(TrendChart, { data: summary.data ?? [] })
739
+ ] }),
740
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { "aria-label": "Delivery by type", className: "rounded-lg border border-border p-4", children: [
741
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: "By type" }),
742
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(TypeTable, { rows: types.data ?? [] })
743
+ ] })
744
+ ] })
745
+ ] });
746
+ }
747
+
748
+ // src/template-status-badge.tsx
749
+ var import_react_ui9 = require("@quanticjs/react-ui");
750
+ var import_jsx_runtime10 = require("react/jsx-runtime");
751
+ var STATUS_STYLES = {
752
+ draft: "bg-muted text-muted-foreground",
753
+ published: "bg-primary text-primary-foreground",
754
+ archived: "bg-secondary text-secondary-foreground"
755
+ };
756
+ function TemplateStatusBadge({
757
+ status,
758
+ className
759
+ }) {
760
+ const style = STATUS_STYLES[status] ?? "bg-muted text-muted-foreground";
761
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
762
+ "span",
763
+ {
764
+ className: (0, import_react_ui9.cn)(
765
+ "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium capitalize",
766
+ style,
767
+ className
768
+ ),
769
+ children: status
770
+ }
771
+ );
772
+ }
773
+
774
+ // src/template-list.tsx
775
+ var import_react_ui10 = require("@quanticjs/react-ui");
776
+ var import_react_query8 = require("@quanticjs/react-query");
777
+ var import_jsx_runtime11 = require("react/jsx-runtime");
778
+ function TemplateList({ basePath = "/api/templates", className }) {
779
+ const { data, isLoading, isError, refetch } = (0, import_react_query8.useApiQuery)(
780
+ ["templates", basePath],
781
+ (client) => client.get(basePath)
782
+ );
783
+ if (isLoading) {
784
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
785
+ "div",
786
+ {
787
+ role: "status",
788
+ "aria-label": "Loading templates",
789
+ className: (0, import_react_ui10.cn)("flex flex-col gap-2 p-4", className),
790
+ children: [
791
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "sr-only", children: "Loading templates" }),
792
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "aria-hidden": "true", className: "h-12 animate-pulse rounded bg-muted" }, i))
793
+ ]
794
+ }
795
+ );
796
+ }
797
+ if (isError) {
798
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: (0, import_react_ui10.cn)("flex flex-col items-start gap-3 p-4", className), children: [
799
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load templates" }),
800
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
801
+ "button",
802
+ {
803
+ type: "button",
804
+ onClick: () => void refetch(),
805
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
806
+ children: "Try again"
807
+ }
808
+ )
809
+ ] });
810
+ }
811
+ const items = data ?? [];
812
+ if (items.length === 0) {
813
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: (0, import_react_ui10.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No templates found" });
814
+ }
815
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("section", { "aria-label": "Templates", className: (0, import_react_ui10.cn)("flex flex-col", className), children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("table", { className: "w-full text-sm", children: [
816
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
817
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Template" }),
818
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
819
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Locales" }),
820
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Versions" })
821
+ ] }) }),
822
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("tbody", { children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("tr", { className: "border-b border-border", children: [
823
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { className: "py-3 pe-4 font-medium text-foreground", children: item.templateId }),
824
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(TemplateStatusBadge, { status: item.latestStatus }) }),
825
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: item.locales.join(", ") }),
826
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: item.versionCount })
827
+ ] }, item.templateId)) })
828
+ ] }) });
829
+ }
830
+
831
+ // src/template-editor.tsx
832
+ var import_react6 = require("react");
833
+ var import_react_ui11 = require("@quanticjs/react-ui");
834
+ var import_react_query9 = require("@quanticjs/react-query");
835
+ var import_jsx_runtime12 = require("react/jsx-runtime");
836
+ var FIELD_DEFS = [
837
+ { key: "subject", label: "Subject", multiline: false },
838
+ { key: "heading", label: "Heading", multiline: false },
839
+ { key: "body", label: "Body", multiline: true },
840
+ { key: "cta", label: "CTA label", multiline: false },
841
+ { key: "html", label: "HTML", multiline: true }
842
+ ];
843
+ function bracesBalanced(value) {
844
+ const open = (value.match(/\{\{/g) ?? []).length;
845
+ const close = (value.match(/\}\}/g) ?? []).length;
846
+ return open === close;
847
+ }
848
+ function TemplateEditor({
849
+ templateId,
850
+ versionId,
851
+ initialFields,
852
+ initialStatus = "draft",
853
+ basePath = "/api/templates",
854
+ className
855
+ }) {
856
+ const toast = (0, import_react_ui11.useToast)();
857
+ const [fields, setFields] = (0, import_react6.useState)({
858
+ subject: initialFields?.subject ?? "",
859
+ heading: initialFields?.heading ?? "",
860
+ body: initialFields?.body ?? "",
861
+ cta: initialFields?.cta ?? "",
862
+ html: initialFields?.html ?? ""
863
+ });
864
+ const [errors, setErrors] = (0, import_react6.useState)({});
865
+ const [status, setStatus] = (0, import_react6.useState)(initialStatus);
866
+ const [activeVersionId, setActiveVersionId] = (0, import_react6.useState)(versionId);
867
+ const save = (0, import_react_query9.useApiMutation)(
868
+ (client, payload) => client.put(`${basePath}/${templateId}`, payload),
869
+ {
870
+ invalidates: [["templates"]],
871
+ onSuccess: (result) => {
872
+ setStatus(result.status);
873
+ setActiveVersionId(result.id);
874
+ toast.success("Draft saved");
875
+ },
876
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
877
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
878
+ })
879
+ }
880
+ );
881
+ const publish = (0, import_react_query9.useApiMutation)(
882
+ (client, vid) => client.post(`${basePath}/${templateId}/versions/${vid}/publish`, {}),
883
+ {
884
+ invalidates: [["templates"]],
885
+ onSuccess: () => {
886
+ setStatus("published");
887
+ toast.success("Version published");
888
+ },
889
+ onError: (error) => toast.error(
890
+ error.isConflict ? "Version already published" : error.isServerError ? "Something went wrong" : error.title,
891
+ { description: `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})` }
892
+ )
893
+ }
894
+ );
895
+ const validate = () => {
896
+ const next = {};
897
+ for (const { key } of FIELD_DEFS) {
898
+ if (!bracesBalanced(fields[key])) {
899
+ next[key] = "Unbalanced Handlebars braces \u2014 every {{ needs a matching }}";
900
+ }
901
+ }
902
+ setErrors(next);
903
+ return Object.keys(next).length === 0;
904
+ };
905
+ const onSubmit = (event) => {
906
+ event.preventDefault();
907
+ if (!validate()) return;
908
+ save.mutate(fields);
909
+ };
910
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("form", { onSubmit, className: (0, import_react_ui11.cn)("flex flex-col gap-4", className), noValidate: true, children: [
911
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("header", { className: "flex items-center justify-between", children: [
912
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("h2", { className: "text-sm font-semibold text-foreground", children: [
913
+ "Edit template: ",
914
+ templateId
915
+ ] }),
916
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TemplateStatusBadge, { status })
917
+ ] }),
918
+ FIELD_DEFS.map(({ key, label, multiline }) => {
919
+ const fieldId = `template-${key}`;
920
+ const errorId = `${fieldId}-error`;
921
+ const error = errors[key];
922
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex flex-col gap-1", children: [
923
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("label", { htmlFor: fieldId, className: "text-sm font-medium text-foreground", children: label }),
924
+ multiline ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
925
+ "textarea",
926
+ {
927
+ id: fieldId,
928
+ value: fields[key],
929
+ rows: 4,
930
+ "aria-invalid": error ? "true" : void 0,
931
+ "aria-describedby": error ? errorId : void 0,
932
+ onChange: (e) => setFields((prev) => ({ ...prev, [key]: e.target.value })),
933
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
934
+ }
935
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
936
+ "input",
937
+ {
938
+ id: fieldId,
939
+ type: "text",
940
+ value: fields[key],
941
+ "aria-invalid": error ? "true" : void 0,
942
+ "aria-describedby": error ? errorId : void 0,
943
+ onChange: (e) => setFields((prev) => ({ ...prev, [key]: e.target.value })),
944
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
945
+ }
946
+ ),
947
+ error && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { id: errorId, className: "text-xs text-destructive", children: error })
948
+ ] }, key);
949
+ }),
950
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex items-center gap-3", children: [
951
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
952
+ "button",
953
+ {
954
+ type: "submit",
955
+ disabled: save.isPending,
956
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
957
+ children: save.isPending ? "Saving\u2026" : "Save draft"
958
+ }
959
+ ),
960
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
961
+ "button",
962
+ {
963
+ type: "button",
964
+ disabled: !activeVersionId || publish.isPending,
965
+ onClick: () => activeVersionId && publish.mutate(activeVersionId),
966
+ className: "rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
967
+ children: publish.isPending ? "Publishing\u2026" : "Publish"
968
+ }
969
+ )
970
+ ] })
971
+ ] });
972
+ }
973
+
974
+ // src/template-preview-pane.tsx
975
+ var import_react7 = require("react");
976
+ var import_react_ui12 = require("@quanticjs/react-ui");
977
+ var import_react_query10 = require("@quanticjs/react-query");
978
+ var import_jsx_runtime13 = require("react/jsx-runtime");
979
+ function TemplatePreviewPane({
980
+ templateId,
981
+ versionId,
982
+ vars = {},
983
+ basePath = "/api/templates",
984
+ className
985
+ }) {
986
+ const preview = (0, import_react_query10.useApiMutation)(
987
+ (client, payload) => client.post(`${basePath}/${templateId}/preview`, payload)
988
+ );
989
+ const { mutate, data, isPending, isError } = preview;
990
+ (0, import_react7.useEffect)(() => {
991
+ mutate({ versionId, vars });
992
+ }, [templateId, versionId]);
993
+ if (isPending) {
994
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
995
+ "div",
996
+ {
997
+ role: "status",
998
+ "aria-label": "Loading preview",
999
+ className: (0, import_react_ui12.cn)("flex flex-col gap-2 p-4", className),
1000
+ children: [
1001
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "sr-only", children: "Loading preview" }),
1002
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "aria-hidden": "true", className: "h-6 w-1/2 animate-pulse rounded bg-muted" }),
1003
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "aria-hidden": "true", className: "h-40 animate-pulse rounded bg-muted" })
1004
+ ]
1005
+ }
1006
+ );
1007
+ }
1008
+ if (isError || !data) {
1009
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: (0, import_react_ui12.cn)("flex flex-col items-start gap-3 p-4", className), children: [
1010
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load preview" }),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1012
+ "button",
1013
+ {
1014
+ type: "button",
1015
+ onClick: () => mutate({ versionId, vars }),
1016
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1017
+ children: "Try again"
1018
+ }
1019
+ )
1020
+ ] });
1021
+ }
1022
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("section", { "aria-label": "Template preview", className: (0, import_react_ui12.cn)("flex flex-col gap-4", className), children: [
1023
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: data.subject }),
1024
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1025
+ "iframe",
1026
+ {
1027
+ title: "Email preview",
1028
+ sandbox: "",
1029
+ srcDoc: data.html,
1030
+ className: "h-96 w-full rounded-md border border-border bg-background"
1031
+ }
1032
+ ),
1033
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex flex-col gap-1", children: [
1034
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("h3", { className: "text-xs font-medium text-muted-foreground", children: "Plain text" }),
1035
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("pre", { className: "overflow-auto whitespace-pre-wrap rounded-md border border-border bg-muted p-3 text-xs text-foreground", children: data.text })
1036
+ ] })
1037
+ ] });
1038
+ }
1039
+
1040
+ // src/template-version-history.tsx
1041
+ var import_react_ui13 = require("@quanticjs/react-ui");
1042
+ var import_react_query11 = require("@quanticjs/react-query");
1043
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1044
+ function TemplateVersionHistory({
1045
+ templateId,
1046
+ basePath = "/api/templates",
1047
+ className,
1048
+ canRollback = true
1049
+ }) {
1050
+ const toast = (0, import_react_ui13.useToast)();
1051
+ const versionsKey = ["templates", templateId, "versions"];
1052
+ const { data, isLoading, isError, refetch } = (0, import_react_query11.useApiQuery)(
1053
+ versionsKey,
1054
+ (client) => client.get(`${basePath}/${templateId}/versions`)
1055
+ );
1056
+ const rollback = (0, import_react_query11.useApiMutation)(
1057
+ (client, id) => client.post(`${basePath}/${templateId}/versions/${id}/rollback`, {}),
1058
+ {
1059
+ invalidates: [[...versionsKey], ["templates"]],
1060
+ onSuccess: () => toast.success("Version rolled back"),
1061
+ onError: (error) => toast.error(
1062
+ error.isConflict ? "Version cannot be rolled back" : error.isServerError ? "Something went wrong" : error.title,
1063
+ { description: `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})` }
1064
+ )
1065
+ }
1066
+ );
1067
+ if (isLoading) {
1068
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1069
+ "div",
1070
+ {
1071
+ role: "status",
1072
+ "aria-label": "Loading versions",
1073
+ className: (0, import_react_ui13.cn)("flex flex-col gap-2 p-4", className),
1074
+ children: [
1075
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "sr-only", children: "Loading versions" }),
1076
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
1077
+ ]
1078
+ }
1079
+ );
1080
+ }
1081
+ if (isError) {
1082
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: (0, import_react_ui13.cn)("flex flex-col items-start gap-3 p-4", className), children: [
1083
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load versions" }),
1084
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1085
+ "button",
1086
+ {
1087
+ type: "button",
1088
+ onClick: () => void refetch(),
1089
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1090
+ children: "Try again"
1091
+ }
1092
+ )
1093
+ ] });
1094
+ }
1095
+ const versions = data ?? [];
1096
+ if (versions.length === 0) {
1097
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: (0, import_react_ui13.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No versions" });
1098
+ }
1099
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("section", { "aria-label": "Version history", className: (0, import_react_ui13.cn)("flex flex-col", className), children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("table", { className: "w-full text-sm", children: [
1100
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
1101
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Version" }),
1102
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
1103
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Author" }),
1104
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Date" }),
1105
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "sr-only", children: "Actions" }) })
1106
+ ] }) }),
1107
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("tbody", { children: versions.map((version) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("tr", { className: "border-b border-border", children: [
1108
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("td", { className: "py-3 pe-4 font-medium text-foreground", children: [
1109
+ "v",
1110
+ version.versionNumber
1111
+ ] }),
1112
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(TemplateStatusBadge, { status: version.status }) }),
1113
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: version.createdBy }),
1114
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: version.createdAt }),
1115
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("td", { className: "px-4 py-3 text-end", children: canRollback && version.status === "archived" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1116
+ "button",
1117
+ {
1118
+ type: "button",
1119
+ disabled: rollback.isPending,
1120
+ onClick: () => rollback.mutate(version.id),
1121
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
1122
+ children: "Roll back"
1123
+ }
1124
+ ) })
1125
+ ] }, version.id)) })
1126
+ ] }) });
1127
+ }
1128
+
1129
+ // src/delivery-log-viewer.tsx
1130
+ var import_react8 = require("react");
1131
+ var import_react_ui14 = require("@quanticjs/react-ui");
1132
+ var import_react_query12 = require("@quanticjs/react-query");
1133
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1134
+ var LIMIT = 20;
1135
+ function DeliveryLogViewer({
1136
+ templateId,
1137
+ basePath = "/api/templates",
1138
+ className
1139
+ }) {
1140
+ const [page, setPage] = (0, import_react8.useState)(1);
1141
+ const { data, isLoading, isError, refetch } = (0, import_react_query12.useApiQuery)(
1142
+ ["templates", templateId, "delivery-logs", page],
1143
+ (client) => client.get(`${basePath}/${templateId}/delivery-logs?page=${page}&limit=${LIMIT}`)
1144
+ );
1145
+ if (isLoading) {
1146
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1147
+ "div",
1148
+ {
1149
+ role: "status",
1150
+ "aria-label": "Loading delivery logs",
1151
+ className: (0, import_react_ui14.cn)("flex flex-col gap-2 p-4", className),
1152
+ children: [
1153
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "sr-only", children: "Loading delivery logs" }),
1154
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
1155
+ ]
1156
+ }
1157
+ );
1158
+ }
1159
+ if (isError) {
1160
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: (0, import_react_ui14.cn)("flex flex-col items-start gap-3 p-4", className), children: [
1161
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load delivery logs" }),
1162
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1163
+ "button",
1164
+ {
1165
+ type: "button",
1166
+ onClick: () => void refetch(),
1167
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1168
+ children: "Try again"
1169
+ }
1170
+ )
1171
+ ] });
1172
+ }
1173
+ const rows = data?.items ?? [];
1174
+ const totalPages = data?.totalPages ?? 1;
1175
+ if (rows.length === 0) {
1176
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: (0, import_react_ui14.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No delivery logs" });
1177
+ }
1178
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("section", { "aria-label": "Delivery logs", className: (0, import_react_ui14.cn)("flex flex-col gap-3", className), children: [
1179
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("table", { className: "w-full text-sm", children: [
1180
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
1181
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Recipient" }),
1182
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
1183
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Attempts" }),
1184
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Last error" })
1185
+ ] }) }),
1186
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("tr", { className: "border-b border-border", children: [
1187
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("td", { className: "py-3 pe-4 text-foreground", children: row.recipientEmail }),
1188
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(TemplateStatusBadge, { status: row.status }) }),
1189
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.attempts }),
1190
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.lastError ?? "\u2014" })
1191
+ ] }, row.id)) })
1192
+ ] }),
1193
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("nav", { "aria-label": "Delivery log pagination", className: "flex items-center justify-between", children: [
1194
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1195
+ "button",
1196
+ {
1197
+ type: "button",
1198
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
1199
+ disabled: page <= 1,
1200
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
1201
+ children: "Previous"
1202
+ }
1203
+ ),
1204
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-xs text-muted-foreground", children: [
1205
+ "Page ",
1206
+ page,
1207
+ " of ",
1208
+ totalPages
1209
+ ] }),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1211
+ "button",
1212
+ {
1213
+ type: "button",
1214
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
1215
+ disabled: page >= totalPages,
1216
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
1217
+ children: "Next"
1218
+ }
1219
+ )
1220
+ ] })
1221
+ ] });
1222
+ }
1223
+
1224
+ // src/use-broadcasts.ts
1225
+ var import_react_query13 = require("@quanticjs/react-query");
1226
+ var LIMIT2 = 20;
1227
+ function useBroadcasts({ page = 1, status, basePath = "/api" } = {}) {
1228
+ const statusParam = status ? `&status=${encodeURIComponent(status)}` : "";
1229
+ return (0, import_react_query13.useApiQuery)(
1230
+ ["broadcasts", { page, status }],
1231
+ (client) => client.get(`${basePath}/v1/broadcasts?page=${page}&limit=${LIMIT2}${statusParam}`)
1232
+ );
1233
+ }
1234
+
1235
+ // src/broadcast-list.tsx
1236
+ var import_react9 = require("react");
1237
+ var import_react_ui15 = require("@quanticjs/react-ui");
1238
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1239
+ function statusVariant(status) {
1240
+ switch (status) {
1241
+ case "completed":
1242
+ return "success";
1243
+ case "completed_with_errors":
1244
+ return "warning";
1245
+ case "failed":
1246
+ return "destructive";
1247
+ case "cancelled":
1248
+ return "neutral";
1249
+ case "in_progress":
1250
+ case "running":
1251
+ case "pending":
1252
+ return "info";
1253
+ default:
1254
+ return "neutral";
1255
+ }
1256
+ }
1257
+ function BroadcastList({
1258
+ basePath = "/api",
1259
+ status,
1260
+ onSelect,
1261
+ className
1262
+ }) {
1263
+ const [page, setPage] = (0, import_react9.useState)(1);
1264
+ const { data, isLoading, isError, refetch } = useBroadcasts({ page, status, basePath });
1265
+ if (isLoading) {
1266
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1267
+ "div",
1268
+ {
1269
+ role: "status",
1270
+ "aria-label": "Loading broadcasts",
1271
+ className: (0, import_react_ui15.cn)("flex flex-col gap-2 p-4", className),
1272
+ children: [
1273
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "sr-only", children: "Loading broadcasts" }),
1274
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
1275
+ ]
1276
+ }
1277
+ );
1278
+ }
1279
+ if (isError) {
1280
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: (0, import_react_ui15.cn)("flex flex-col items-start gap-3 p-4", className), children: [
1281
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load broadcasts" }),
1282
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1283
+ "button",
1284
+ {
1285
+ type: "button",
1286
+ onClick: () => void refetch(),
1287
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1288
+ children: "Try again"
1289
+ }
1290
+ )
1291
+ ] });
1292
+ }
1293
+ const rows = data?.items ?? [];
1294
+ const totalPages = data?.totalPages ?? 1;
1295
+ if (rows.length === 0) {
1296
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: (0, import_react_ui15.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No broadcasts" });
1297
+ }
1298
+ const renderTemplateCell = (row) => onSelect ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1299
+ "button",
1300
+ {
1301
+ type: "button",
1302
+ onClick: () => onSelect(row.id),
1303
+ className: "rounded text-start font-medium text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1304
+ children: row.templateId
1305
+ }
1306
+ ) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-foreground", children: row.templateId });
1307
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { "aria-label": "Broadcasts", className: (0, import_react_ui15.cn)("flex flex-col gap-3", className), children: [
1308
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("table", { className: "w-full text-sm", children: [
1309
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
1310
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Template" }),
1311
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
1312
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Channels" }),
1313
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Sent / Failed / Skipped" }),
1314
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created by" }),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" })
1316
+ ] }) }),
1317
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("tr", { className: "border-b border-border", children: [
1318
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "py-3 pe-4", children: renderTemplateCell(row) }),
1319
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_ui15.StatusBadge, { variant: statusVariant(row.status), children: row.status }) }),
1320
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.channels.length > 0 ? row.channels.join(", ") : "\u2014" }),
1321
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("td", { className: "px-4 py-3 text-muted-foreground", children: [
1322
+ row.sentCount,
1323
+ " / ",
1324
+ row.failedCount,
1325
+ " / ",
1326
+ row.skippedCount
1327
+ ] }),
1328
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.createdBy }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui15.formatDateTime)(row.createdAt) })
1330
+ ] }, row.id)) })
1331
+ ] }),
1332
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("nav", { "aria-label": "Broadcast pagination", className: "flex items-center justify-between", children: [
1333
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1334
+ "button",
1335
+ {
1336
+ type: "button",
1337
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
1338
+ disabled: page <= 1,
1339
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
1340
+ children: "Previous"
1341
+ }
1342
+ ),
1343
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-xs text-muted-foreground", children: [
1344
+ "Page ",
1345
+ page,
1346
+ " of ",
1347
+ totalPages
1348
+ ] }),
1349
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1350
+ "button",
1351
+ {
1352
+ type: "button",
1353
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
1354
+ disabled: page >= totalPages,
1355
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
1356
+ children: "Next"
1357
+ }
1358
+ )
1359
+ ] })
1360
+ ] });
1361
+ }
1362
+
1363
+ // src/broadcast-progress.tsx
1364
+ var import_react_ui16 = require("@quanticjs/react-ui");
1365
+ var import_react_query14 = require("@quanticjs/react-query");
1366
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1367
+ var TERMINAL_STATUSES = ["completed", "completed_with_errors", "failed", "cancelled"];
1368
+ function isTerminalStatus(status) {
1369
+ return status !== void 0 && TERMINAL_STATUSES.includes(status);
1370
+ }
1371
+ function statusVariant2(status) {
1372
+ switch (status) {
1373
+ case "completed":
1374
+ return "success";
1375
+ case "completed_with_errors":
1376
+ return "warning";
1377
+ case "failed":
1378
+ return "destructive";
1379
+ case "cancelled":
1380
+ return "neutral";
1381
+ default:
1382
+ return "info";
1383
+ }
1384
+ }
1385
+ function BroadcastProgress({
1386
+ broadcastId,
1387
+ basePath = "/api",
1388
+ onCancelled,
1389
+ className
1390
+ }) {
1391
+ const toast = (0, import_react_ui16.useToast)();
1392
+ const { data, isLoading, isError, refetch } = (0, import_react_query14.useApiQuery)(
1393
+ ["broadcasts", broadcastId],
1394
+ (client) => client.get(`${basePath}/v1/broadcasts/${broadcastId}`),
1395
+ {
1396
+ refetchInterval: (query) => isTerminalStatus(query.state.data?.status) ? false : 5e3
1397
+ }
1398
+ );
1399
+ const cancel = (0, import_react_query14.useApiMutation)(
1400
+ (client) => client.delete(`${basePath}/v1/broadcasts/${broadcastId}`),
1401
+ {
1402
+ invalidates: [["broadcasts"]],
1403
+ onSuccess: () => {
1404
+ toast.success("Broadcast cancelled");
1405
+ onCancelled?.();
1406
+ },
1407
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
1408
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
1409
+ })
1410
+ }
1411
+ );
1412
+ if (isLoading) {
1413
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
1414
+ "div",
1415
+ {
1416
+ role: "status",
1417
+ "aria-label": "Loading broadcast",
1418
+ className: (0, import_react_ui16.cn)("flex flex-col gap-2 p-4", className),
1419
+ children: [
1420
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "sr-only", children: "Loading broadcast" }),
1421
+ [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
1422
+ ]
1423
+ }
1424
+ );
1425
+ }
1426
+ if (isError) {
1427
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: (0, import_react_ui16.cn)("flex flex-col items-start gap-3 p-4", className), children: [
1428
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load broadcast" }),
1429
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1430
+ "button",
1431
+ {
1432
+ type: "button",
1433
+ onClick: () => void refetch(),
1434
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1435
+ children: "Try again"
1436
+ }
1437
+ )
1438
+ ] });
1439
+ }
1440
+ if (!data) {
1441
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: (0, import_react_ui16.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No broadcast" });
1442
+ }
1443
+ const total = data.totalRecipients;
1444
+ const processed = data.sentCount + data.failedCount + data.skippedCount;
1445
+ const pct3 = total > 0 ? Math.min(100, Math.round(processed / total * 100)) : 0;
1446
+ const terminal = isTerminalStatus(data.status);
1447
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { "aria-label": "Broadcast progress", className: (0, import_react_ui16.cn)("flex flex-col gap-4 p-4", className), children: [
1448
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("header", { className: "flex items-center justify-between", children: [
1449
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("h2", { className: "text-sm font-semibold text-foreground", children: [
1450
+ "Broadcast ",
1451
+ data.templateId
1452
+ ] }),
1453
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_ui16.StatusBadge, { variant: statusVariant2(data.status), appearance: "solid", children: data.status })
1454
+ ] }),
1455
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1456
+ "div",
1457
+ {
1458
+ role: "progressbar",
1459
+ "aria-valuenow": pct3,
1460
+ "aria-valuemin": 0,
1461
+ "aria-valuemax": 100,
1462
+ "aria-label": "Broadcast completion",
1463
+ className: "h-2 w-full overflow-hidden rounded-full bg-muted",
1464
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "h-full rounded-full bg-primary", style: { width: `${pct3}%` } })
1465
+ }
1466
+ ),
1467
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
1468
+ processed,
1469
+ " of ",
1470
+ total,
1471
+ " processed (",
1472
+ pct3,
1473
+ "%)"
1474
+ ] }),
1475
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("dl", { className: "grid grid-cols-2 gap-3 text-sm sm:grid-cols-4", children: [
1476
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col rounded-md border border-border bg-card p-3", children: [
1477
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dt", { className: "text-xs text-muted-foreground", children: "Sent" }),
1478
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dd", { className: "text-foreground", children: data.sentCount })
1479
+ ] }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col rounded-md border border-border bg-card p-3", children: [
1481
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dt", { className: "text-xs text-muted-foreground", children: "Failed" }),
1482
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dd", { className: "text-foreground", children: data.failedCount })
1483
+ ] }),
1484
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col rounded-md border border-border bg-card p-3", children: [
1485
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dt", { className: "text-xs text-muted-foreground", children: "Skipped" }),
1486
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dd", { className: "text-foreground", children: data.skippedCount })
1487
+ ] }),
1488
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex flex-col rounded-md border border-border bg-card p-3", children: [
1489
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dt", { className: "text-xs text-muted-foreground", children: "Total" }),
1490
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dd", { className: "text-foreground", children: total })
1491
+ ] })
1492
+ ] }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1494
+ "button",
1495
+ {
1496
+ type: "button",
1497
+ disabled: terminal || cancel.isPending,
1498
+ onClick: () => cancel.mutate(),
1499
+ className: "rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
1500
+ children: cancel.isPending ? "Cancelling\u2026" : "Cancel"
1501
+ }
1502
+ ) })
1503
+ ] });
1504
+ }
1505
+
1506
+ // src/broadcast-composer.tsx
1507
+ var import_react10 = require("react");
1508
+ var import_react_ui17 = require("@quanticjs/react-ui");
1509
+ var import_react_query15 = require("@quanticjs/react-query");
1510
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1511
+ var CHANNELS2 = ["inapp", "email", "push", "sms", "webhook", "slack", "teams"];
1512
+ function normalizeList(data) {
1513
+ if (!data) return [];
1514
+ if (Array.isArray(data)) return data;
1515
+ return data.items ?? [];
1516
+ }
1517
+ function parseRecipients(raw) {
1518
+ return raw.split(/[\n,]/).map((r) => r.trim()).filter((r) => r.length > 0);
1519
+ }
1520
+ function BroadcastComposer({
1521
+ basePath = "/api",
1522
+ onCreated,
1523
+ className
1524
+ }) {
1525
+ const toast = (0, import_react_ui17.useToast)();
1526
+ const [idempotencyKey] = (0, import_react10.useState)(() => crypto.randomUUID());
1527
+ const [templateId, setTemplateId] = (0, import_react10.useState)("");
1528
+ const [channels, setChannels] = (0, import_react10.useState)([]);
1529
+ const [audienceMode, setAudienceMode] = (0, import_react10.useState)("segment");
1530
+ const [segmentId, setSegmentId] = (0, import_react10.useState)("");
1531
+ const [recipientsRaw, setRecipientsRaw] = (0, import_react10.useState)("");
1532
+ const [type, setType] = (0, import_react10.useState)("");
1533
+ const [errors, setErrors] = (0, import_react10.useState)({});
1534
+ const templatesQuery = (0, import_react_query15.useApiQuery)(
1535
+ ["templates"],
1536
+ (client) => client.get(`${basePath}/templates`)
1537
+ );
1538
+ const segmentsQuery = (0, import_react_query15.useApiQuery)(
1539
+ ["segments"],
1540
+ (client) => client.get(`${basePath}/v1/segments`)
1541
+ );
1542
+ const create = (0, import_react_query15.useApiMutation)(
1543
+ (client, payload) => client.post(`${basePath}/v1/broadcasts`, payload),
1544
+ {
1545
+ invalidates: [["broadcasts"]],
1546
+ onSuccess: (result) => {
1547
+ toast.success("Broadcast created");
1548
+ onCreated?.(result.id);
1549
+ },
1550
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
1551
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
1552
+ })
1553
+ }
1554
+ );
1555
+ const isLoading = templatesQuery.isLoading || segmentsQuery.isLoading;
1556
+ const isError = templatesQuery.isError || segmentsQuery.isError;
1557
+ if (isLoading) {
1558
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1559
+ "div",
1560
+ {
1561
+ role: "status",
1562
+ "aria-label": "Loading broadcast composer",
1563
+ className: (0, import_react_ui17.cn)("flex flex-col gap-2 p-4", className),
1564
+ children: [
1565
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "sr-only", children: "Loading broadcast composer" }),
1566
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
1567
+ ]
1568
+ }
1569
+ );
1570
+ }
1571
+ if (isError) {
1572
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: (0, import_react_ui17.cn)("flex flex-col items-start gap-3 p-4", className), children: [
1573
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load broadcast composer" }),
1574
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1575
+ "button",
1576
+ {
1577
+ type: "button",
1578
+ onClick: () => {
1579
+ void templatesQuery.refetch();
1580
+ void segmentsQuery.refetch();
1581
+ },
1582
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1583
+ children: "Try again"
1584
+ }
1585
+ )
1586
+ ] });
1587
+ }
1588
+ const templates = normalizeList(templatesQuery.data);
1589
+ const segments = normalizeList(segmentsQuery.data);
1590
+ const toggleChannel = (channel) => {
1591
+ setChannels(
1592
+ (prev) => prev.includes(channel) ? prev.filter((c) => c !== channel) : [...prev, channel]
1593
+ );
1594
+ };
1595
+ const validate = (recipientIds) => {
1596
+ const next = {};
1597
+ if (!templateId) next.templateId = "Select a template";
1598
+ if (channels.length === 0) next.channels = "Select at least one channel";
1599
+ if (audienceMode === "segment" && !segmentId) {
1600
+ next.audience = "Select a segment";
1601
+ }
1602
+ if (audienceMode === "recipients" && recipientIds.length === 0) {
1603
+ next.audience = "Add at least one recipient id";
1604
+ }
1605
+ setErrors(next);
1606
+ return Object.keys(next).length === 0;
1607
+ };
1608
+ const onSubmit = (event) => {
1609
+ event.preventDefault();
1610
+ const recipientIds = parseRecipients(recipientsRaw);
1611
+ if (!validate(recipientIds)) return;
1612
+ create.mutate({
1613
+ templateId,
1614
+ channels,
1615
+ type: type.trim() ? type.trim() : null,
1616
+ segmentId: audienceMode === "segment" ? segmentId : null,
1617
+ recipientIds: audienceMode === "recipients" ? recipientIds : null,
1618
+ idempotencyKey
1619
+ });
1620
+ };
1621
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("form", { onSubmit, className: (0, import_react_ui17.cn)("flex flex-col gap-4 p-4", className), noValidate: true, children: [
1622
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: "New broadcast" }),
1623
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col gap-1", children: [
1624
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { htmlFor: "composer-template", className: "text-sm font-medium text-foreground", children: "Template" }),
1625
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1626
+ "select",
1627
+ {
1628
+ id: "composer-template",
1629
+ value: templateId,
1630
+ "aria-invalid": errors.templateId ? "true" : void 0,
1631
+ "aria-describedby": errors.templateId ? "composer-template-error" : void 0,
1632
+ onChange: (e) => setTemplateId(e.target.value),
1633
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1634
+ children: [
1635
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: "", children: "Select a template\u2026" }),
1636
+ templates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: t.id, children: t.name ?? t.id }, t.id))
1637
+ ]
1638
+ }
1639
+ ),
1640
+ errors.templateId && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { id: "composer-template-error", className: "text-xs text-destructive", children: errors.templateId })
1641
+ ] }),
1642
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1643
+ "fieldset",
1644
+ {
1645
+ className: "flex flex-col gap-2",
1646
+ "aria-invalid": errors.channels ? "true" : void 0,
1647
+ "aria-describedby": errors.channels ? "composer-channels-error" : void 0,
1648
+ children: [
1649
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("legend", { className: "text-sm font-medium text-foreground", children: "Channels" }),
1650
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "flex flex-wrap gap-3", children: CHANNELS2.map((channel) => {
1651
+ const checkboxId = `composer-channel-${channel}`;
1652
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1653
+ "label",
1654
+ {
1655
+ htmlFor: checkboxId,
1656
+ className: "flex items-center gap-2 text-sm text-foreground",
1657
+ children: [
1658
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1659
+ "input",
1660
+ {
1661
+ id: checkboxId,
1662
+ type: "checkbox",
1663
+ checked: channels.includes(channel),
1664
+ onChange: () => toggleChannel(channel),
1665
+ className: "rounded border-border focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
1666
+ }
1667
+ ),
1668
+ channel
1669
+ ]
1670
+ },
1671
+ channel
1672
+ );
1673
+ }) }),
1674
+ errors.channels && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { id: "composer-channels-error", className: "text-xs text-destructive", children: errors.channels })
1675
+ ]
1676
+ }
1677
+ ),
1678
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("fieldset", { className: "flex flex-col gap-2", children: [
1679
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("legend", { className: "text-sm font-medium text-foreground", children: "Audience" }),
1680
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex gap-4", children: [
1681
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1682
+ "label",
1683
+ {
1684
+ htmlFor: "composer-audience-segment",
1685
+ className: "flex items-center gap-2 text-sm text-foreground",
1686
+ children: [
1687
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1688
+ "input",
1689
+ {
1690
+ id: "composer-audience-segment",
1691
+ type: "radio",
1692
+ name: "composer-audience",
1693
+ checked: audienceMode === "segment",
1694
+ onChange: () => setAudienceMode("segment"),
1695
+ className: "border-border focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
1696
+ }
1697
+ ),
1698
+ "Segment"
1699
+ ]
1700
+ }
1701
+ ),
1702
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1703
+ "label",
1704
+ {
1705
+ htmlFor: "composer-audience-recipients",
1706
+ className: "flex items-center gap-2 text-sm text-foreground",
1707
+ children: [
1708
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1709
+ "input",
1710
+ {
1711
+ id: "composer-audience-recipients",
1712
+ type: "radio",
1713
+ name: "composer-audience",
1714
+ checked: audienceMode === "recipients",
1715
+ onChange: () => setAudienceMode("recipients"),
1716
+ className: "border-border focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
1717
+ }
1718
+ ),
1719
+ "Recipients"
1720
+ ]
1721
+ }
1722
+ )
1723
+ ] }),
1724
+ audienceMode === "segment" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
1725
+ "select",
1726
+ {
1727
+ id: "composer-segment",
1728
+ "aria-label": "Segment",
1729
+ value: segmentId,
1730
+ "aria-invalid": errors.audience ? "true" : void 0,
1731
+ "aria-describedby": errors.audience ? "composer-audience-error" : void 0,
1732
+ onChange: (e) => setSegmentId(e.target.value),
1733
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1734
+ children: [
1735
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: "", children: "Select a segment\u2026" }),
1736
+ segments.map((s) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: s.id, children: s.name }, s.id))
1737
+ ]
1738
+ }
1739
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1740
+ "textarea",
1741
+ {
1742
+ id: "composer-recipients",
1743
+ "aria-label": "Recipient ids",
1744
+ value: recipientsRaw,
1745
+ rows: 4,
1746
+ placeholder: "Recipient ids separated by commas or new lines",
1747
+ "aria-invalid": errors.audience ? "true" : void 0,
1748
+ "aria-describedby": errors.audience ? "composer-audience-error" : void 0,
1749
+ onChange: (e) => setRecipientsRaw(e.target.value),
1750
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
1751
+ }
1752
+ ),
1753
+ errors.audience && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { id: "composer-audience-error", className: "text-xs text-destructive", children: errors.audience })
1754
+ ] }),
1755
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex flex-col gap-1", children: [
1756
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("label", { htmlFor: "composer-type", className: "text-sm font-medium text-foreground", children: "Type (optional)" }),
1757
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1758
+ "input",
1759
+ {
1760
+ id: "composer-type",
1761
+ type: "text",
1762
+ value: type,
1763
+ onChange: (e) => setType(e.target.value),
1764
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
1765
+ }
1766
+ )
1767
+ ] }),
1768
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1769
+ "button",
1770
+ {
1771
+ type: "submit",
1772
+ disabled: create.isPending,
1773
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
1774
+ children: create.isPending ? "Creating\u2026" : "Create broadcast"
1775
+ }
1776
+ ) })
1777
+ ] });
1778
+ }
1779
+
1780
+ // src/segment-list.tsx
1781
+ var import_react_ui18 = require("@quanticjs/react-ui");
1782
+ var import_react_query16 = require("@quanticjs/react-query");
1783
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1784
+ function normalize(data) {
1785
+ if (Array.isArray(data)) return data;
1786
+ return data?.items ?? [];
1787
+ }
1788
+ function typeVariant(type) {
1789
+ switch (type) {
1790
+ case "dynamic":
1791
+ return "info";
1792
+ case "static":
1793
+ return "neutral";
1794
+ default:
1795
+ return "neutral";
1796
+ }
1797
+ }
1798
+ function SegmentList({
1799
+ basePath = "/api",
1800
+ onSelect,
1801
+ onCreate,
1802
+ className
1803
+ }) {
1804
+ const { data, isLoading, isError, refetch } = (0, import_react_query16.useApiQuery)(
1805
+ ["segments"],
1806
+ (client) => client.get(`${basePath}/v1/segments`)
1807
+ );
1808
+ if (isLoading) {
1809
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
1810
+ "div",
1811
+ {
1812
+ role: "status",
1813
+ "aria-label": "Loading segments",
1814
+ className: (0, import_react_ui18.cn)("flex flex-col gap-2 p-4", className),
1815
+ children: [
1816
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "sr-only", children: "Loading segments" }),
1817
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
1818
+ ]
1819
+ }
1820
+ );
1821
+ }
1822
+ if (isError) {
1823
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: (0, import_react_ui18.cn)("flex flex-col items-start gap-3 p-4", className), children: [
1824
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load segments" }),
1825
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1826
+ "button",
1827
+ {
1828
+ type: "button",
1829
+ onClick: () => void refetch(),
1830
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1831
+ children: "Try again"
1832
+ }
1833
+ )
1834
+ ] });
1835
+ }
1836
+ const rows = normalize(data);
1837
+ const header = onCreate ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("header", { className: "flex items-center justify-end", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1838
+ "button",
1839
+ {
1840
+ type: "button",
1841
+ onClick: () => onCreate(),
1842
+ className: "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1843
+ children: "New segment"
1844
+ }
1845
+ ) }) : null;
1846
+ if (rows.length === 0) {
1847
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": "Segments", className: (0, import_react_ui18.cn)("flex flex-col gap-3", className), children: [
1848
+ header,
1849
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "No segments" })
1850
+ ] });
1851
+ }
1852
+ const renderNameCell = (row) => onSelect ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1853
+ "button",
1854
+ {
1855
+ type: "button",
1856
+ onClick: () => onSelect(row.id),
1857
+ className: "rounded text-start font-medium text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1858
+ children: row.name
1859
+ }
1860
+ ) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-foreground", children: row.name });
1861
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("section", { "aria-label": "Segments", className: (0, import_react_ui18.cn)("flex flex-col gap-3", className), children: [
1862
+ header,
1863
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("table", { className: "w-full text-sm", children: [
1864
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
1865
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Name" }),
1866
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Type" }),
1867
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Description" }),
1868
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Recipients" }),
1869
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" })
1870
+ ] }) }),
1871
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("tr", { className: "border-b border-border", children: [
1872
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("td", { className: "py-3 pe-4", children: renderNameCell(row) }),
1873
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_ui18.StatusBadge, { variant: typeVariant(row.type), appearance: "dot", children: row.type }) }),
1874
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.description ?? "\u2014" }),
1875
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.recipientIds?.length ?? "\u2014" }),
1876
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui18.formatDateTime)(row.createdAt) })
1877
+ ] }, row.id)) })
1878
+ ] })
1879
+ ] });
1880
+ }
1881
+
1882
+ // src/segment-builder.tsx
1883
+ var import_react11 = require("react");
1884
+ var import_react_ui19 = require("@quanticjs/react-ui");
1885
+ var import_react_query17 = require("@quanticjs/react-query");
1886
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1887
+ function parseRecipientIds(value) {
1888
+ return value.split(/[\n,]/).map((id) => id.trim()).filter((id) => id.length > 0);
1889
+ }
1890
+ function SegmentBuilder({
1891
+ basePath = "/api",
1892
+ segmentId,
1893
+ initial,
1894
+ onSaved,
1895
+ onDeleted,
1896
+ className
1897
+ }) {
1898
+ const toast = (0, import_react_ui19.useToast)();
1899
+ const [name, setName] = (0, import_react11.useState)(initial?.name ?? "");
1900
+ const [type, setType] = (0, import_react11.useState)(initial?.type ?? "static");
1901
+ const [description, setDescription] = (0, import_react11.useState)(initial?.description ?? "");
1902
+ const [recipientText, setRecipientText] = (0, import_react11.useState)((initial?.recipientIds ?? []).join("\n"));
1903
+ const [errors, setErrors] = (0, import_react11.useState)({});
1904
+ const isEdit = Boolean(segmentId);
1905
+ const onMutationError = (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
1906
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
1907
+ });
1908
+ const save = (0, import_react_query17.useApiMutation)(
1909
+ (client, payload) => segmentId ? client.patch(`${basePath}/v1/segments/${segmentId}`, payload) : client.post(`${basePath}/v1/segments`, payload),
1910
+ {
1911
+ invalidates: [["segments"]],
1912
+ onSuccess: (result) => {
1913
+ toast.success(isEdit ? "Segment updated" : "Segment created");
1914
+ onSaved?.(result.id);
1915
+ },
1916
+ onError: onMutationError
1917
+ }
1918
+ );
1919
+ const remove = (0, import_react_query17.useApiMutation)(
1920
+ (client) => client.delete(`${basePath}/v1/segments/${segmentId}`),
1921
+ {
1922
+ invalidates: [["segments"]],
1923
+ onSuccess: () => {
1924
+ toast.success("Segment deleted");
1925
+ onDeleted?.();
1926
+ },
1927
+ onError: onMutationError
1928
+ }
1929
+ );
1930
+ const validate = (recipientIds) => {
1931
+ const next = {};
1932
+ if (name.trim().length === 0) {
1933
+ next.name = "Name is required";
1934
+ }
1935
+ if (type === "static" && recipientIds.length === 0) {
1936
+ next.recipientIds = "A static segment needs at least one recipient id";
1937
+ }
1938
+ setErrors(next);
1939
+ return Object.keys(next).length === 0;
1940
+ };
1941
+ const onSubmit = (event) => {
1942
+ event.preventDefault();
1943
+ const recipientIds = parseRecipientIds(recipientText);
1944
+ if (!validate(recipientIds)) return;
1945
+ save.mutate({ name: name.trim(), type, description: description.trim(), recipientIds });
1946
+ };
1947
+ const onDelete = () => {
1948
+ if (!segmentId) return;
1949
+ if (!window.confirm("Delete this segment? This cannot be undone.")) return;
1950
+ remove.mutate();
1951
+ };
1952
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("form", { onSubmit, className: (0, import_react_ui19.cn)("flex flex-col gap-4", className), noValidate: true, children: [
1953
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("header", { className: "flex items-center justify-between", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: isEdit ? `Edit segment: ${segmentId}` : "New segment" }) }),
1954
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-col gap-1", children: [
1955
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("label", { htmlFor: "segment-name", className: "text-sm font-medium text-foreground", children: "Name" }),
1956
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1957
+ "input",
1958
+ {
1959
+ id: "segment-name",
1960
+ type: "text",
1961
+ value: name,
1962
+ "aria-invalid": errors.name ? "true" : void 0,
1963
+ "aria-describedby": errors.name ? "segment-name-error" : void 0,
1964
+ onChange: (e) => setName(e.target.value),
1965
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
1966
+ }
1967
+ ),
1968
+ errors.name && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { id: "segment-name-error", className: "text-xs text-destructive", children: errors.name })
1969
+ ] }),
1970
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-col gap-1", children: [
1971
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("label", { htmlFor: "segment-type", className: "text-sm font-medium text-foreground", children: "Type" }),
1972
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
1973
+ "select",
1974
+ {
1975
+ id: "segment-type",
1976
+ value: type,
1977
+ onChange: (e) => setType(e.target.value),
1978
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
1979
+ children: [
1980
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("option", { value: "static", children: "static" }),
1981
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("option", { value: "dynamic", children: "dynamic" })
1982
+ ]
1983
+ }
1984
+ )
1985
+ ] }),
1986
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-col gap-1", children: [
1987
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("label", { htmlFor: "segment-description", className: "text-sm font-medium text-foreground", children: "Description" }),
1988
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1989
+ "textarea",
1990
+ {
1991
+ id: "segment-description",
1992
+ value: description,
1993
+ rows: 3,
1994
+ onChange: (e) => setDescription(e.target.value),
1995
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
1996
+ }
1997
+ )
1998
+ ] }),
1999
+ type === "static" && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-col gap-1", children: [
2000
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("label", { htmlFor: "segment-recipients", className: "text-sm font-medium text-foreground", children: "Recipient ids" }),
2001
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2002
+ "textarea",
2003
+ {
2004
+ id: "segment-recipients",
2005
+ value: recipientText,
2006
+ rows: 4,
2007
+ placeholder: "Comma or newline separated",
2008
+ "aria-invalid": errors.recipientIds ? "true" : void 0,
2009
+ "aria-describedby": errors.recipientIds ? "segment-recipients-error" : void 0,
2010
+ onChange: (e) => setRecipientText(e.target.value),
2011
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2012
+ }
2013
+ ),
2014
+ errors.recipientIds && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { id: "segment-recipients-error", className: "text-xs text-destructive", children: errors.recipientIds })
2015
+ ] }),
2016
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-3", children: [
2017
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2018
+ "button",
2019
+ {
2020
+ type: "submit",
2021
+ disabled: save.isPending,
2022
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2023
+ children: save.isPending ? "Saving\u2026" : isEdit ? "Save changes" : "Create segment"
2024
+ }
2025
+ ),
2026
+ isEdit && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2027
+ "button",
2028
+ {
2029
+ type: "button",
2030
+ disabled: remove.isPending,
2031
+ onClick: onDelete,
2032
+ className: "rounded-md border border-border px-4 py-2 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2033
+ children: remove.isPending ? "Deleting\u2026" : "Delete"
2034
+ }
2035
+ )
2036
+ ] })
2037
+ ] });
2038
+ }
2039
+
2040
+ // src/suppression-manager.tsx
2041
+ var import_react12 = require("react");
2042
+ var import_react_ui20 = require("@quanticjs/react-ui");
2043
+ var import_react_query18 = require("@quanticjs/react-query");
2044
+ var import_jsx_runtime21 = require("react/jsx-runtime");
2045
+ var LIMIT3 = 20;
2046
+ var CHANNELS3 = ["inapp", "email", "push", "sms"];
2047
+ function SuppressionManager({ basePath = "/api", className }) {
2048
+ const toast = (0, import_react_ui20.useToast)();
2049
+ const [page, setPage] = (0, import_react12.useState)(1);
2050
+ const [channel, setChannel] = (0, import_react12.useState)("email");
2051
+ const [address, setAddress] = (0, import_react12.useState)("");
2052
+ const [reason, setReason] = (0, import_react12.useState)("");
2053
+ const { data, isLoading, isError, refetch } = (0, import_react_query18.useApiQuery)(
2054
+ ["suppression", page],
2055
+ (client) => client.get(`${basePath}/v1/admin/suppression?page=${page}&limit=${LIMIT3}`)
2056
+ );
2057
+ const onMutationError = (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
2058
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
2059
+ });
2060
+ const add = (0, import_react_query18.useApiMutation)(
2061
+ (client, payload) => client.post(`${basePath}/v1/admin/suppression`, payload),
2062
+ {
2063
+ invalidates: [["suppression"]],
2064
+ onSuccess: () => {
2065
+ toast.success("Suppression added");
2066
+ setAddress("");
2067
+ setReason("");
2068
+ void refetch();
2069
+ },
2070
+ onError: onMutationError
2071
+ }
2072
+ );
2073
+ const remove = (0, import_react_query18.useApiMutation)(
2074
+ (client, id) => client.delete(`${basePath}/v1/admin/suppression/${id}`),
2075
+ {
2076
+ invalidates: [["suppression"]],
2077
+ onSuccess: () => {
2078
+ toast.success("Suppression removed");
2079
+ void refetch();
2080
+ },
2081
+ onError: onMutationError
2082
+ }
2083
+ );
2084
+ const onAdd = (event) => {
2085
+ event.preventDefault();
2086
+ add.mutate({ channel, address: address.trim(), reason: reason.trim() });
2087
+ };
2088
+ const addForm = /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("form", { onSubmit: onAdd, className: "flex flex-wrap items-end gap-3", noValidate: true, children: [
2089
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-col gap-1", children: [
2090
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("label", { htmlFor: "suppression-channel", className: "text-sm font-medium text-foreground", children: "Channel" }),
2091
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2092
+ "select",
2093
+ {
2094
+ id: "suppression-channel",
2095
+ value: channel,
2096
+ onChange: (e) => setChannel(e.target.value),
2097
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2098
+ children: CHANNELS3.map((c) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("option", { value: c, children: c }, c))
2099
+ }
2100
+ )
2101
+ ] }),
2102
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-col gap-1", children: [
2103
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("label", { htmlFor: "suppression-address", className: "text-sm font-medium text-foreground", children: "Address" }),
2104
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2105
+ "input",
2106
+ {
2107
+ id: "suppression-address",
2108
+ type: "text",
2109
+ value: address,
2110
+ onChange: (e) => setAddress(e.target.value),
2111
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2112
+ }
2113
+ )
2114
+ ] }),
2115
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-col gap-1", children: [
2116
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("label", { htmlFor: "suppression-reason", className: "text-sm font-medium text-foreground", children: "Reason" }),
2117
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2118
+ "input",
2119
+ {
2120
+ id: "suppression-reason",
2121
+ type: "text",
2122
+ value: reason,
2123
+ onChange: (e) => setReason(e.target.value),
2124
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2125
+ }
2126
+ )
2127
+ ] }),
2128
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2129
+ "button",
2130
+ {
2131
+ type: "submit",
2132
+ disabled: add.isPending,
2133
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2134
+ children: add.isPending ? "Adding\u2026" : "Add suppression"
2135
+ }
2136
+ )
2137
+ ] });
2138
+ let body;
2139
+ if (isLoading) {
2140
+ body = /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { role: "status", "aria-label": "Loading suppressions", className: "flex flex-col gap-2 p-4", children: [
2141
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "sr-only", children: "Loading suppressions" }),
2142
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
2143
+ ] });
2144
+ } else if (isError) {
2145
+ body = /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-col items-start gap-3 p-4", children: [
2146
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load suppressions" }),
2147
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2148
+ "button",
2149
+ {
2150
+ type: "button",
2151
+ onClick: () => void refetch(),
2152
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2153
+ children: "Try again"
2154
+ }
2155
+ )
2156
+ ] });
2157
+ } else {
2158
+ const rows = data?.items ?? [];
2159
+ const totalPages = data?.totalPages ?? 1;
2160
+ if (rows.length === 0) {
2161
+ body = /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "No suppressions" });
2162
+ } else {
2163
+ body = /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(import_jsx_runtime21.Fragment, { children: [
2164
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("table", { className: "w-full text-sm", children: [
2165
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
2166
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Channel" }),
2167
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Address" }),
2168
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Reason" }),
2169
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" }),
2170
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "sr-only", children: "Actions" }) })
2171
+ ] }) }),
2172
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("tr", { className: "border-b border-border", children: [
2173
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("td", { className: "py-3 pe-4 text-foreground", children: row.channel }),
2174
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.address }),
2175
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.reason }),
2176
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui20.formatDateTime)(row.createdAt) }),
2177
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2178
+ "button",
2179
+ {
2180
+ type: "button",
2181
+ disabled: remove.isPending,
2182
+ onClick: () => remove.mutate(row.id),
2183
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2184
+ children: "Remove"
2185
+ }
2186
+ ) })
2187
+ ] }, row.id)) })
2188
+ ] }),
2189
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("nav", { "aria-label": "Suppression pagination", className: "flex items-center justify-between", children: [
2190
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2191
+ "button",
2192
+ {
2193
+ type: "button",
2194
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
2195
+ disabled: page <= 1,
2196
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2197
+ children: "Previous"
2198
+ }
2199
+ ),
2200
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { className: "text-xs text-muted-foreground", children: [
2201
+ "Page ",
2202
+ page,
2203
+ " of ",
2204
+ totalPages
2205
+ ] }),
2206
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
2207
+ "button",
2208
+ {
2209
+ type: "button",
2210
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
2211
+ disabled: page >= totalPages,
2212
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2213
+ children: "Next"
2214
+ }
2215
+ )
2216
+ ] })
2217
+ ] });
2218
+ }
2219
+ }
2220
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("section", { "aria-label": "Suppression list", className: (0, import_react_ui20.cn)("flex flex-col gap-4", className), children: [
2221
+ addForm,
2222
+ body
2223
+ ] });
2224
+ }
2225
+
2226
+ // src/dlq-console.tsx
2227
+ var import_react13 = require("react");
2228
+ var import_react_ui21 = require("@quanticjs/react-ui");
2229
+ var import_react_query19 = require("@quanticjs/react-query");
2230
+ var import_jsx_runtime22 = require("react/jsx-runtime");
2231
+ var LIMIT4 = 20;
2232
+ var STATUS_FILTERS = ["queued", "replayed", "discarded"];
2233
+ function buildErrorDescription(error) {
2234
+ return error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`;
2235
+ }
2236
+ function statusVariant3(status) {
2237
+ switch (status) {
2238
+ case "replayed":
2239
+ return "success";
2240
+ case "discarded":
2241
+ return "neutral";
2242
+ case "queued":
2243
+ return "warning";
2244
+ default:
2245
+ return "neutral";
2246
+ }
2247
+ }
2248
+ function DlqMessageDetailRow({ id, basePath }) {
2249
+ const { data, isLoading, isError, refetch } = (0, import_react_query19.useApiQuery)(
2250
+ ["dlq", "detail", id],
2251
+ (client) => client.get(`${basePath}/admin/dlq/${id}`)
2252
+ );
2253
+ if (isLoading) {
2254
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { role: "status", "aria-label": "Loading message detail", className: "flex flex-col gap-2 p-3", children: [
2255
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sr-only", children: "Loading message detail" }),
2256
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "aria-hidden": "true", className: "h-16 animate-pulse rounded bg-muted" })
2257
+ ] });
2258
+ }
2259
+ if (isError) {
2260
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex flex-col items-start gap-3 p-3", children: [
2261
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load message detail" }),
2262
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2263
+ "button",
2264
+ {
2265
+ type: "button",
2266
+ onClick: () => void refetch(),
2267
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2268
+ children: "Try again"
2269
+ }
2270
+ )
2271
+ ] });
2272
+ }
2273
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex flex-col gap-2 p-3 text-sm", children: [
2274
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("p", { className: "text-muted-foreground", children: [
2275
+ "Error: ",
2276
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "text-foreground", children: data?.errorMessage ?? "\u2014" })
2277
+ ] }),
2278
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("pre", { className: "max-h-64 overflow-auto rounded bg-muted p-3 text-xs text-foreground", children: JSON.stringify(data?.payload ?? {}, null, 2) })
2279
+ ] });
2280
+ }
2281
+ function DlqConsole({ basePath = "/api", className }) {
2282
+ const toast = (0, import_react_ui21.useToast)();
2283
+ const [page, setPage] = (0, import_react13.useState)(1);
2284
+ const [statusFilter, setStatusFilter] = (0, import_react13.useState)("");
2285
+ const [expandedId, setExpandedId] = (0, import_react13.useState)(null);
2286
+ const statusQuery = statusFilter ? `&status=${statusFilter}` : "";
2287
+ const { data, isLoading, isError, refetch } = (0, import_react_query19.useApiQuery)(
2288
+ ["dlq", page, statusFilter],
2289
+ (client) => client.get(`${basePath}/admin/dlq?page=${page}&limit=${LIMIT4}${statusQuery}`)
2290
+ );
2291
+ const replay = (0, import_react_query19.useApiMutation)(
2292
+ (client, id) => client.post(`${basePath}/admin/dlq/${id}/replay`, {}),
2293
+ {
2294
+ invalidates: [["dlq"]],
2295
+ onSuccess: () => {
2296
+ toast.success("Message queued for replay");
2297
+ void refetch();
2298
+ },
2299
+ onError: (error) => {
2300
+ if (error.isConflict) {
2301
+ toast.error("Message already replayed", {
2302
+ description: `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
2303
+ });
2304
+ void refetch();
2305
+ return;
2306
+ }
2307
+ toast.error(error.isServerError ? "Something went wrong" : error.title, {
2308
+ description: buildErrorDescription(error)
2309
+ });
2310
+ }
2311
+ }
2312
+ );
2313
+ const discard = (0, import_react_query19.useApiMutation)(
2314
+ (client, id) => client.delete(`${basePath}/admin/dlq/${id}`),
2315
+ {
2316
+ invalidates: [["dlq"]],
2317
+ onSuccess: () => {
2318
+ toast.success("Message discarded");
2319
+ void refetch();
2320
+ },
2321
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
2322
+ description: buildErrorDescription(error)
2323
+ })
2324
+ }
2325
+ );
2326
+ const filterControl = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-2", children: [
2327
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("label", { htmlFor: "dlq-status", className: "text-sm font-medium text-foreground", children: "Status" }),
2328
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2329
+ "select",
2330
+ {
2331
+ id: "dlq-status",
2332
+ value: statusFilter,
2333
+ onChange: (e) => {
2334
+ setStatusFilter(e.target.value);
2335
+ setPage(1);
2336
+ },
2337
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2338
+ children: [
2339
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("option", { value: "", children: "All" }),
2340
+ STATUS_FILTERS.map((s) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("option", { value: s, children: s }, s))
2341
+ ]
2342
+ }
2343
+ )
2344
+ ] });
2345
+ let body;
2346
+ if (isLoading) {
2347
+ body = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
2348
+ "div",
2349
+ {
2350
+ role: "status",
2351
+ "aria-label": "Loading dead-letter messages",
2352
+ className: "flex flex-col gap-2 p-4",
2353
+ children: [
2354
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sr-only", children: "Loading dead-letter messages" }),
2355
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
2356
+ ]
2357
+ }
2358
+ );
2359
+ } else if (isError) {
2360
+ body = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex flex-col items-start gap-3 p-4", children: [
2361
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load dead-letter messages" }),
2362
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2363
+ "button",
2364
+ {
2365
+ type: "button",
2366
+ onClick: () => void refetch(),
2367
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2368
+ children: "Try again"
2369
+ }
2370
+ )
2371
+ ] });
2372
+ } else {
2373
+ const rows = data?.items ?? [];
2374
+ const totalPages = data?.totalPages ?? 1;
2375
+ if (rows.length === 0) {
2376
+ body = /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "No dead-letter messages" });
2377
+ } else {
2378
+ body = /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_jsx_runtime22.Fragment, { children: [
2379
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("table", { className: "w-full text-sm", children: [
2380
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
2381
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Request" }),
2382
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Failure reason" }),
2383
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
2384
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Attempts" }),
2385
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "First seen" }),
2386
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { className: "sr-only", children: "Actions" }) })
2387
+ ] }) }),
2388
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("tbody", { children: rows.map((row) => {
2389
+ const isExpanded = expandedId === row.id;
2390
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(import_react13.Fragment, { children: [
2391
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("tr", { className: "border-b border-border", children: [
2392
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("td", { className: "py-3 pe-4", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2393
+ "button",
2394
+ {
2395
+ type: "button",
2396
+ "aria-expanded": isExpanded,
2397
+ onClick: () => setExpandedId(isExpanded ? null : row.id),
2398
+ className: "rounded text-start font-medium text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2399
+ children: row.requestId ?? row.id
2400
+ }
2401
+ ) }),
2402
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.failureReason }),
2403
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(import_react_ui21.StatusBadge, { variant: statusVariant3(row.status), appearance: "dot", children: row.status }) }),
2404
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.attemptCount }),
2405
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui21.formatDateTime)(row.firstSeenAt) }),
2406
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "flex items-center gap-2", children: [
2407
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2408
+ "button",
2409
+ {
2410
+ type: "button",
2411
+ disabled: replay.isPending,
2412
+ onClick: () => replay.mutate(row.id),
2413
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2414
+ children: "Replay"
2415
+ }
2416
+ ),
2417
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2418
+ "button",
2419
+ {
2420
+ type: "button",
2421
+ disabled: discard.isPending,
2422
+ onClick: () => discard.mutate(row.id),
2423
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2424
+ children: "Discard"
2425
+ }
2426
+ )
2427
+ ] }) })
2428
+ ] }),
2429
+ isExpanded && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("tr", { className: "border-b border-border", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("td", { colSpan: 6, className: "bg-muted/40", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(DlqMessageDetailRow, { id: row.id, basePath }) }) })
2430
+ ] }, row.id);
2431
+ }) })
2432
+ ] }),
2433
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("nav", { "aria-label": "Dead-letter pagination", className: "flex items-center justify-between", children: [
2434
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2435
+ "button",
2436
+ {
2437
+ type: "button",
2438
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
2439
+ disabled: page <= 1,
2440
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2441
+ children: "Previous"
2442
+ }
2443
+ ),
2444
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("span", { className: "text-xs text-muted-foreground", children: [
2445
+ "Page ",
2446
+ page,
2447
+ " of ",
2448
+ totalPages
2449
+ ] }),
2450
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
2451
+ "button",
2452
+ {
2453
+ type: "button",
2454
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
2455
+ disabled: page >= totalPages,
2456
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2457
+ children: "Next"
2458
+ }
2459
+ )
2460
+ ] })
2461
+ ] });
2462
+ }
2463
+ }
2464
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("section", { "aria-label": "Dead-letter queue", className: (0, import_react_ui21.cn)("flex flex-col gap-4", className), children: [
2465
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("header", { className: "flex items-center justify-between", children: [
2466
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: "Dead-letter queue" }),
2467
+ filterControl
2468
+ ] }),
2469
+ body
2470
+ ] });
2471
+ }
2472
+
2473
+ // src/catalog-editor.tsx
2474
+ var import_react14 = require("react");
2475
+ var import_react_ui22 = require("@quanticjs/react-ui");
2476
+ var import_react_query20 = require("@quanticjs/react-query");
2477
+ var import_jsx_runtime23 = require("react/jsx-runtime");
2478
+ function normalize2(data) {
2479
+ if (!data) return [];
2480
+ if (Array.isArray(data)) return data;
2481
+ if ("entries" in data && Array.isArray(data.entries)) {
2482
+ return data.entries ?? [];
2483
+ }
2484
+ return Object.values(data).filter(
2485
+ (entry) => typeof entry === "object" && entry !== null && "key" in entry && "locale" in entry && "value" in entry
2486
+ );
2487
+ }
2488
+ function CatalogEditor({ basePath = "/api", className }) {
2489
+ const toast = (0, import_react_ui22.useToast)();
2490
+ const [filter, setFilter] = (0, import_react14.useState)("");
2491
+ const [editingId, setEditingId] = (0, import_react14.useState)(null);
2492
+ const [editValue, setEditValue] = (0, import_react14.useState)("");
2493
+ const [newKey, setNewKey] = (0, import_react14.useState)("");
2494
+ const [newLocale, setNewLocale] = (0, import_react14.useState)("");
2495
+ const [newValue, setNewValue] = (0, import_react14.useState)("");
2496
+ const { data, isLoading, isError, refetch } = (0, import_react_query20.useApiQuery)(["i18n-catalog"], (client) => client.get(`${basePath}/i18n/catalog/export`));
2497
+ const onMutationError = (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
2498
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
2499
+ });
2500
+ const upsert = (0, import_react_query20.useApiMutation)(
2501
+ (client, payload) => client.put(`${basePath}/i18n/catalog/entries/${encodeURIComponent(payload.key)}`, {
2502
+ locale: payload.locale,
2503
+ value: payload.value
2504
+ }),
2505
+ {
2506
+ invalidates: [["i18n-catalog"]],
2507
+ onSuccess: () => {
2508
+ toast.success("Entry saved");
2509
+ setEditingId(null);
2510
+ void refetch();
2511
+ },
2512
+ onError: onMutationError
2513
+ }
2514
+ );
2515
+ const add = (0, import_react_query20.useApiMutation)(
2516
+ (client, payload) => client.put(`${basePath}/i18n/catalog/entries/${encodeURIComponent(payload.key)}`, {
2517
+ locale: payload.locale,
2518
+ value: payload.value
2519
+ }),
2520
+ {
2521
+ invalidates: [["i18n-catalog"]],
2522
+ onSuccess: () => {
2523
+ toast.success("Entry added");
2524
+ setNewKey("");
2525
+ setNewLocale("");
2526
+ setNewValue("");
2527
+ void refetch();
2528
+ },
2529
+ onError: onMutationError
2530
+ }
2531
+ );
2532
+ const remove = (0, import_react_query20.useApiMutation)(
2533
+ (client, payload) => client.delete(
2534
+ `${basePath}/i18n/catalog/entries/${encodeURIComponent(payload.key)}?locale=${encodeURIComponent(payload.locale)}`
2535
+ ),
2536
+ {
2537
+ invalidates: [["i18n-catalog"]],
2538
+ onSuccess: () => {
2539
+ toast.success("Entry removed");
2540
+ void refetch();
2541
+ },
2542
+ onError: onMutationError
2543
+ }
2544
+ );
2545
+ const entries = (0, import_react14.useMemo)(() => normalize2(data), [data]);
2546
+ const filtered = (0, import_react14.useMemo)(() => {
2547
+ const q = filter.trim().toLowerCase();
2548
+ if (!q) return entries;
2549
+ return entries.filter((e) => e.key.toLowerCase().includes(q));
2550
+ }, [entries, filter]);
2551
+ const onAdd = (event) => {
2552
+ event.preventDefault();
2553
+ add.mutate({ key: newKey.trim(), locale: newLocale.trim(), value: newValue });
2554
+ };
2555
+ const startEdit = (entry) => {
2556
+ setEditingId(`${entry.key}:${entry.locale}`);
2557
+ setEditValue(entry.value);
2558
+ };
2559
+ const addForm = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("form", { onSubmit: onAdd, className: "flex flex-wrap items-end gap-3", noValidate: true, children: [
2560
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col gap-1", children: [
2561
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("label", { htmlFor: "catalog-new-key", className: "text-sm font-medium text-foreground", children: "Key" }),
2562
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2563
+ "input",
2564
+ {
2565
+ id: "catalog-new-key",
2566
+ type: "text",
2567
+ value: newKey,
2568
+ onChange: (e) => setNewKey(e.target.value),
2569
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2570
+ }
2571
+ )
2572
+ ] }),
2573
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col gap-1", children: [
2574
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("label", { htmlFor: "catalog-new-locale", className: "text-sm font-medium text-foreground", children: "Locale" }),
2575
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2576
+ "input",
2577
+ {
2578
+ id: "catalog-new-locale",
2579
+ type: "text",
2580
+ value: newLocale,
2581
+ onChange: (e) => setNewLocale(e.target.value),
2582
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2583
+ }
2584
+ )
2585
+ ] }),
2586
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col gap-1", children: [
2587
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("label", { htmlFor: "catalog-new-value", className: "text-sm font-medium text-foreground", children: "Value" }),
2588
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2589
+ "input",
2590
+ {
2591
+ id: "catalog-new-value",
2592
+ type: "text",
2593
+ value: newValue,
2594
+ onChange: (e) => setNewValue(e.target.value),
2595
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2596
+ }
2597
+ )
2598
+ ] }),
2599
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2600
+ "button",
2601
+ {
2602
+ type: "submit",
2603
+ disabled: add.isPending,
2604
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2605
+ children: add.isPending ? "Adding\u2026" : "Add entry"
2606
+ }
2607
+ )
2608
+ ] });
2609
+ const filterInput = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col gap-1", children: [
2610
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("label", { htmlFor: "catalog-filter", className: "text-sm font-medium text-foreground", children: "Filter by key" }),
2611
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2612
+ "input",
2613
+ {
2614
+ id: "catalog-filter",
2615
+ type: "text",
2616
+ value: filter,
2617
+ onChange: (e) => setFilter(e.target.value),
2618
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2619
+ }
2620
+ )
2621
+ ] });
2622
+ let body;
2623
+ if (isLoading) {
2624
+ body = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { role: "status", "aria-label": "Loading catalog", className: "flex flex-col gap-2 p-4", children: [
2625
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "sr-only", children: "Loading catalog" }),
2626
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
2627
+ ] });
2628
+ } else if (isError) {
2629
+ body = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("div", { className: "flex flex-col items-start gap-3 p-4", children: [
2630
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load catalog" }),
2631
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2632
+ "button",
2633
+ {
2634
+ type: "button",
2635
+ onClick: () => void refetch(),
2636
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2637
+ children: "Try again"
2638
+ }
2639
+ )
2640
+ ] });
2641
+ } else if (filtered.length === 0) {
2642
+ body = /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: entries.length === 0 ? "No catalog entries" : "No entries match your filter" });
2643
+ } else {
2644
+ body = /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("table", { className: "w-full text-sm", children: [
2645
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
2646
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Key" }),
2647
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Locale" }),
2648
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Value" }),
2649
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "sr-only", children: "Actions" }) })
2650
+ ] }) }),
2651
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("tbody", { children: filtered.map((entry) => {
2652
+ const rowId = `${entry.key}:${entry.locale}`;
2653
+ const isEditing = editingId === rowId;
2654
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("tr", { className: "border-b border-border", children: [
2655
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("td", { className: "py-3 pe-4 font-mono text-foreground", children: entry.key }),
2656
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("td", { className: "px-4 py-3 text-foreground", children: entry.locale }),
2657
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("td", { className: "px-4 py-3 text-foreground", children: isEditing ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2658
+ "input",
2659
+ {
2660
+ type: "text",
2661
+ "aria-label": `Value for ${entry.key} (${entry.locale})`,
2662
+ value: editValue,
2663
+ onChange: (e) => setEditValue(e.target.value),
2664
+ className: "w-full rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2665
+ }
2666
+ ) : entry.value }),
2667
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { className: "flex items-center gap-2", children: isEditing ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
2668
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2669
+ "button",
2670
+ {
2671
+ type: "button",
2672
+ disabled: upsert.isPending,
2673
+ onClick: () => upsert.mutate({
2674
+ key: entry.key,
2675
+ locale: entry.locale,
2676
+ value: editValue
2677
+ }),
2678
+ className: "rounded-md bg-primary px-3 py-1 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2679
+ children: "Save"
2680
+ }
2681
+ ),
2682
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2683
+ "button",
2684
+ {
2685
+ type: "button",
2686
+ onClick: () => setEditingId(null),
2687
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2688
+ children: "Cancel"
2689
+ }
2690
+ )
2691
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
2692
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2693
+ "button",
2694
+ {
2695
+ type: "button",
2696
+ onClick: () => startEdit(entry),
2697
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2698
+ children: "Edit"
2699
+ }
2700
+ ),
2701
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
2702
+ "button",
2703
+ {
2704
+ type: "button",
2705
+ disabled: remove.isPending,
2706
+ onClick: () => remove.mutate({ key: entry.key, locale: entry.locale }),
2707
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2708
+ children: "Delete"
2709
+ }
2710
+ )
2711
+ ] }) }) })
2712
+ ] }, rowId);
2713
+ }) })
2714
+ ] });
2715
+ }
2716
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("section", { "aria-label": "Catalog editor", className: (0, import_react_ui22.cn)("flex flex-col gap-4", className), children: [
2717
+ addForm,
2718
+ filterInput,
2719
+ body
2720
+ ] });
2721
+ }
2722
+
2723
+ // src/missing-translations-panel.tsx
2724
+ var import_react_ui23 = require("@quanticjs/react-ui");
2725
+ var import_react_query21 = require("@quanticjs/react-query");
2726
+ var import_jsx_runtime24 = require("react/jsx-runtime");
2727
+ function normalize3(data) {
2728
+ if (Array.isArray(data)) return data;
2729
+ return data?.missing ?? [];
2730
+ }
2731
+ function MissingTranslationsPanel({
2732
+ basePath = "/api",
2733
+ className
2734
+ }) {
2735
+ const { data, isLoading, isError, refetch } = (0, import_react_query21.useApiQuery)(
2736
+ ["i18n-missing"],
2737
+ (client) => client.get(`${basePath}/i18n/catalog/missing`)
2738
+ );
2739
+ if (isLoading) {
2740
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
2741
+ "div",
2742
+ {
2743
+ role: "status",
2744
+ "aria-label": "Loading missing translations",
2745
+ className: (0, import_react_ui23.cn)("flex flex-col gap-2 p-4", className),
2746
+ children: [
2747
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "sr-only", children: "Loading missing translations" }),
2748
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
2749
+ ]
2750
+ }
2751
+ );
2752
+ }
2753
+ if (isError) {
2754
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: (0, import_react_ui23.cn)("flex flex-col items-start gap-3 p-4", className), children: [
2755
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load missing translations" }),
2756
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
2757
+ "button",
2758
+ {
2759
+ type: "button",
2760
+ onClick: () => void refetch(),
2761
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2762
+ children: "Try again"
2763
+ }
2764
+ )
2765
+ ] });
2766
+ }
2767
+ const rows = normalize3(data);
2768
+ if (rows.length === 0) {
2769
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: (0, import_react_ui23.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No missing translations" });
2770
+ }
2771
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("section", { "aria-label": "Missing translations", className: (0, import_react_ui23.cn)("flex flex-col gap-3", className), children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("table", { className: "w-full text-sm", children: [
2772
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
2773
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Key" }),
2774
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Locale" }),
2775
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Type" })
2776
+ ] }) }),
2777
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("tbody", { children: rows.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("tr", { className: "border-b border-border", children: [
2778
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("td", { className: "py-3 pe-4 font-mono text-foreground", children: row.key }),
2779
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.locale }),
2780
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.type ?? "\u2014" })
2781
+ ] }, `${row.key}:${row.locale}:${i}`)) })
2782
+ ] }) });
2783
+ }
2784
+
2785
+ // src/fallback-report-panel.tsx
2786
+ var import_react_ui24 = require("@quanticjs/react-ui");
2787
+ var import_react_query22 = require("@quanticjs/react-query");
2788
+ var import_jsx_runtime25 = require("react/jsx-runtime");
2789
+ function normalize4(data) {
2790
+ if (Array.isArray(data)) return data;
2791
+ return data?.entries ?? [];
2792
+ }
2793
+ function FallbackReportPanel({ basePath = "/api", className }) {
2794
+ const { data, isLoading, isError, refetch } = (0, import_react_query22.useApiQuery)(
2795
+ ["i18n-fallback-report"],
2796
+ (client) => client.get(`${basePath}/i18n/catalog/fallback-report`)
2797
+ );
2798
+ if (isLoading) {
2799
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
2800
+ "div",
2801
+ {
2802
+ role: "status",
2803
+ "aria-label": "Loading fallback report",
2804
+ className: (0, import_react_ui24.cn)("flex flex-col gap-2 p-4", className),
2805
+ children: [
2806
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "sr-only", children: "Loading fallback report" }),
2807
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
2808
+ ]
2809
+ }
2810
+ );
2811
+ }
2812
+ if (isError) {
2813
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: (0, import_react_ui24.cn)("flex flex-col items-start gap-3 p-4", className), children: [
2814
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load fallback report" }),
2815
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
2816
+ "button",
2817
+ {
2818
+ type: "button",
2819
+ onClick: () => void refetch(),
2820
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2821
+ children: "Try again"
2822
+ }
2823
+ )
2824
+ ] });
2825
+ }
2826
+ const rows = normalize4(data);
2827
+ if (rows.length === 0) {
2828
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("div", { className: (0, import_react_ui24.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No fallbacks reported" });
2829
+ }
2830
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("section", { "aria-label": "Fallback report", className: (0, import_react_ui24.cn)("flex flex-col gap-3", className), children: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("table", { className: "w-full text-sm", children: [
2831
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
2832
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Key" }),
2833
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Requested" }),
2834
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Resolved" }),
2835
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Chain" })
2836
+ ] }) }),
2837
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("tbody", { children: rows.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("tr", { className: "border-b border-border", children: [
2838
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("td", { className: "py-3 pe-4 font-mono text-foreground", children: row.key }),
2839
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.requestedLocale }),
2840
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.resolvedLocale }),
2841
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.steps && row.steps.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("ol", { className: "flex flex-wrap items-center gap-1", children: row.steps.map((step, si) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("li", { className: "flex items-center gap-1", children: [
2842
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "rounded bg-muted px-1.5 py-0.5 text-xs text-foreground", children: step }),
2843
+ si < (row.steps?.length ?? 0) - 1 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { "aria-hidden": "true", className: "text-muted-foreground", children: "\u2192" })
2844
+ ] }, `${step}:${si}`)) }) : "\u2014" })
2845
+ ] }, `${row.key}:${row.requestedLocale}:${i}`)) })
2846
+ ] }) });
2847
+ }
2848
+
2849
+ // src/recipient-admin-panel.tsx
2850
+ var import_react15 = require("react");
2851
+ var import_react_ui25 = require("@quanticjs/react-ui");
2852
+ var import_react_query23 = require("@quanticjs/react-query");
2853
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2854
+ var LIMIT5 = 20;
2855
+ function RecipientAdminPanel({ basePath = "/api", className }) {
2856
+ const toast = (0, import_react_ui25.useToast)();
2857
+ const [page, setPage] = (0, import_react15.useState)(1);
2858
+ const [search, setSearch] = (0, import_react15.useState)("");
2859
+ const [query, setQuery] = (0, import_react15.useState)("");
2860
+ const { data, isLoading, isError, refetch } = (0, import_react_query23.useApiQuery)(
2861
+ ["recipients", page, query],
2862
+ (client) => client.get(
2863
+ `${basePath}/v1/admin/recipients?page=${page}&limit=${LIMIT5}&search=${encodeURIComponent(query)}`
2864
+ )
2865
+ );
2866
+ const onMutationError = (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
2867
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
2868
+ });
2869
+ const exportData = (0, import_react_query23.useApiMutation)(
2870
+ (client, userId) => client.post(`${basePath}/v1/admin/recipients/${userId}/export`, {}),
2871
+ {
2872
+ onSuccess: () => toast.success("Export ready"),
2873
+ onError: onMutationError
2874
+ }
2875
+ );
2876
+ const erase = (0, import_react_query23.useApiMutation)(
2877
+ (client, userId) => client.delete(`${basePath}/v1/admin/recipients/${userId}/erase`),
2878
+ {
2879
+ invalidates: [["recipients"]],
2880
+ onSuccess: () => {
2881
+ toast.success("Recipient erased");
2882
+ void refetch();
2883
+ },
2884
+ onError: onMutationError
2885
+ }
2886
+ );
2887
+ const onSearch = (event) => {
2888
+ event.preventDefault();
2889
+ setPage(1);
2890
+ setQuery(search.trim());
2891
+ };
2892
+ const onErase = (userId) => {
2893
+ if (window.confirm("Erase all data for this recipient? This cannot be undone.")) {
2894
+ erase.mutate(userId);
2895
+ }
2896
+ };
2897
+ const consents = (row) => {
2898
+ const active = [];
2899
+ if (row.consentEmail) active.push("email");
2900
+ if (row.consentPush) active.push("push");
2901
+ if (row.consentSms) active.push("sms");
2902
+ return active.length > 0 ? active.join(", ") : "none";
2903
+ };
2904
+ const searchForm = /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("form", { onSubmit: onSearch, className: "flex flex-wrap items-end gap-3", noValidate: true, children: [
2905
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col gap-1", children: [
2906
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("label", { htmlFor: "recipient-search", className: "text-sm font-medium text-foreground", children: "Search recipients" }),
2907
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2908
+ "input",
2909
+ {
2910
+ id: "recipient-search",
2911
+ type: "text",
2912
+ value: search,
2913
+ onChange: (e) => setSearch(e.target.value),
2914
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
2915
+ }
2916
+ )
2917
+ ] }),
2918
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2919
+ "button",
2920
+ {
2921
+ type: "submit",
2922
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2923
+ children: "Search"
2924
+ }
2925
+ )
2926
+ ] });
2927
+ let body;
2928
+ if (isLoading) {
2929
+ body = /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { role: "status", "aria-label": "Loading recipients", className: "flex flex-col gap-2 p-4", children: [
2930
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "sr-only", children: "Loading recipients" }),
2931
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
2932
+ ] });
2933
+ } else if (isError) {
2934
+ body = /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex flex-col items-start gap-3 p-4", children: [
2935
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load recipients" }),
2936
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2937
+ "button",
2938
+ {
2939
+ type: "button",
2940
+ onClick: () => void refetch(),
2941
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
2942
+ children: "Try again"
2943
+ }
2944
+ )
2945
+ ] });
2946
+ } else {
2947
+ const rows = data?.items ?? [];
2948
+ const totalPages = data?.totalPages ?? 1;
2949
+ if (rows.length === 0) {
2950
+ body = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "No recipients" });
2951
+ } else {
2952
+ body = /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
2953
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("table", { className: "w-full text-sm", children: [
2954
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
2955
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "User" }),
2956
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Email" }),
2957
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Phone" }),
2958
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Consents" }),
2959
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" }),
2960
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { className: "sr-only", children: "Actions" }) })
2961
+ ] }) }),
2962
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("tr", { className: "border-b border-border", children: [
2963
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("td", { className: "py-3 pe-4 font-mono text-foreground", children: row.userId }),
2964
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.email ?? "\u2014" }),
2965
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.phone ?? "\u2014" }),
2966
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: consents(row) }),
2967
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui25.formatDateTime)(row.createdAt) }),
2968
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex items-center gap-2", children: [
2969
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2970
+ "button",
2971
+ {
2972
+ type: "button",
2973
+ disabled: exportData.isPending,
2974
+ onClick: () => exportData.mutate(row.userId),
2975
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2976
+ children: "Export"
2977
+ }
2978
+ ),
2979
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2980
+ "button",
2981
+ {
2982
+ type: "button",
2983
+ disabled: erase.isPending,
2984
+ onClick: () => onErase(row.userId),
2985
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
2986
+ children: "Erase"
2987
+ }
2988
+ )
2989
+ ] }) })
2990
+ ] }, row.userId)) })
2991
+ ] }),
2992
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("nav", { "aria-label": "Recipient pagination", className: "flex items-center justify-between", children: [
2993
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2994
+ "button",
2995
+ {
2996
+ type: "button",
2997
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
2998
+ disabled: page <= 1,
2999
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3000
+ children: "Previous"
3001
+ }
3002
+ ),
3003
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "text-xs text-muted-foreground", children: [
3004
+ "Page ",
3005
+ page,
3006
+ " of ",
3007
+ totalPages
3008
+ ] }),
3009
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
3010
+ "button",
3011
+ {
3012
+ type: "button",
3013
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
3014
+ disabled: page >= totalPages,
3015
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3016
+ children: "Next"
3017
+ }
3018
+ )
3019
+ ] })
3020
+ ] });
3021
+ }
3022
+ }
3023
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("section", { "aria-label": "Recipient administration", className: (0, import_react_ui25.cn)("flex flex-col gap-4", className), children: [
3024
+ searchForm,
3025
+ body
3026
+ ] });
3027
+ }
3028
+
3029
+ // src/webhook-endpoint-manager.tsx
3030
+ var import_react16 = require("react");
3031
+ var import_react_ui26 = require("@quanticjs/react-ui");
3032
+ var import_react_query24 = require("@quanticjs/react-query");
3033
+ var import_jsx_runtime27 = require("react/jsx-runtime");
3034
+ var EVENT_TYPES = [
3035
+ "notification.sent",
3036
+ "notification.delivered",
3037
+ "notification.failed",
3038
+ "notification.bounced"
3039
+ ];
3040
+ function normalizeEndpoints(data) {
3041
+ if (Array.isArray(data)) return data;
3042
+ return data?.items ?? [];
3043
+ }
3044
+ function normalizeDeliveries(data) {
3045
+ if (Array.isArray(data)) return data;
3046
+ return data?.items ?? [];
3047
+ }
3048
+ function toastError(toast, error) {
3049
+ toast.error(error.isServerError ? "Something went wrong" : error.title, {
3050
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
3051
+ });
3052
+ }
3053
+ function WebhookDeliveries({ endpointId, basePath }) {
3054
+ const { data, isLoading, isError, refetch } = (0, import_react_query24.useApiQuery)(
3055
+ ["webhook-deliveries", endpointId],
3056
+ (client) => client.get(`${basePath}/webhook-endpoints/${endpointId}/deliveries`)
3057
+ );
3058
+ if (isLoading) {
3059
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { role: "status", "aria-label": "Loading deliveries", className: "flex flex-col gap-2 p-3", children: [
3060
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "sr-only", children: "Loading deliveries" }),
3061
+ [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "aria-hidden": "true", className: "h-8 animate-pulse rounded bg-muted" }, i))
3062
+ ] });
3063
+ }
3064
+ if (isError) {
3065
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex flex-col items-start gap-2 p-3", children: [
3066
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load deliveries" }),
3067
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3068
+ "button",
3069
+ {
3070
+ type: "button",
3071
+ onClick: () => void refetch(),
3072
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3073
+ children: "Try again"
3074
+ }
3075
+ )
3076
+ ] });
3077
+ }
3078
+ const rows = normalizeDeliveries(data);
3079
+ if (rows.length === 0) {
3080
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "p-3 text-center text-sm text-muted-foreground", children: "No deliveries" });
3081
+ }
3082
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("table", { className: "w-full text-sm", children: [
3083
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
3084
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Delivery" }),
3085
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
3086
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" })
3087
+ ] }) }),
3088
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("tr", { className: "border-b border-border", children: [
3089
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "py-2 pe-4 font-mono text-foreground", children: row.id }),
3090
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "px-4 py-2 text-muted-foreground", children: row.status }),
3091
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "px-4 py-2 text-muted-foreground", children: (0, import_react_ui26.formatDateTime)(row.createdAt) })
3092
+ ] }, row.id)) })
3093
+ ] });
3094
+ }
3095
+ function WebhookEndpointManager({
3096
+ basePath = "/api",
3097
+ className
3098
+ }) {
3099
+ const toast = (0, import_react_ui26.useToast)();
3100
+ const [url, setUrl] = (0, import_react16.useState)("");
3101
+ const [events, setEvents] = (0, import_react16.useState)([]);
3102
+ const [active, setActive] = (0, import_react16.useState)(true);
3103
+ const [urlError, setUrlError] = (0, import_react16.useState)();
3104
+ const [expandedId, setExpandedId] = (0, import_react16.useState)(null);
3105
+ const { data, isLoading, isError, refetch } = (0, import_react_query24.useApiQuery)(["webhook-endpoints"], (client) => client.get(`${basePath}/webhook-endpoints`));
3106
+ const create = (0, import_react_query24.useApiMutation)(
3107
+ (client, payload) => client.post(`${basePath}/webhook-endpoints`, payload),
3108
+ {
3109
+ invalidates: [["webhook-endpoints"]],
3110
+ onSuccess: () => {
3111
+ toast.success("Endpoint created");
3112
+ setUrl("");
3113
+ setEvents([]);
3114
+ setActive(true);
3115
+ void refetch();
3116
+ },
3117
+ onError: (error) => toastError(toast, error)
3118
+ }
3119
+ );
3120
+ const toggle = (0, import_react_query24.useApiMutation)(
3121
+ (client, payload) => client.patch(`${basePath}/webhook-endpoints/${payload.id}`, { active: payload.active }),
3122
+ {
3123
+ invalidates: [["webhook-endpoints"]],
3124
+ onSuccess: () => {
3125
+ toast.success("Endpoint updated");
3126
+ void refetch();
3127
+ },
3128
+ onError: (error) => toastError(toast, error)
3129
+ }
3130
+ );
3131
+ const remove = (0, import_react_query24.useApiMutation)(
3132
+ (client, id) => client.delete(`${basePath}/webhook-endpoints/${id}`),
3133
+ {
3134
+ invalidates: [["webhook-endpoints"]],
3135
+ onSuccess: () => {
3136
+ toast.success("Endpoint deleted");
3137
+ void refetch();
3138
+ },
3139
+ onError: (error) => toastError(toast, error)
3140
+ }
3141
+ );
3142
+ const toggleEvent = (event) => {
3143
+ setEvents(
3144
+ (prev) => prev.includes(event) ? prev.filter((e) => e !== event) : [...prev, event]
3145
+ );
3146
+ };
3147
+ const onCreate = (event) => {
3148
+ event.preventDefault();
3149
+ const trimmed = url.trim();
3150
+ if (!trimmed) {
3151
+ setUrlError("URL is required");
3152
+ return;
3153
+ }
3154
+ if (!trimmed.startsWith("http")) {
3155
+ setUrlError("URL must start with http");
3156
+ return;
3157
+ }
3158
+ setUrlError(void 0);
3159
+ create.mutate({ url: trimmed, events, active });
3160
+ };
3161
+ const onDelete = (id) => {
3162
+ if (window.confirm("Delete this webhook endpoint?")) {
3163
+ remove.mutate(id);
3164
+ }
3165
+ };
3166
+ const createForm = /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("form", { onSubmit: onCreate, className: "flex flex-col gap-3", noValidate: true, children: [
3167
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex flex-col gap-1", children: [
3168
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("label", { htmlFor: "webhook-url", className: "text-sm font-medium text-foreground", children: "Endpoint URL" }),
3169
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3170
+ "input",
3171
+ {
3172
+ id: "webhook-url",
3173
+ type: "text",
3174
+ value: url,
3175
+ "aria-invalid": urlError ? "true" : void 0,
3176
+ "aria-describedby": urlError ? "webhook-url-error" : void 0,
3177
+ onChange: (e) => setUrl(e.target.value),
3178
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3179
+ }
3180
+ ),
3181
+ urlError && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { id: "webhook-url-error", className: "text-xs text-destructive", children: urlError })
3182
+ ] }),
3183
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("fieldset", { className: "flex flex-col gap-2", children: [
3184
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("legend", { className: "text-sm font-medium text-foreground", children: "Events" }),
3185
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "flex flex-wrap gap-3", children: EVENT_TYPES.map((evt) => {
3186
+ const id = `webhook-event-${evt}`;
3187
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
3188
+ "label",
3189
+ {
3190
+ htmlFor: id,
3191
+ className: "flex items-center gap-2 text-sm text-foreground",
3192
+ children: [
3193
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3194
+ "input",
3195
+ {
3196
+ id,
3197
+ type: "checkbox",
3198
+ checked: events.includes(evt),
3199
+ onChange: () => toggleEvent(evt),
3200
+ className: "focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3201
+ }
3202
+ ),
3203
+ evt
3204
+ ]
3205
+ },
3206
+ evt
3207
+ );
3208
+ }) })
3209
+ ] }),
3210
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("label", { htmlFor: "webhook-active", className: "flex items-center gap-2 text-sm text-foreground", children: [
3211
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3212
+ "input",
3213
+ {
3214
+ id: "webhook-active",
3215
+ type: "checkbox",
3216
+ checked: active,
3217
+ onChange: (e) => setActive(e.target.checked),
3218
+ className: "focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3219
+ }
3220
+ ),
3221
+ "Active"
3222
+ ] }),
3223
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3224
+ "button",
3225
+ {
3226
+ type: "submit",
3227
+ disabled: create.isPending,
3228
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3229
+ children: create.isPending ? "Creating\u2026" : "Create endpoint"
3230
+ }
3231
+ ) })
3232
+ ] });
3233
+ let body;
3234
+ if (isLoading) {
3235
+ body = /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { role: "status", "aria-label": "Loading webhook endpoints", className: "flex flex-col gap-2 p-4", children: [
3236
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "sr-only", children: "Loading webhook endpoints" }),
3237
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
3238
+ ] });
3239
+ } else if (isError) {
3240
+ body = /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex flex-col items-start gap-3 p-4", children: [
3241
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load webhook endpoints" }),
3242
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3243
+ "button",
3244
+ {
3245
+ type: "button",
3246
+ onClick: () => void refetch(),
3247
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3248
+ children: "Try again"
3249
+ }
3250
+ )
3251
+ ] });
3252
+ } else {
3253
+ const rows = normalizeEndpoints(data);
3254
+ if (rows.length === 0) {
3255
+ body = /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "No webhook endpoints" });
3256
+ } else {
3257
+ body = /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("table", { className: "w-full text-sm", children: [
3258
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
3259
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "URL" }),
3260
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Events" }),
3261
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Active" }),
3262
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" }),
3263
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { className: "sr-only", children: "Actions" }) })
3264
+ ] }) }),
3265
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("tbody", { children: rows.map((row) => {
3266
+ const isExpanded = expandedId === row.id;
3267
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_react16.Fragment, { children: [
3268
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("tr", { className: "border-b border-border", children: [
3269
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "py-3 pe-4 font-mono text-foreground", children: row.url }),
3270
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.events.length > 0 ? row.events.join(", ") : "\u2014" }),
3271
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react_ui26.StatusBadge, { variant: row.active ? "success" : "neutral", appearance: "dot", children: row.active ? "active" : "inactive" }) }),
3272
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui26.formatDateTime)(row.createdAt) }),
3273
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { className: "flex items-center gap-2", children: [
3274
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3275
+ "button",
3276
+ {
3277
+ type: "button",
3278
+ disabled: toggle.isPending,
3279
+ onClick: () => toggle.mutate({ id: row.id, active: !row.active }),
3280
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3281
+ children: row.active ? "Disable" : "Enable"
3282
+ }
3283
+ ),
3284
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3285
+ "button",
3286
+ {
3287
+ type: "button",
3288
+ onClick: () => setExpandedId(isExpanded ? null : row.id),
3289
+ "aria-expanded": isExpanded,
3290
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3291
+ children: "Deliveries"
3292
+ }
3293
+ ),
3294
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
3295
+ "button",
3296
+ {
3297
+ type: "button",
3298
+ disabled: remove.isPending,
3299
+ onClick: () => onDelete(row.id),
3300
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3301
+ children: "Delete"
3302
+ }
3303
+ )
3304
+ ] }) })
3305
+ ] }),
3306
+ isExpanded && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("tr", { className: "border-b border-border", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("td", { colSpan: 5, className: "bg-muted px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(WebhookDeliveries, { endpointId: row.id, basePath }) }) })
3307
+ ] }, row.id);
3308
+ }) })
3309
+ ] });
3310
+ }
3311
+ }
3312
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("section", { "aria-label": "Webhook endpoints", className: (0, import_react_ui26.cn)("flex flex-col gap-4", className), children: [
3313
+ createForm,
3314
+ body
3315
+ ] });
3316
+ }
3317
+
3318
+ // src/operations-overview.tsx
3319
+ var import_react_ui27 = require("@quanticjs/react-ui");
3320
+ var import_react_query25 = require("@quanticjs/react-query");
3321
+ var import_jsx_runtime28 = require("react/jsx-runtime");
3322
+ function OperationsOverview({ basePath = "/api", className }) {
3323
+ const { data, isLoading, isError, refetch } = (0, import_react_query25.useApiQuery)(
3324
+ ["operations-overview"],
3325
+ (client) => client.get(`${basePath}/v1/admin/overview`)
3326
+ );
3327
+ if (isLoading) {
3328
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
3329
+ "div",
3330
+ {
3331
+ role: "status",
3332
+ "aria-label": "Loading operations overview",
3333
+ className: (0, import_react_ui27.cn)("flex flex-col gap-2 p-4", className),
3334
+ children: [
3335
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "sr-only", children: "Loading operations overview" }),
3336
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { "aria-hidden": "true", className: "h-20 animate-pulse rounded bg-muted" }, i))
3337
+ ]
3338
+ }
3339
+ );
3340
+ }
3341
+ if (isError) {
3342
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: (0, import_react_ui27.cn)("flex flex-col items-start gap-3 p-4", className), children: [
3343
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load operations overview" }),
3344
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
3345
+ "button",
3346
+ {
3347
+ type: "button",
3348
+ onClick: () => void refetch(),
3349
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3350
+ children: "Try again"
3351
+ }
3352
+ )
3353
+ ] });
3354
+ }
3355
+ if (!data) {
3356
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: (0, import_react_ui27.cn)("p-6 text-center text-sm text-muted-foreground", className), children: "No overview data" });
3357
+ }
3358
+ const windowHours = data.windowHours;
3359
+ const channels = data.channels ?? [];
3360
+ const dlqPending = data.dlqPending;
3361
+ const cards = [
3362
+ { label: `Sends (${windowHours}h)`, value: data.totalSends },
3363
+ { label: `Delivered (${windowHours}h)`, value: data.totalDelivered },
3364
+ { label: `Failed (${windowHours}h)`, value: data.totalFailed }
3365
+ ];
3366
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("section", { "aria-label": "Operations overview", className: (0, import_react_ui27.cn)("flex flex-col gap-4", className), children: [
3367
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: cards.map((card) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "rounded-md border border-border bg-card p-4", children: [
3368
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "text-xs font-medium text-muted-foreground", children: card.label }),
3369
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("p", { className: "mt-1 text-2xl font-semibold text-foreground", children: card.value })
3370
+ ] }, card.label)) }),
3371
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex flex-wrap items-center gap-4", children: [
3372
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex items-center gap-2", children: [
3373
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "text-sm text-muted-foreground", children: "DLQ pending" }),
3374
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_ui27.StatusBadge, { variant: dlqPending > 0 ? "destructive" : "success", children: dlqPending })
3375
+ ] }),
3376
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex items-center gap-2", children: [
3377
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "text-sm text-muted-foreground", children: "Broadcasts in flight" }),
3378
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "text-sm font-medium text-foreground", children: data.broadcastsInFlight })
3379
+ ] }),
3380
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { className: "flex items-center gap-2", children: [
3381
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("span", { className: "text-sm text-muted-foreground", children: "Queue" }),
3382
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(import_react_ui27.StatusBadge, { variant: data.queueHealthy ? "success" : "destructive", children: data.queueHealthy ? "Healthy" : "Unhealthy" })
3383
+ ] })
3384
+ ] }),
3385
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("table", { className: "w-full text-sm", children: [
3386
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
3387
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Channel" }),
3388
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Sends" }),
3389
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Delivered" }),
3390
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Failed" })
3391
+ ] }) }),
3392
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("tbody", { children: channels.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("tr", { className: "border-b border-border", children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("td", { colSpan: 4, className: "py-3 text-center text-muted-foreground", children: "No channel activity" }) }) : channels.map((row) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("tr", { className: "border-b border-border", children: [
3393
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("td", { className: "py-3 pe-4 text-foreground", children: row.channel }),
3394
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.sends }),
3395
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.delivered }),
3396
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.failed })
3397
+ ] }, row.channel)) })
3398
+ ] }),
3399
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("p", { className: "text-xs text-muted-foreground", children: [
3400
+ "Generated ",
3401
+ (0, import_react_ui27.formatDateTime)(data.generatedAt)
3402
+ ] })
3403
+ ] });
3404
+ }
3405
+
3406
+ // src/delivery-log-explorer.tsx
3407
+ var import_react17 = require("react");
3408
+ var import_react_ui28 = require("@quanticjs/react-ui");
3409
+ var import_react_query26 = require("@quanticjs/react-query");
3410
+ var import_jsx_runtime29 = require("react/jsx-runtime");
3411
+ var LIMIT6 = 20;
3412
+ var EMPTY_FILTERS = {
3413
+ channel: "",
3414
+ status: "",
3415
+ recipient: "",
3416
+ userId: "",
3417
+ from: "",
3418
+ to: ""
3419
+ };
3420
+ function channelVariant(channel) {
3421
+ switch (channel) {
3422
+ case "email":
3423
+ return "neutral";
3424
+ case "sms":
3425
+ return "warning";
3426
+ default:
3427
+ return "neutral";
3428
+ }
3429
+ }
3430
+ function DeliveryLogExplorer({ basePath = "/api", className }) {
3431
+ const [page, setPage] = (0, import_react17.useState)(1);
3432
+ const [draft, setDraft] = (0, import_react17.useState)(EMPTY_FILTERS);
3433
+ const [applied, setApplied] = (0, import_react17.useState)(EMPTY_FILTERS);
3434
+ const queryString = (() => {
3435
+ const params = new URLSearchParams();
3436
+ params.set("page", String(page));
3437
+ params.set("limit", String(LIMIT6));
3438
+ if (applied.channel) params.set("channel", applied.channel);
3439
+ if (applied.status) params.set("status", applied.status);
3440
+ if (applied.userId) params.set("userId", applied.userId);
3441
+ if (applied.recipient) params.set("recipient", applied.recipient);
3442
+ if (applied.from) params.set("from", applied.from);
3443
+ if (applied.to) params.set("to", applied.to);
3444
+ return params.toString();
3445
+ })();
3446
+ const { data, isLoading, isError, refetch } = (0, import_react_query26.useApiQuery)(
3447
+ ["delivery-logs", page, applied],
3448
+ (client) => client.get(`${basePath}/v1/admin/delivery-logs?${queryString}`)
3449
+ );
3450
+ const onApply = (event) => {
3451
+ event.preventDefault();
3452
+ setPage(1);
3453
+ setApplied({
3454
+ channel: draft.channel,
3455
+ status: draft.status.trim(),
3456
+ recipient: draft.recipient.trim(),
3457
+ userId: draft.userId.trim(),
3458
+ from: draft.from,
3459
+ to: draft.to
3460
+ });
3461
+ };
3462
+ const setField = (key, value) => setDraft((prev) => ({ ...prev, [key]: value }));
3463
+ const filterForm = /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("form", { onSubmit: onApply, className: "flex flex-wrap items-end gap-3", noValidate: true, children: [
3464
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col gap-1", children: [
3465
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: "dle-channel", className: "text-sm font-medium text-foreground", children: "Channel" }),
3466
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
3467
+ "select",
3468
+ {
3469
+ id: "dle-channel",
3470
+ value: draft.channel,
3471
+ onChange: (e) => setField("channel", e.target.value),
3472
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3473
+ children: [
3474
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("option", { value: "", children: "All" }),
3475
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("option", { value: "email", children: "Email" }),
3476
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("option", { value: "sms", children: "SMS" })
3477
+ ]
3478
+ }
3479
+ )
3480
+ ] }),
3481
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col gap-1", children: [
3482
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: "dle-status", className: "text-sm font-medium text-foreground", children: "Status" }),
3483
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3484
+ "input",
3485
+ {
3486
+ id: "dle-status",
3487
+ type: "text",
3488
+ value: draft.status,
3489
+ onChange: (e) => setField("status", e.target.value),
3490
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3491
+ }
3492
+ )
3493
+ ] }),
3494
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col gap-1", children: [
3495
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: "dle-recipient", className: "text-sm font-medium text-foreground", children: "Recipient" }),
3496
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3497
+ "input",
3498
+ {
3499
+ id: "dle-recipient",
3500
+ type: "text",
3501
+ value: draft.recipient,
3502
+ onChange: (e) => setField("recipient", e.target.value),
3503
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3504
+ }
3505
+ )
3506
+ ] }),
3507
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col gap-1", children: [
3508
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: "dle-user", className: "text-sm font-medium text-foreground", children: "User ID" }),
3509
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3510
+ "input",
3511
+ {
3512
+ id: "dle-user",
3513
+ type: "text",
3514
+ value: draft.userId,
3515
+ onChange: (e) => setField("userId", e.target.value),
3516
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3517
+ }
3518
+ )
3519
+ ] }),
3520
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col gap-1", children: [
3521
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: "dle-from", className: "text-sm font-medium text-foreground", children: "From" }),
3522
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3523
+ "input",
3524
+ {
3525
+ id: "dle-from",
3526
+ type: "date",
3527
+ value: draft.from,
3528
+ onChange: (e) => setField("from", e.target.value),
3529
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3530
+ }
3531
+ )
3532
+ ] }),
3533
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col gap-1", children: [
3534
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("label", { htmlFor: "dle-to", className: "text-sm font-medium text-foreground", children: "To" }),
3535
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3536
+ "input",
3537
+ {
3538
+ id: "dle-to",
3539
+ type: "date",
3540
+ value: draft.to,
3541
+ onChange: (e) => setField("to", e.target.value),
3542
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3543
+ }
3544
+ )
3545
+ ] }),
3546
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3547
+ "button",
3548
+ {
3549
+ type: "submit",
3550
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3551
+ children: "Apply"
3552
+ }
3553
+ )
3554
+ ] });
3555
+ let body;
3556
+ if (isLoading) {
3557
+ body = /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { role: "status", "aria-label": "Loading delivery logs", className: "flex flex-col gap-2 p-4", children: [
3558
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { className: "sr-only", children: "Loading delivery logs" }),
3559
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
3560
+ ] });
3561
+ } else if (isError) {
3562
+ body = /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { className: "flex flex-col items-start gap-3 p-4", children: [
3563
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load delivery logs" }),
3564
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3565
+ "button",
3566
+ {
3567
+ type: "button",
3568
+ onClick: () => void refetch(),
3569
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3570
+ children: "Try again"
3571
+ }
3572
+ )
3573
+ ] });
3574
+ } else {
3575
+ const rows = data?.items ?? [];
3576
+ const totalPages = data?.totalPages ?? 1;
3577
+ if (rows.length === 0) {
3578
+ body = /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "No delivery logs" });
3579
+ } else {
3580
+ body = /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
3581
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("table", { className: "w-full text-sm", children: [
3582
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
3583
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Channel" }),
3584
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Recipient" }),
3585
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "User" }),
3586
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
3587
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Provider" }),
3588
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Attempts" }),
3589
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" })
3590
+ ] }) }),
3591
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("tr", { className: "border-b border-border", children: [
3592
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("td", { className: "py-3 pe-4", children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_react_ui28.StatusBadge, { variant: channelVariant(row.channel), appearance: "dot", children: row.channel }) }),
3593
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.recipient ?? "\u2014" }),
3594
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("td", { className: "px-4 py-3 font-mono text-foreground", children: row.userId }),
3595
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("td", { className: "px-4 py-3 text-foreground", children: row.status }),
3596
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.provider ?? "\u2014" }),
3597
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.attempts }),
3598
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui28.formatDateTime)(row.createdAt) })
3599
+ ] }, row.id)) })
3600
+ ] }),
3601
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("nav", { "aria-label": "Delivery log pagination", className: "flex items-center justify-between", children: [
3602
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3603
+ "button",
3604
+ {
3605
+ type: "button",
3606
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
3607
+ disabled: page <= 1,
3608
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3609
+ children: "Previous"
3610
+ }
3611
+ ),
3612
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { className: "text-xs text-muted-foreground", children: [
3613
+ "Page ",
3614
+ page,
3615
+ " of ",
3616
+ totalPages
3617
+ ] }),
3618
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
3619
+ "button",
3620
+ {
3621
+ type: "button",
3622
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
3623
+ disabled: page >= totalPages,
3624
+ className: "rounded-md border border-border px-3 py-1 text-sm text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3625
+ children: "Next"
3626
+ }
3627
+ )
3628
+ ] })
3629
+ ] });
3630
+ }
3631
+ }
3632
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("section", { "aria-label": "Delivery log explorer", className: (0, import_react_ui28.cn)("flex flex-col gap-4", className), children: [
3633
+ filterForm,
3634
+ body
3635
+ ] });
3636
+ }
3637
+
3638
+ // src/quiet-hours-form.tsx
3639
+ var import_react18 = require("react");
3640
+ var import_react_ui29 = require("@quanticjs/react-ui");
3641
+ var import_react_query27 = require("@quanticjs/react-query");
3642
+ var import_jsx_runtime30 = require("react/jsx-runtime");
3643
+ var DEFAULTS = {
3644
+ enabled: false,
3645
+ start: "22:00",
3646
+ end: "08:00",
3647
+ timezone: "UTC"
3648
+ };
3649
+ function normalize5(raw) {
3650
+ const obj = raw ?? {};
3651
+ return {
3652
+ enabled: typeof obj.enabled === "boolean" ? obj.enabled : DEFAULTS.enabled,
3653
+ start: typeof obj.start === "string" ? obj.start : typeof obj.startHour === "string" ? obj.startHour : DEFAULTS.start,
3654
+ end: typeof obj.end === "string" ? obj.end : typeof obj.endHour === "string" ? obj.endHour : DEFAULTS.end,
3655
+ timezone: typeof obj.timezone === "string" ? obj.timezone : DEFAULTS.timezone
3656
+ };
3657
+ }
3658
+ function QuietHoursForm({ basePath = "/api", className }) {
3659
+ const toast = (0, import_react_ui29.useToast)();
3660
+ const url = `${basePath}/notifications/config/quiet-hours`;
3661
+ const { data, isLoading, isError, refetch } = (0, import_react_query27.useApiQuery)(
3662
+ ["quiet-hours"],
3663
+ (client) => client.get(url)
3664
+ );
3665
+ const [form, setForm] = (0, import_react18.useState)(DEFAULTS);
3666
+ (0, import_react18.useEffect)(() => {
3667
+ if (data !== void 0) {
3668
+ setForm(normalize5(data));
3669
+ }
3670
+ }, [data]);
3671
+ const save = (0, import_react_query27.useApiMutation)((client, payload) => client.put(url, payload), {
3672
+ invalidates: [["quiet-hours"]],
3673
+ onSuccess: () => toast.success("Quiet hours saved"),
3674
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
3675
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
3676
+ })
3677
+ });
3678
+ const onSubmit = (event) => {
3679
+ event.preventDefault();
3680
+ save.mutate(form);
3681
+ };
3682
+ if (isLoading) {
3683
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
3684
+ "div",
3685
+ {
3686
+ role: "status",
3687
+ "aria-label": "Loading quiet hours",
3688
+ className: (0, import_react_ui29.cn)("flex flex-col gap-2 p-4", className),
3689
+ children: [
3690
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { className: "sr-only", children: "Loading quiet hours" }),
3691
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
3692
+ ]
3693
+ }
3694
+ );
3695
+ }
3696
+ if (isError) {
3697
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: (0, import_react_ui29.cn)("flex flex-col items-start gap-3 p-4", className), children: [
3698
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load quiet hours" }),
3699
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3700
+ "button",
3701
+ {
3702
+ type: "button",
3703
+ onClick: () => void refetch(),
3704
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3705
+ children: "Try again"
3706
+ }
3707
+ )
3708
+ ] });
3709
+ }
3710
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("form", { onSubmit, className: (0, import_react_ui29.cn)("flex flex-col gap-4", className), noValidate: true, children: [
3711
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: "Quiet hours" }),
3712
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("label", { className: "flex items-center gap-2 text-sm text-foreground", children: [
3713
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3714
+ "input",
3715
+ {
3716
+ type: "checkbox",
3717
+ checked: form.enabled,
3718
+ onChange: (e) => setForm((prev) => ({ ...prev, enabled: e.target.checked })),
3719
+ className: "focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3720
+ }
3721
+ ),
3722
+ "Enable quiet hours"
3723
+ ] }),
3724
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-wrap gap-4", children: [
3725
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-col gap-1", children: [
3726
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("label", { htmlFor: "qh-start", className: "text-sm font-medium text-foreground", children: "Start" }),
3727
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3728
+ "input",
3729
+ {
3730
+ id: "qh-start",
3731
+ type: "time",
3732
+ value: form.start,
3733
+ onChange: (e) => setForm((prev) => ({ ...prev, start: e.target.value })),
3734
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3735
+ }
3736
+ )
3737
+ ] }),
3738
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-col gap-1", children: [
3739
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("label", { htmlFor: "qh-end", className: "text-sm font-medium text-foreground", children: "End" }),
3740
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3741
+ "input",
3742
+ {
3743
+ id: "qh-end",
3744
+ type: "time",
3745
+ value: form.end,
3746
+ onChange: (e) => setForm((prev) => ({ ...prev, end: e.target.value })),
3747
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3748
+ }
3749
+ )
3750
+ ] }),
3751
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { className: "flex flex-col gap-1", children: [
3752
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("label", { htmlFor: "qh-tz", className: "text-sm font-medium text-foreground", children: "Timezone" }),
3753
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3754
+ "input",
3755
+ {
3756
+ id: "qh-tz",
3757
+ type: "text",
3758
+ value: form.timezone,
3759
+ onChange: (e) => setForm((prev) => ({ ...prev, timezone: e.target.value })),
3760
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3761
+ }
3762
+ )
3763
+ ] })
3764
+ ] }),
3765
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
3766
+ "button",
3767
+ {
3768
+ type: "submit",
3769
+ disabled: save.isPending,
3770
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3771
+ children: save.isPending ? "Saving\u2026" : "Save"
3772
+ }
3773
+ ) })
3774
+ ] });
3775
+ }
3776
+
3777
+ // src/frequency-cap-table.tsx
3778
+ var import_react19 = require("react");
3779
+ var import_react_ui30 = require("@quanticjs/react-ui");
3780
+ var import_react_query28 = require("@quanticjs/react-query");
3781
+ var import_jsx_runtime31 = require("react/jsx-runtime");
3782
+ function normalize6(raw) {
3783
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw?.caps) ? raw.caps : [];
3784
+ return list.map((entry) => {
3785
+ const obj = entry ?? {};
3786
+ const max = typeof obj.maxPerDay === "number" ? obj.maxPerDay : typeof obj.limit === "number" ? obj.limit : typeof obj.perDay === "number" ? obj.perDay : 0;
3787
+ return { type: typeof obj.type === "string" ? obj.type : "", maxPerDay: max };
3788
+ });
3789
+ }
3790
+ function FrequencyCapTable({ basePath = "/api", className }) {
3791
+ const toast = (0, import_react_ui30.useToast)();
3792
+ const url = `${basePath}/notifications/config/frequency-cap`;
3793
+ const { data, isLoading, isError, refetch } = (0, import_react_query28.useApiQuery)(
3794
+ ["frequency-cap"],
3795
+ (client) => client.get(url)
3796
+ );
3797
+ const [caps, setCaps] = (0, import_react19.useState)([]);
3798
+ const [newType, setNewType] = (0, import_react19.useState)("");
3799
+ const [newMax, setNewMax] = (0, import_react19.useState)(0);
3800
+ (0, import_react19.useEffect)(() => {
3801
+ if (data !== void 0) {
3802
+ setCaps(normalize6(data));
3803
+ }
3804
+ }, [data]);
3805
+ const save = (0, import_react_query28.useApiMutation)(
3806
+ (client, payload) => client.put(url, payload),
3807
+ {
3808
+ invalidates: [["frequency-cap"]],
3809
+ onSuccess: () => toast.success("Frequency caps saved"),
3810
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
3811
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
3812
+ })
3813
+ }
3814
+ );
3815
+ const updateRow = (index, value) => setCaps((prev) => prev.map((cap, i) => i === index ? { ...cap, maxPerDay: value } : cap));
3816
+ const removeRow = (index) => setCaps((prev) => prev.filter((_, i) => i !== index));
3817
+ const addRow = (event) => {
3818
+ event.preventDefault();
3819
+ const type = newType.trim();
3820
+ if (!type) return;
3821
+ setCaps((prev) => [...prev, { type, maxPerDay: newMax }]);
3822
+ setNewType("");
3823
+ setNewMax(0);
3824
+ };
3825
+ if (isLoading) {
3826
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
3827
+ "div",
3828
+ {
3829
+ role: "status",
3830
+ "aria-label": "Loading frequency caps",
3831
+ className: (0, import_react_ui30.cn)("flex flex-col gap-2 p-4", className),
3832
+ children: [
3833
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "sr-only", children: "Loading frequency caps" }),
3834
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
3835
+ ]
3836
+ }
3837
+ );
3838
+ }
3839
+ if (isError) {
3840
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: (0, import_react_ui30.cn)("flex flex-col items-start gap-3 p-4", className), children: [
3841
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load frequency caps" }),
3842
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
3843
+ "button",
3844
+ {
3845
+ type: "button",
3846
+ onClick: () => void refetch(),
3847
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3848
+ children: "Try again"
3849
+ }
3850
+ )
3851
+ ] });
3852
+ }
3853
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("section", { "aria-label": "Frequency caps", className: (0, import_react_ui30.cn)("flex flex-col gap-4", className), children: [
3854
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: "Frequency caps" }),
3855
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("table", { className: "w-full text-sm", children: [
3856
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
3857
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Type" }),
3858
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Max per day" }),
3859
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: "sr-only", children: "Actions" }) })
3860
+ ] }) }),
3861
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("tbody", { children: caps.map((cap, index) => {
3862
+ const inputId = `cap-${index}`;
3863
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("tr", { className: "border-b border-border", children: [
3864
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("td", { className: "py-3 pe-4 text-foreground", children: cap.type }),
3865
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("td", { className: "px-4 py-3", children: [
3866
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("label", { htmlFor: inputId, className: "sr-only", children: [
3867
+ "Max per day for ",
3868
+ cap.type
3869
+ ] }),
3870
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
3871
+ "input",
3872
+ {
3873
+ id: inputId,
3874
+ type: "number",
3875
+ min: 0,
3876
+ value: cap.maxPerDay,
3877
+ onChange: (e) => updateRow(index, Number(e.target.value)),
3878
+ className: "w-24 rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3879
+ }
3880
+ )
3881
+ ] }),
3882
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
3883
+ "button",
3884
+ {
3885
+ type: "button",
3886
+ onClick: () => removeRow(index),
3887
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3888
+ children: "Remove"
3889
+ }
3890
+ ) })
3891
+ ] }, `${cap.type}-${index}`);
3892
+ }) })
3893
+ ] }),
3894
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("form", { onSubmit: addRow, className: "flex flex-wrap items-end gap-3", noValidate: true, children: [
3895
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex flex-col gap-1", children: [
3896
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("label", { htmlFor: "cap-new-type", className: "text-sm font-medium text-foreground", children: "New type" }),
3897
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
3898
+ "input",
3899
+ {
3900
+ id: "cap-new-type",
3901
+ type: "text",
3902
+ value: newType,
3903
+ onChange: (e) => setNewType(e.target.value),
3904
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3905
+ }
3906
+ )
3907
+ ] }),
3908
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex flex-col gap-1", children: [
3909
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("label", { htmlFor: "cap-new-max", className: "text-sm font-medium text-foreground", children: "Max per day" }),
3910
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
3911
+ "input",
3912
+ {
3913
+ id: "cap-new-max",
3914
+ type: "number",
3915
+ min: 0,
3916
+ value: newMax,
3917
+ onChange: (e) => setNewMax(Number(e.target.value)),
3918
+ className: "w-24 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
3919
+ }
3920
+ )
3921
+ ] }),
3922
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
3923
+ "button",
3924
+ {
3925
+ type: "submit",
3926
+ className: "rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3927
+ children: "Add"
3928
+ }
3929
+ )
3930
+ ] }),
3931
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
3932
+ "button",
3933
+ {
3934
+ type: "button",
3935
+ disabled: save.isPending,
3936
+ onClick: () => save.mutate({ caps }),
3937
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
3938
+ children: save.isPending ? "Saving\u2026" : "Save"
3939
+ }
3940
+ ) })
3941
+ ] });
3942
+ }
3943
+
3944
+ // src/tenant-config-form.tsx
3945
+ var import_react20 = require("react");
3946
+ var import_react_ui31 = require("@quanticjs/react-ui");
3947
+ var import_react_query29 = require("@quanticjs/react-query");
3948
+ var import_jsx_runtime32 = require("react/jsx-runtime");
3949
+ function normalize7(raw) {
3950
+ const obj = raw ?? {};
3951
+ const toList = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
3952
+ return {
3953
+ notificationTypes: toList(obj.notificationTypes),
3954
+ immediateEmailTypes: toList(obj.immediateEmailTypes)
3955
+ };
3956
+ }
3957
+ function TagEditor({ id, label, values, onAdd, onRemove }) {
3958
+ const [draft, setDraft] = (0, import_react20.useState)("");
3959
+ const add = (event) => {
3960
+ event.preventDefault();
3961
+ const value = draft.trim();
3962
+ if (!value) return;
3963
+ onAdd(value);
3964
+ setDraft("");
3965
+ };
3966
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex flex-col gap-2", children: [
3967
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-sm font-medium text-foreground", children: label }),
3968
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("ul", { className: "flex flex-wrap gap-2", children: values.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("li", { className: "text-sm text-muted-foreground", children: "None" }) : values.map((value) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
3969
+ "li",
3970
+ {
3971
+ className: "flex items-center gap-1 rounded-md border border-border bg-card px-2 py-1 text-sm text-foreground",
3972
+ children: [
3973
+ value,
3974
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
3975
+ "button",
3976
+ {
3977
+ type: "button",
3978
+ "aria-label": `Remove ${value}`,
3979
+ onClick: () => onRemove(value),
3980
+ className: "rounded text-muted-foreground hover:text-destructive focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
3981
+ children: "\xD7"
3982
+ }
3983
+ )
3984
+ ]
3985
+ },
3986
+ value
3987
+ )) }),
3988
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("form", { onSubmit: add, className: "flex items-end gap-2", noValidate: true, children: [
3989
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: "flex flex-col gap-1", children: [
3990
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("label", { htmlFor: id, className: "sr-only", children: [
3991
+ "Add to ",
3992
+ label
3993
+ ] }),
3994
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
3995
+ "input",
3996
+ {
3997
+ id,
3998
+ type: "text",
3999
+ value: draft,
4000
+ onChange: (e) => setDraft(e.target.value),
4001
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
4002
+ }
4003
+ )
4004
+ ] }),
4005
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4006
+ "button",
4007
+ {
4008
+ type: "submit",
4009
+ className: "rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
4010
+ children: "Add"
4011
+ }
4012
+ )
4013
+ ] })
4014
+ ] });
4015
+ }
4016
+ function TenantConfigForm({ basePath = "/api", className }) {
4017
+ const toast = (0, import_react_ui31.useToast)();
4018
+ const url = `${basePath}/admin/notification-config`;
4019
+ const { data, isLoading, isError, refetch } = (0, import_react_query29.useApiQuery)(
4020
+ ["tenant-config"],
4021
+ (client) => client.get(url)
4022
+ );
4023
+ const [config, setConfig] = (0, import_react20.useState)({
4024
+ notificationTypes: [],
4025
+ immediateEmailTypes: []
4026
+ });
4027
+ const [subsetError, setSubsetError] = (0, import_react20.useState)(void 0);
4028
+ (0, import_react20.useEffect)(() => {
4029
+ if (data !== void 0) {
4030
+ setConfig(normalize7(data));
4031
+ }
4032
+ }, [data]);
4033
+ const save = (0, import_react_query29.useApiMutation)(
4034
+ (client, payload) => client.put(url, payload),
4035
+ {
4036
+ invalidates: [["tenant-config"]],
4037
+ onSuccess: () => toast.success("Configuration saved"),
4038
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
4039
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
4040
+ })
4041
+ }
4042
+ );
4043
+ const addType = (value) => setConfig(
4044
+ (prev) => prev.notificationTypes.includes(value) ? prev : { ...prev, notificationTypes: [...prev.notificationTypes, value] }
4045
+ );
4046
+ const removeType = (value) => setConfig((prev) => ({
4047
+ ...prev,
4048
+ notificationTypes: prev.notificationTypes.filter((t) => t !== value)
4049
+ }));
4050
+ const addImmediate = (value) => setConfig(
4051
+ (prev) => prev.immediateEmailTypes.includes(value) ? prev : { ...prev, immediateEmailTypes: [...prev.immediateEmailTypes, value] }
4052
+ );
4053
+ const removeImmediate = (value) => setConfig((prev) => ({
4054
+ ...prev,
4055
+ immediateEmailTypes: prev.immediateEmailTypes.filter((t) => t !== value)
4056
+ }));
4057
+ const onSubmit = (event) => {
4058
+ event.preventDefault();
4059
+ const invalid = config.immediateEmailTypes.filter((t) => !config.notificationTypes.includes(t));
4060
+ if (invalid.length > 0) {
4061
+ setSubsetError(
4062
+ `Immediate email types must be a subset of notification types. Not allowed: ${invalid.join(", ")}`
4063
+ );
4064
+ return;
4065
+ }
4066
+ setSubsetError(void 0);
4067
+ save.mutate(config);
4068
+ };
4069
+ if (isLoading) {
4070
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
4071
+ "div",
4072
+ {
4073
+ role: "status",
4074
+ "aria-label": "Loading tenant configuration",
4075
+ className: (0, import_react_ui31.cn)("flex flex-col gap-2 p-4", className),
4076
+ children: [
4077
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "sr-only", children: "Loading tenant configuration" }),
4078
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
4079
+ ]
4080
+ }
4081
+ );
4082
+ }
4083
+ if (isError) {
4084
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { className: (0, import_react_ui31.cn)("flex flex-col items-start gap-3 p-4", className), children: [
4085
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load tenant configuration" }),
4086
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4087
+ "button",
4088
+ {
4089
+ type: "button",
4090
+ onClick: () => void refetch(),
4091
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
4092
+ children: "Try again"
4093
+ }
4094
+ )
4095
+ ] });
4096
+ }
4097
+ const errorId = "tenant-config-subset-error";
4098
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
4099
+ "form",
4100
+ {
4101
+ onSubmit,
4102
+ className: (0, import_react_ui31.cn)("flex flex-col gap-5", className),
4103
+ noValidate: true,
4104
+ "aria-invalid": subsetError ? "true" : void 0,
4105
+ "aria-describedby": subsetError ? errorId : void 0,
4106
+ children: [
4107
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: "Notification configuration" }),
4108
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4109
+ TagEditor,
4110
+ {
4111
+ id: "tc-notification-types",
4112
+ label: "Notification types",
4113
+ values: config.notificationTypes,
4114
+ onAdd: addType,
4115
+ onRemove: removeType
4116
+ }
4117
+ ),
4118
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4119
+ TagEditor,
4120
+ {
4121
+ id: "tc-immediate-email-types",
4122
+ label: "Immediate email types",
4123
+ values: config.immediateEmailTypes,
4124
+ onAdd: addImmediate,
4125
+ onRemove: removeImmediate
4126
+ }
4127
+ ),
4128
+ subsetError && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("p", { id: errorId, className: "text-xs text-destructive", children: subsetError }),
4129
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
4130
+ "button",
4131
+ {
4132
+ type: "submit",
4133
+ disabled: save.isPending,
4134
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
4135
+ children: save.isPending ? "Saving\u2026" : "Save"
4136
+ }
4137
+ ) })
4138
+ ]
4139
+ }
4140
+ );
4141
+ }
4142
+
4143
+ // src/tracking-config-form.tsx
4144
+ var import_react21 = require("react");
4145
+ var import_react_ui32 = require("@quanticjs/react-ui");
4146
+ var import_react_query30 = require("@quanticjs/react-query");
4147
+ var import_jsx_runtime33 = require("react/jsx-runtime");
4148
+ function normalize8(raw) {
4149
+ const obj = raw ?? {};
4150
+ const bool = (...keys) => {
4151
+ for (const key of keys) {
4152
+ if (typeof obj[key] === "boolean") return obj[key];
4153
+ }
4154
+ return false;
4155
+ };
4156
+ return {
4157
+ openTrackingEnabled: bool("openTrackingEnabled", "opensEnabled"),
4158
+ clickTrackingEnabled: bool("clickTrackingEnabled", "clicksEnabled")
4159
+ };
4160
+ }
4161
+ function TrackingConfigForm({ basePath = "/api", className }) {
4162
+ const toast = (0, import_react_ui32.useToast)();
4163
+ const url = `${basePath}/analytics/notifications/tracking-config`;
4164
+ const { data, isLoading, isError, refetch } = (0, import_react_query30.useApiQuery)(
4165
+ ["tracking-config"],
4166
+ (client) => client.get(url)
4167
+ );
4168
+ const [form, setForm] = (0, import_react21.useState)({
4169
+ openTrackingEnabled: false,
4170
+ clickTrackingEnabled: false
4171
+ });
4172
+ (0, import_react21.useEffect)(() => {
4173
+ if (data !== void 0) {
4174
+ setForm(normalize8(data));
4175
+ }
4176
+ }, [data]);
4177
+ const save = (0, import_react_query30.useApiMutation)(
4178
+ (client, payload) => client.post(url, payload),
4179
+ {
4180
+ invalidates: [["tracking-config"]],
4181
+ onSuccess: () => toast.success("Tracking configuration saved"),
4182
+ onError: (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
4183
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
4184
+ })
4185
+ }
4186
+ );
4187
+ const onSubmit = (event) => {
4188
+ event.preventDefault();
4189
+ save.mutate(form);
4190
+ };
4191
+ if (isLoading) {
4192
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
4193
+ "div",
4194
+ {
4195
+ role: "status",
4196
+ "aria-label": "Loading tracking configuration",
4197
+ className: (0, import_react_ui32.cn)("flex flex-col gap-2 p-4", className),
4198
+ children: [
4199
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("span", { className: "sr-only", children: "Loading tracking configuration" }),
4200
+ [0, 1].map((i) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
4201
+ ]
4202
+ }
4203
+ );
4204
+ }
4205
+ if (isError) {
4206
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("div", { className: (0, import_react_ui32.cn)("flex flex-col items-start gap-3 p-4", className), children: [
4207
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load tracking configuration" }),
4208
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
4209
+ "button",
4210
+ {
4211
+ type: "button",
4212
+ onClick: () => void refetch(),
4213
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
4214
+ children: "Try again"
4215
+ }
4216
+ )
4217
+ ] });
4218
+ }
4219
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("form", { onSubmit, className: (0, import_react_ui32.cn)("flex flex-col gap-4", className), noValidate: true, children: [
4220
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("h2", { className: "text-sm font-semibold text-foreground", children: "Tracking configuration" }),
4221
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("label", { className: "flex items-center gap-2 text-sm text-foreground", children: [
4222
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
4223
+ "input",
4224
+ {
4225
+ type: "checkbox",
4226
+ checked: form.openTrackingEnabled,
4227
+ onChange: (e) => setForm((prev) => ({ ...prev, openTrackingEnabled: e.target.checked })),
4228
+ className: "focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
4229
+ }
4230
+ ),
4231
+ "Open tracking"
4232
+ ] }),
4233
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)("label", { className: "flex items-center gap-2 text-sm text-foreground", children: [
4234
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
4235
+ "input",
4236
+ {
4237
+ type: "checkbox",
4238
+ checked: form.clickTrackingEnabled,
4239
+ onChange: (e) => setForm((prev) => ({ ...prev, clickTrackingEnabled: e.target.checked })),
4240
+ className: "focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
4241
+ }
4242
+ ),
4243
+ "Click tracking"
4244
+ ] }),
4245
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
4246
+ "button",
4247
+ {
4248
+ type: "submit",
4249
+ disabled: save.isPending,
4250
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
4251
+ children: save.isPending ? "Saving\u2026" : "Save"
4252
+ }
4253
+ ) })
4254
+ ] });
4255
+ }
4256
+
4257
+ // src/api-key-manager.tsx
4258
+ var import_react22 = require("react");
4259
+ var import_react_ui33 = require("@quanticjs/react-ui");
4260
+ var import_react_query31 = require("@quanticjs/react-query");
4261
+ var import_jsx_runtime34 = require("react/jsx-runtime");
4262
+ function normalize9(raw) {
4263
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw?.items) ? raw.items : [];
4264
+ return list;
4265
+ }
4266
+ function ApiKeyManager({ basePath = "/api", className }) {
4267
+ const toast = (0, import_react_ui33.useToast)();
4268
+ const url = `${basePath}/v1/admin/api-keys`;
4269
+ const { data, isLoading, isError, refetch } = (0, import_react_query31.useApiQuery)(
4270
+ ["api-keys"],
4271
+ (client) => client.get(url)
4272
+ );
4273
+ const [name, setName] = (0, import_react22.useState)("");
4274
+ const [applicationKey, setApplicationKey] = (0, import_react22.useState)("");
4275
+ const onMutationError = (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
4276
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
4277
+ });
4278
+ const create = (0, import_react_query31.useApiMutation)(
4279
+ (client, vars) => client.post(url, {
4280
+ name: vars.name,
4281
+ ...vars.applicationKey ? { applicationKey: vars.applicationKey } : {}
4282
+ }),
4283
+ {
4284
+ invalidates: [["api-keys"]],
4285
+ onSuccess: (result) => {
4286
+ const secret = result?.key ?? result?.secret;
4287
+ if (secret) {
4288
+ toast.success("API key created \u2014 copy it now", {
4289
+ description: `This secret is shown only once: ${secret}`
4290
+ });
4291
+ } else {
4292
+ toast.success("API key created");
4293
+ }
4294
+ setName("");
4295
+ setApplicationKey("");
4296
+ },
4297
+ onError: onMutationError
4298
+ }
4299
+ );
4300
+ const revoke = (0, import_react_query31.useApiMutation)((client, id) => client.delete(`${url}/${id}`), {
4301
+ invalidates: [["api-keys"]],
4302
+ onSuccess: () => {
4303
+ toast.success("API key revoked");
4304
+ void refetch();
4305
+ },
4306
+ onError: onMutationError
4307
+ });
4308
+ const onCreate = (event) => {
4309
+ event.preventDefault();
4310
+ const trimmed = name.trim();
4311
+ if (!trimmed) return;
4312
+ create.mutate({ name: trimmed, applicationKey: applicationKey.trim() || void 0 });
4313
+ };
4314
+ const onRevoke = (id) => {
4315
+ if (window.confirm("Revoke this API key? Applications using it will lose access.")) {
4316
+ revoke.mutate(id);
4317
+ }
4318
+ };
4319
+ const createForm = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("form", { onSubmit: onCreate, className: "flex flex-wrap items-end gap-3", noValidate: true, children: [
4320
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-col gap-1", children: [
4321
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("label", { htmlFor: "api-key-name", className: "text-sm font-medium text-foreground", children: "New key name" }),
4322
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
4323
+ "input",
4324
+ {
4325
+ id: "api-key-name",
4326
+ type: "text",
4327
+ value: name,
4328
+ onChange: (e) => setName(e.target.value),
4329
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
4330
+ }
4331
+ )
4332
+ ] }),
4333
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-col gap-1", children: [
4334
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("label", { htmlFor: "api-key-app", className: "text-sm font-medium text-foreground", children: "Application (optional)" }),
4335
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
4336
+ "input",
4337
+ {
4338
+ id: "api-key-app",
4339
+ type: "text",
4340
+ value: applicationKey,
4341
+ onChange: (e) => setApplicationKey(e.target.value),
4342
+ placeholder: "delivery-hub",
4343
+ className: "rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
4344
+ }
4345
+ )
4346
+ ] }),
4347
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
4348
+ "button",
4349
+ {
4350
+ type: "submit",
4351
+ disabled: create.isPending,
4352
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
4353
+ children: create.isPending ? "Creating\u2026" : "Create key"
4354
+ }
4355
+ )
4356
+ ] });
4357
+ let body;
4358
+ if (isLoading) {
4359
+ body = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { role: "status", "aria-label": "Loading API keys", className: "flex flex-col gap-2 p-4", children: [
4360
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "sr-only", children: "Loading API keys" }),
4361
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
4362
+ ] });
4363
+ } else if (isError) {
4364
+ body = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("div", { className: "flex flex-col items-start gap-3 p-4", children: [
4365
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load API keys" }),
4366
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
4367
+ "button",
4368
+ {
4369
+ type: "button",
4370
+ onClick: () => void refetch(),
4371
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
4372
+ children: "Try again"
4373
+ }
4374
+ )
4375
+ ] });
4376
+ } else {
4377
+ const rows = normalize9(data);
4378
+ if (rows.length === 0) {
4379
+ body = /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "No API keys" });
4380
+ } else {
4381
+ body = /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("table", { className: "w-full text-sm", children: [
4382
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
4383
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("th", { scope: "col", className: "py-2 pe-4 font-medium", children: "Name" }),
4384
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Prefix" }),
4385
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Application" }),
4386
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Status" }),
4387
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Created" }),
4388
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: "Last used" }),
4389
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("th", { scope: "col", className: "px-4 py-2 font-medium", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { className: "sr-only", children: "Actions" }) })
4390
+ ] }) }),
4391
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("tbody", { children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("tr", { className: "border-b border-border", children: [
4392
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("td", { className: "py-3 pe-4 text-foreground", children: row.name }),
4393
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("td", { className: "px-4 py-3 font-mono text-muted-foreground", children: row.prefix ?? "\u2014" }),
4394
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("td", { className: "px-4 py-3 font-mono text-muted-foreground", children: row.applicationKey ?? "\u2014" }),
4395
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_react_ui33.StatusBadge, { variant: row.revoked ? "destructive" : "success", children: row.revoked ? "Revoked" : "Active" }) }),
4396
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: (0, import_react_ui33.formatDateTime)(row.createdAt) }),
4397
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("td", { className: "px-4 py-3 text-muted-foreground", children: row.lastUsedAt ? (0, import_react_ui33.formatDateTime)(row.lastUsedAt) : "\u2014" }),
4398
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("td", { className: "px-4 py-3", children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
4399
+ "button",
4400
+ {
4401
+ type: "button",
4402
+ disabled: revoke.isPending || row.revoked,
4403
+ onClick: () => onRevoke(row.id),
4404
+ className: "rounded-md border border-border px-3 py-1 text-sm font-medium text-destructive hover:bg-muted focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
4405
+ children: "Revoke"
4406
+ }
4407
+ ) })
4408
+ ] }, row.id)) })
4409
+ ] });
4410
+ }
4411
+ }
4412
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)("section", { "aria-label": "API key management", className: (0, import_react_ui33.cn)("flex flex-col gap-4", className), children: [
4413
+ createForm,
4414
+ body
4415
+ ] });
4416
+ }
4417
+
4418
+ // src/application-registry-panel.tsx
4419
+ var import_react23 = require("react");
4420
+ var import_react_ui34 = require("@quanticjs/react-ui");
4421
+ var import_react_query32 = require("@quanticjs/react-query");
4422
+ var import_jsx_runtime35 = require("react/jsx-runtime");
4423
+ function normalize10(raw) {
4424
+ const list = Array.isArray(raw) ? raw : Array.isArray(raw?.items) ? raw.items : [];
4425
+ return list;
4426
+ }
4427
+ function ApplicationRegistryPanel({
4428
+ basePath = "/api",
4429
+ className
4430
+ }) {
4431
+ const toast = (0, import_react_ui34.useToast)();
4432
+ const url = `${basePath}/admin/applications`;
4433
+ const { data, isLoading, isError, refetch } = (0, import_react_query32.useApiQuery)(
4434
+ ["applications"],
4435
+ (client) => client.get(url)
4436
+ );
4437
+ const [key, setKey] = (0, import_react23.useState)("");
4438
+ const [displayName, setDisplayName] = (0, import_react23.useState)("");
4439
+ const [description, setDescription] = (0, import_react23.useState)("");
4440
+ const onMutationError = (error) => toast.error(error.isServerError ? "Something went wrong" : error.title, {
4441
+ description: error.isServerError ? `Please try again. (ref: ${error.correlationId ?? "unknown"})` : `${error.detail ?? ""} (ref: ${error.correlationId ?? "unknown"})`
4442
+ });
4443
+ const register = (0, import_react_query32.useApiMutation)((client, body) => client.post(url, body), {
4444
+ invalidates: [["applications"]],
4445
+ onSuccess: () => {
4446
+ toast.success("Application registered");
4447
+ setKey("");
4448
+ setDisplayName("");
4449
+ setDescription("");
4450
+ },
4451
+ onError: onMutationError
4452
+ });
4453
+ const setStatus = (0, import_react_query32.useApiMutation)((client, body) => client.patch(`${url}/${body.key}`, { status: body.status }), {
4454
+ invalidates: [["applications"]],
4455
+ onSuccess: () => {
4456
+ toast.success("Application updated");
4457
+ void refetch();
4458
+ },
4459
+ onError: onMutationError
4460
+ });
4461
+ const onRegister = (event) => {
4462
+ event.preventDefault();
4463
+ const k = key.trim();
4464
+ const name = displayName.trim();
4465
+ if (!k || !name) return;
4466
+ register.mutate({ key: k, displayName: name, description: description.trim() || void 0 });
4467
+ };
4468
+ const registerForm = /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("form", { onSubmit: onRegister, className: "flex flex-wrap items-end gap-3", noValidate: true, children: [
4469
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-col gap-1", children: [
4470
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("label", { htmlFor: "application-key", className: "text-sm font-medium text-foreground", children: "Key (slug)" }),
4471
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
4472
+ "input",
4473
+ {
4474
+ id: "application-key",
4475
+ type: "text",
4476
+ value: key,
4477
+ onChange: (e) => setKey(e.target.value),
4478
+ placeholder: "delivery-hub",
4479
+ className: "rounded border border-border bg-background px-3 py-1.5 text-sm text-foreground"
4480
+ }
4481
+ )
4482
+ ] }),
4483
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-col gap-1", children: [
4484
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("label", { htmlFor: "application-name", className: "text-sm font-medium text-foreground", children: "Display name" }),
4485
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
4486
+ "input",
4487
+ {
4488
+ id: "application-name",
4489
+ type: "text",
4490
+ value: displayName,
4491
+ onChange: (e) => setDisplayName(e.target.value),
4492
+ placeholder: "DeliveryHub",
4493
+ className: "rounded border border-border bg-background px-3 py-1.5 text-sm text-foreground"
4494
+ }
4495
+ )
4496
+ ] }),
4497
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "flex flex-col gap-1", children: [
4498
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("label", { htmlFor: "application-description", className: "text-sm font-medium text-foreground", children: "Description (optional)" }),
4499
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
4500
+ "input",
4501
+ {
4502
+ id: "application-description",
4503
+ type: "text",
4504
+ value: description,
4505
+ onChange: (e) => setDescription(e.target.value),
4506
+ className: "rounded border border-border bg-background px-3 py-1.5 text-sm text-foreground"
4507
+ }
4508
+ )
4509
+ ] }),
4510
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
4511
+ "button",
4512
+ {
4513
+ type: "submit",
4514
+ disabled: register.isPending,
4515
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
4516
+ children: "Register"
4517
+ }
4518
+ )
4519
+ ] });
4520
+ if (isLoading) {
4521
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
4522
+ "div",
4523
+ {
4524
+ role: "status",
4525
+ "aria-label": "Loading applications",
4526
+ className: (0, import_react_ui34.cn)("flex flex-col gap-2 p-4", className),
4527
+ children: [
4528
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("span", { className: "sr-only", children: "Loading applications" }),
4529
+ [0, 1, 2].map((i) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { "aria-hidden": "true", className: "h-10 animate-pulse rounded bg-muted" }, i))
4530
+ ]
4531
+ }
4532
+ );
4533
+ }
4534
+ if (isError) {
4535
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: (0, import_react_ui34.cn)("flex flex-col items-start gap-3 p-4", className), children: [
4536
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "text-sm text-foreground", children: "Failed to load applications" }),
4537
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
4538
+ "button",
4539
+ {
4540
+ type: "button",
4541
+ onClick: () => void refetch(),
4542
+ className: "rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring",
4543
+ children: "Try again"
4544
+ }
4545
+ )
4546
+ ] });
4547
+ }
4548
+ const apps = normalize10(data);
4549
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("section", { "aria-label": "Applications", className: (0, import_react_ui34.cn)("flex flex-col gap-4 p-4", className), children: [
4550
+ registerForm,
4551
+ apps.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("p", { className: "p-6 text-center text-sm text-muted-foreground", children: "No applications registered" }) : /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("table", { className: "w-full text-sm", children: [
4552
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("tr", { className: "border-b border-border text-start text-muted-foreground", children: [
4553
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("th", { className: "py-2 font-medium", children: "Key" }),
4554
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("th", { className: "py-2 font-medium", children: "Name" }),
4555
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("th", { className: "py-2 font-medium", children: "Status" }),
4556
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("th", { className: "py-2 font-medium", children: "Created" }),
4557
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("th", { className: "py-2 font-medium", children: "Actions" })
4558
+ ] }) }),
4559
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("tbody", { children: apps.map((app) => /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("tr", { className: "border-b border-border", children: [
4560
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("td", { className: "py-2 font-mono text-foreground", children: app.key }),
4561
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("td", { className: "py-2 text-foreground", children: app.displayName }),
4562
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("td", { className: "py-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_react_ui34.StatusBadge, { variant: app.status === "active" ? "success" : "neutral", children: app.status }) }),
4563
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("td", { className: "py-2 text-muted-foreground", children: (0, import_react_ui34.formatDateTime)(app.createdAt) }),
4564
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("td", { className: "py-2", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
4565
+ "button",
4566
+ {
4567
+ type: "button",
4568
+ disabled: setStatus.isPending,
4569
+ onClick: () => setStatus.mutate({
4570
+ key: app.key,
4571
+ status: app.status === "active" ? "disabled" : "active"
4572
+ }),
4573
+ className: "text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:opacity-50",
4574
+ children: app.status === "active" ? "Disable" : "Enable"
4575
+ }
4576
+ ) })
4577
+ ] }, app.id)) })
4578
+ ] })
4579
+ ] });
4580
+ }
4581
+ // Annotate the CommonJS export names for ESM import in node:
4582
+ 0 && (module.exports = {
4583
+ ApiKeyManager,
4584
+ ApplicationRegistryPanel,
4585
+ BroadcastComposer,
4586
+ BroadcastList,
4587
+ BroadcastProgress,
4588
+ CatalogEditor,
4589
+ DeliveryAnalyticsPage,
4590
+ DeliveryLogExplorer,
4591
+ DeliveryLogViewer,
4592
+ DlqConsole,
4593
+ FallbackReportPanel,
4594
+ FrequencyCapTable,
4595
+ FunnelStats,
4596
+ MissingTranslationsPanel,
4597
+ NotificationBell,
4598
+ NotificationInbox,
4599
+ NotificationPreferences,
4600
+ NotificationProvider,
4601
+ NotificationRealtimeProvider,
4602
+ OperationsOverview,
4603
+ QuietHoursForm,
4604
+ RecipientAdminPanel,
4605
+ SegmentBuilder,
4606
+ SegmentList,
4607
+ SuppressionManager,
4608
+ TemplateEditor,
4609
+ TemplateList,
4610
+ TemplatePreviewPane,
4611
+ TemplateStatusBadge,
4612
+ TemplateVersionHistory,
4613
+ TenantConfigForm,
4614
+ TrackingConfigForm,
4615
+ TrendChart,
4616
+ TypeTable,
4617
+ WebhookEndpointManager,
4618
+ useBroadcasts,
4619
+ useDeliveryAnalytics,
4620
+ useDeliveryTypes,
4621
+ useFunnelStats,
4622
+ useNotificationConfig,
4623
+ useNotificationFeed,
4624
+ useRealtimeContext,
4625
+ useUnreadCount
4626
+ });
4627
+ //# sourceMappingURL=index.cjs.map