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