@burdenoff/microfe-channel 2026.527.1 → 2026.530.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/ChannelRoutes.js +5 -5
- package/dist/channel/ChannelRoutes.js.map +1 -1
- package/dist/channel/constants/permissions.js +3 -0
- package/dist/channel/constants/permissions.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.js +227 -221
- package/dist/channel/modules/channels/pages/ChannelDashboardPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelDetailsPage.js +392 -391
- package/dist/channel/modules/channels/pages/ChannelDetailsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js +1 -1
- package/dist/channel/modules/channels/pages/ChannelUsersPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ChannelsListPage.js +216 -212
- package/dist/channel/modules/channels/pages/ChannelsListPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js +99 -98
- package/dist/channel/modules/channels/pages/ConnectionDetailsPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/ConnectionsListPage.js +129 -126
- package/dist/channel/modules/channels/pages/ConnectionsListPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/CreateChannelPage.js +40 -50
- package/dist/channel/modules/channels/pages/CreateChannelPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js +1 -1
- package/dist/channel/modules/channels/pages/DeliveryHistoryPage.js.map +1 -1
- package/dist/channel/modules/channels/pages/WebhookEventsPage.js +1 -1
- package/dist/channel/modules/channels/pages/WebhookEventsPage.js.map +1 -1
- package/dist/channel/pages/ChannelLandingPage.js +50 -34
- package/dist/channel/pages/ChannelLandingPage.js.map +1 -1
- package/package.json +2 -2
|
@@ -7,8 +7,9 @@ import { useI18n as a } from "@burdenoff/fe-libs/shared/providers/shell/I18nProv
|
|
|
7
7
|
import { AlertCircle as o, ArrowLeft as s, Bell as c, CheckCircle2 as l, Globe as u, Hash as d, Inbox as f, Loader2 as p, Mail as m, MessageCircle as h, MessageSquare as g, Radio as _, Save as v, Send as y, Smartphone as b, Users as x } from "lucide-react";
|
|
8
8
|
import { useNavigate as S } from "react-router-dom";
|
|
9
9
|
import { jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
10
|
+
import { OptionalFields as T } from "@burdenoff/fe-libs/ui";
|
|
10
11
|
//#region src/channel/modules/channels/pages/CreateChannelPage.tsx
|
|
11
|
-
var
|
|
12
|
+
var E = [
|
|
12
13
|
{
|
|
13
14
|
value: "EMAIL",
|
|
14
15
|
label: "Email",
|
|
@@ -126,11 +127,11 @@ var T = [
|
|
|
126
127
|
}]
|
|
127
128
|
}
|
|
128
129
|
];
|
|
129
|
-
function
|
|
130
|
+
function D() {
|
|
130
131
|
let c = S(), { organizationId: u } = e(), { createChannel: d } = t(), { t: f } = a(), m = (e, t) => {
|
|
131
132
|
let n = f(e);
|
|
132
133
|
return n === e ? t : n;
|
|
133
|
-
}, [h, g] = i(!1), [y, b] = i(null), [x,
|
|
134
|
+
}, [h, g] = i(!1), [y, b] = i(null), [x, D] = i({
|
|
134
135
|
name: "",
|
|
135
136
|
platform: "EMAIL",
|
|
136
137
|
description: "",
|
|
@@ -138,8 +139,8 @@ function E() {
|
|
|
138
139
|
status: "ACTIVE",
|
|
139
140
|
priority: 100,
|
|
140
141
|
platformConfig: ""
|
|
141
|
-
}),
|
|
142
|
-
|
|
142
|
+
}), O = E.find((e) => e.value === x.platform)?.adapters || [], k = E.find((e) => e.value === x.platform), A = (e) => {
|
|
143
|
+
D((t) => ({
|
|
143
144
|
...t,
|
|
144
145
|
platform: e,
|
|
145
146
|
adapterType: ""
|
|
@@ -183,7 +184,7 @@ function E() {
|
|
|
183
184
|
}),
|
|
184
185
|
/* @__PURE__ */ w("form", {
|
|
185
186
|
onSubmit: async (e) => {
|
|
186
|
-
if (e.preventDefault(), b(null), !
|
|
187
|
+
if (e.preventDefault(), b(null), !x.name.trim()) {
|
|
187
188
|
b(m("channel.validation.nameRequired", "Channel name is required"));
|
|
188
189
|
return;
|
|
189
190
|
}
|
|
@@ -192,8 +193,8 @@ function E() {
|
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
194
195
|
let t;
|
|
195
|
-
if (
|
|
196
|
-
t = JSON.parse(
|
|
196
|
+
if (x.platformConfig.trim()) try {
|
|
197
|
+
t = JSON.parse(x.platformConfig);
|
|
197
198
|
} catch {
|
|
198
199
|
b(m("channel.validation.platformConfigJson", "Platform Config must be valid JSON"));
|
|
199
200
|
return;
|
|
@@ -201,13 +202,13 @@ function E() {
|
|
|
201
202
|
g(!0);
|
|
202
203
|
try {
|
|
203
204
|
let e = await d({
|
|
204
|
-
name:
|
|
205
|
-
platform:
|
|
205
|
+
name: x.name.trim(),
|
|
206
|
+
platform: x.platform,
|
|
206
207
|
organizationId: u,
|
|
207
|
-
description:
|
|
208
|
-
adapterType:
|
|
209
|
-
status:
|
|
210
|
-
priority:
|
|
208
|
+
description: x.description.trim() || void 0,
|
|
209
|
+
adapterType: x.adapterType || void 0,
|
|
210
|
+
status: x.status,
|
|
211
|
+
priority: x.priority,
|
|
211
212
|
platformConfig: t
|
|
212
213
|
});
|
|
213
214
|
e ? c(`../${e.id}`) : b(m("channel.createFailed", "Failed to create channel"));
|
|
@@ -241,8 +242,8 @@ function E() {
|
|
|
241
242
|
}), /* @__PURE__ */ C("input", {
|
|
242
243
|
"aria-label": m("channel.namePlaceholder", "e.g., Primary Email, SMS Alerts"),
|
|
243
244
|
type: "text",
|
|
244
|
-
value:
|
|
245
|
-
onChange: (e) =>
|
|
245
|
+
value: x.name,
|
|
246
|
+
onChange: (e) => D((t) => ({
|
|
246
247
|
...t,
|
|
247
248
|
name: e.target.value
|
|
248
249
|
})),
|
|
@@ -254,8 +255,8 @@ function E() {
|
|
|
254
255
|
children: m("channel.description", "Description")
|
|
255
256
|
}), /* @__PURE__ */ C("textarea", {
|
|
256
257
|
"aria-label": m("channel.descriptionPlaceholder", "Optional description of this channel's purpose"),
|
|
257
|
-
value:
|
|
258
|
-
onChange: (e) =>
|
|
258
|
+
value: x.description,
|
|
259
|
+
onChange: (e) => D((t) => ({
|
|
259
260
|
...t,
|
|
260
261
|
description: e.target.value
|
|
261
262
|
})),
|
|
@@ -277,8 +278,8 @@ function E() {
|
|
|
277
278
|
children: m("channel.providerAdapter", "Provider / Adapter")
|
|
278
279
|
}),
|
|
279
280
|
/* @__PURE__ */ w("select", {
|
|
280
|
-
value:
|
|
281
|
-
onChange: (e) =>
|
|
281
|
+
value: x.adapterType,
|
|
282
|
+
onChange: (e) => D((t) => ({
|
|
282
283
|
...t,
|
|
283
284
|
adapterType: e.target.value
|
|
284
285
|
})),
|
|
@@ -286,14 +287,14 @@ function E() {
|
|
|
286
287
|
children: [/* @__PURE__ */ C("option", {
|
|
287
288
|
value: "",
|
|
288
289
|
children: m("channel.selectProvider", "Select a provider (optional)")
|
|
289
|
-
}),
|
|
290
|
+
}), O.map((e) => /* @__PURE__ */ C("option", {
|
|
290
291
|
value: e.value,
|
|
291
292
|
children: e.label
|
|
292
293
|
}, e.value))]
|
|
293
294
|
}),
|
|
294
295
|
/* @__PURE__ */ C("p", {
|
|
295
296
|
className: "mt-1 text-[11px] text-muted-foreground",
|
|
296
|
-
children:
|
|
297
|
+
children: k ? m("channel.providersAvailableForPlatform", `${O.length} providers available for ${k.label}`) : m("channel.selectPlatformFirst", "Select a platform first")
|
|
297
298
|
})
|
|
298
299
|
] }),
|
|
299
300
|
/* @__PURE__ */ w("div", {
|
|
@@ -304,8 +305,8 @@ function E() {
|
|
|
304
305
|
children: m("channel.status", "Status")
|
|
305
306
|
}),
|
|
306
307
|
/* @__PURE__ */ w("select", {
|
|
307
|
-
value:
|
|
308
|
-
onChange: (e) =>
|
|
308
|
+
value: x.status,
|
|
309
|
+
onChange: (e) => D((t) => ({
|
|
309
310
|
...t,
|
|
310
311
|
status: e.target.value
|
|
311
312
|
})),
|
|
@@ -320,7 +321,7 @@ function E() {
|
|
|
320
321
|
}),
|
|
321
322
|
/* @__PURE__ */ C("p", {
|
|
322
323
|
className: "mt-1 text-[11px] text-muted-foreground",
|
|
323
|
-
children:
|
|
324
|
+
children: x.status === "ACTIVE" ? m("channel.statusHint.active", "Ready to send immediately") : m("channel.statusHint.inactive", "Configure before activating")
|
|
324
325
|
})
|
|
325
326
|
] }), /* @__PURE__ */ w("div", { children: [
|
|
326
327
|
/* @__PURE__ */ C("label", {
|
|
@@ -332,8 +333,8 @@ function E() {
|
|
|
332
333
|
id: "create-channel-priority",
|
|
333
334
|
"aria-label": m("channel.priority", "Priority"),
|
|
334
335
|
type: "number",
|
|
335
|
-
value:
|
|
336
|
-
onChange: (e) =>
|
|
336
|
+
value: x.priority,
|
|
337
|
+
onChange: (e) => D((t) => ({
|
|
337
338
|
...t,
|
|
338
339
|
priority: parseInt(e.target.value) || 0
|
|
339
340
|
})),
|
|
@@ -364,11 +365,11 @@ function E() {
|
|
|
364
365
|
]
|
|
365
366
|
}), /* @__PURE__ */ C("div", {
|
|
366
367
|
className: "grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5",
|
|
367
|
-
children:
|
|
368
|
-
let t = e.icon, n =
|
|
368
|
+
children: E.map((e) => {
|
|
369
|
+
let t = e.icon, n = x.platform === e.value;
|
|
369
370
|
return /* @__PURE__ */ w("button", {
|
|
370
371
|
type: "button",
|
|
371
|
-
onClick: () =>
|
|
372
|
+
onClick: () => A(e.value),
|
|
372
373
|
className: `relative flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all text-center ${n ? `${e.color} border-current ring-1 ring-current` : "border-border hover:bg-surface-hover text-muted-foreground hover:text-foreground"}`,
|
|
373
374
|
children: [
|
|
374
375
|
n && /* @__PURE__ */ C(l, {
|
|
@@ -389,21 +390,10 @@ function E() {
|
|
|
389
390
|
})
|
|
390
391
|
})]
|
|
391
392
|
}),
|
|
392
|
-
/* @__PURE__ */
|
|
393
|
-
|
|
394
|
-
children:
|
|
395
|
-
|
|
396
|
-
onClick: () => E(!x),
|
|
397
|
-
className: "w-full flex items-center justify-between text-left",
|
|
398
|
-
children: [/* @__PURE__ */ C("h2", {
|
|
399
|
-
className: "text-sm font-semibold text-foreground uppercase tracking-wider",
|
|
400
|
-
children: m("channel.advancedOptions", "Advanced Options")
|
|
401
|
-
}), /* @__PURE__ */ C("span", {
|
|
402
|
-
className: "text-xs text-muted-foreground",
|
|
403
|
-
children: x ? m("channel.hide", "Hide") : m("channel.show", "Show")
|
|
404
|
-
})]
|
|
405
|
-
}), x && /* @__PURE__ */ C("div", {
|
|
406
|
-
className: "mt-4 grid gap-4 lg:grid-cols-2",
|
|
393
|
+
/* @__PURE__ */ C(T, {
|
|
394
|
+
title: m("channel.advancedOptions", "Advanced settings"),
|
|
395
|
+
children: /* @__PURE__ */ C("div", {
|
|
396
|
+
className: "grid gap-4 lg:grid-cols-2",
|
|
407
397
|
children: /* @__PURE__ */ w("div", { children: [
|
|
408
398
|
/* @__PURE__ */ w("label", {
|
|
409
399
|
className: "block text-sm font-medium text-foreground mb-1.5",
|
|
@@ -418,8 +408,8 @@ function E() {
|
|
|
418
408
|
}),
|
|
419
409
|
/* @__PURE__ */ C("textarea", {
|
|
420
410
|
"aria-label": m("channel.platformConfig", "Platform Config"),
|
|
421
|
-
value:
|
|
422
|
-
onChange: (e) =>
|
|
411
|
+
value: x.platformConfig,
|
|
412
|
+
onChange: (e) => D((t) => ({
|
|
423
413
|
...t,
|
|
424
414
|
platformConfig: e.target.value
|
|
425
415
|
})),
|
|
@@ -432,7 +422,7 @@ function E() {
|
|
|
432
422
|
children: m("channel.platformConfigHint", "Platform-specific configuration as JSON")
|
|
433
423
|
})
|
|
434
424
|
] })
|
|
435
|
-
})
|
|
425
|
+
})
|
|
436
426
|
}),
|
|
437
427
|
/* @__PURE__ */ w("div", {
|
|
438
428
|
className: "flex items-center justify-between pt-2",
|
|
@@ -443,7 +433,7 @@ function E() {
|
|
|
443
433
|
children: m("channel.cancel", "Cancel")
|
|
444
434
|
}), /* @__PURE__ */ w("button", {
|
|
445
435
|
type: "submit",
|
|
446
|
-
disabled: h || !
|
|
436
|
+
disabled: h || !x.name.trim(),
|
|
447
437
|
className: "inline-flex items-center gap-2 px-5 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg-hover rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
|
|
448
438
|
children: [h ? /* @__PURE__ */ C(p, {
|
|
449
439
|
size: 16,
|
|
@@ -458,6 +448,6 @@ function E() {
|
|
|
458
448
|
});
|
|
459
449
|
}
|
|
460
450
|
//#endregion
|
|
461
|
-
export {
|
|
451
|
+
export { D as CreateChannelPage };
|
|
462
452
|
|
|
463
453
|
//# sourceMappingURL=CreateChannelPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateChannelPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/CreateChannelPage.tsx"],"sourcesContent":["/**\n * Create Channel Page\n *\n * Form fields match the exact CreateChannelInput from global-channel-svc:\n * name (required), platform (required), organizationId (auto), description,\n * adapterType, status, priority, platformConfig\n *\n * organizationId is injected from shell context — not shown in form.\n * Credentials belong to createConnection mutation — not part of channel creation.\n */\n\nimport { useState } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Radio,\n ArrowLeft,\n Save,\n Loader2,\n AlertCircle,\n Mail,\n Smartphone,\n Bell,\n Inbox,\n Globe,\n Hash,\n Users,\n MessageCircle,\n Send,\n MessageSquare,\n CheckCircle2,\n} from 'lucide-react';\nimport { useChannel } from '../../../providers/ChannelProvider';\nimport {\n useChannels,\n type ChannelPlatform,\n type AdapterType,\n type ChannelStatus,\n type CreateChannelInput,\n} from '../../../hooks/useChannels';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\n// ============================================================================\n// PLATFORM CONFIG\n// ============================================================================\n\n// NOTE: AdapterType values MUST match the backend Prisma schema exactly.\n// See: global-channel-svc/prisma/schema/schema.prisma (enum AdapterType)\nconst PLATFORM_OPTIONS: {\n value: ChannelPlatform;\n label: string;\n description: string;\n icon: typeof Mail;\n color: string;\n adapters: { value: AdapterType; label: string }[];\n}[] = [\n {\n value: 'EMAIL',\n label: 'Email',\n description: 'Send emails via various providers',\n icon: Mail,\n color: 'text-accent-blue bg-accent-blue-subtle border-accent-blue-subtle',\n adapters: [\n { value: 'AWS_SES', label: 'AWS SES' },\n { value: 'AZURE_ACS', label: 'Azure Communication Services' },\n ],\n },\n {\n value: 'SMS',\n label: 'SMS',\n description: 'Send text messages',\n icon: Smartphone,\n color: 'text-status-success-text bg-status-success-bg-subtle border-status-success-bg-subtle',\n adapters: [\n { value: 'AWS_SNS', label: 'AWS SNS' },\n { value: 'TWILIO_SMS', label: 'Twilio SMS' },\n ],\n },\n {\n value: 'PUSH',\n label: 'Push',\n description: 'Mobile & web push',\n icon: Bell,\n color: 'text-accent-purple bg-accent-purple-subtle border-accent-purple-subtle',\n adapters: [{ value: 'FCM', label: 'Firebase Cloud Messaging' }],\n },\n {\n value: 'INAPP',\n label: 'In-App',\n description: 'In-app notifications',\n icon: Inbox,\n color: 'text-accent-indigo bg-accent-indigo-subtle border-accent-indigo-subtle',\n adapters: [{ value: 'INAPP_NATIVE', label: 'Native In-App' }],\n },\n {\n value: 'SLACK',\n label: 'Slack',\n description: 'Workspace notifications',\n icon: Hash,\n color: 'text-accent-pink bg-accent-pink-subtle border-accent-pink-subtle',\n adapters: [{ value: 'SLACK_BOT', label: 'Slack Bot' }],\n },\n {\n value: 'TEAMS',\n label: 'Teams',\n description: 'Teams notifications',\n icon: Users,\n color: 'text-accent-cyan bg-accent-cyan-subtle border-accent-cyan-subtle',\n adapters: [{ value: 'TEAMS_BOT', label: 'Teams Bot' }],\n },\n {\n value: 'DISCORD',\n label: 'Discord',\n description: 'Server notifications',\n icon: MessageCircle,\n color: 'text-accent-violet bg-accent-violet-subtle border-accent-violet-subtle',\n adapters: [{ value: 'DISCORD_BOT', label: 'Discord Bot' }],\n },\n {\n value: 'WHATSAPP',\n label: 'WhatsApp',\n description: 'Business messaging',\n icon: Send,\n color: 'text-accent-emerald bg-accent-emerald-subtle border-accent-emerald-subtle',\n adapters: [\n { value: 'WHATSAPP_CLOUD', label: 'WhatsApp Cloud API' },\n { value: 'WHATSAPP_TWILIO', label: 'WhatsApp via Twilio' },\n ],\n },\n {\n value: 'WEBCHAT',\n label: 'Webchat',\n description: 'Chat widgets',\n icon: MessageSquare,\n color: 'text-accent-teal bg-accent-teal-subtle border-accent-teal-subtle',\n adapters: [], // No WEBCHAT adapters in backend Prisma schema yet\n },\n {\n value: 'WEBHOOK',\n label: 'Webhook',\n description: 'HTTP webhooks',\n icon: Globe,\n color: 'text-accent-orange bg-accent-orange-subtle border-accent-orange-subtle',\n adapters: [{ value: 'WEBHOOK_HTTP', label: 'HTTP Webhook' }],\n },\n];\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport function CreateChannelPage() {\n const navigate = useNavigate();\n const { organizationId } = useChannel();\n const { createChannel } = useChannels();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [showAdvanced, setShowAdvanced] = useState(false);\n\n // Form state — matches CreateChannelInput exactly\n const [formData, setFormData] = useState<{\n name: string; // required\n platform: ChannelPlatform; // required\n description: string; // optional\n adapterType: AdapterType | ''; // required (auto-selected based on platform)\n status: ChannelStatus; // optional — default ACTIVE on server\n priority: number; // optional — default 100 on server\n platformConfig: string; // optional — JSON string\n }>({\n name: '',\n platform: 'EMAIL',\n description: '',\n adapterType: 'AWS_SES', // Default to AWS SES for Email platform\n status: 'ACTIVE',\n priority: 100,\n platformConfig: '',\n });\n\n const availableAdapters =\n PLATFORM_OPTIONS.find((p) => p.value === formData.platform)?.adapters || [];\n\n const selectedPlatform = PLATFORM_OPTIONS.find((p) => p.value === formData.platform);\n\n const handlePlatformChange = (platform: ChannelPlatform) => {\n setFormData((prev) => ({ ...prev, platform, adapterType: '' }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!formData.name.trim()) {\n setError(tr('channel.validation.nameRequired', 'Channel name is required'));\n return;\n }\n\n if (!organizationId) {\n setError(\n tr(\n 'channel.validation.organizationRequired',\n 'Organization context is required. Please ensure an organization is selected.'\n )\n );\n return;\n }\n\n // Validate platformConfig JSON if provided\n let parsedPlatformConfig: Record<string, unknown> | undefined;\n if (formData.platformConfig.trim()) {\n try {\n parsedPlatformConfig = JSON.parse(formData.platformConfig);\n } catch {\n setError(tr('channel.validation.platformConfigJson', 'Platform Config must be valid JSON'));\n return;\n }\n }\n\n setIsSubmitting(true);\n try {\n const input: CreateChannelInput = {\n name: formData.name.trim(),\n platform: formData.platform,\n organizationId,\n description: formData.description.trim() || undefined,\n adapterType: (formData.adapterType as AdapterType) || undefined,\n status: formData.status,\n priority: formData.priority,\n platformConfig: parsedPlatformConfig,\n };\n\n const result = await createChannel(input);\n if (result) {\n navigate(`../${result.id}`);\n } else {\n setError(tr('channel.createFailed', 'Failed to create channel'));\n }\n } catch (err: unknown) {\n setError(\n err instanceof Error ? err.message : tr('channel.createFailed', 'Failed to create channel')\n );\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // ── Render ────────────────────────────────────────────────────────────\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CREATE}>\n <div className=\"p-3 sm:p-6 space-y-6 max-w-5xl mx-auto\">\n {/* ── Header ───────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n className=\"p-2 text-muted-foreground hover:text-foreground hover:bg-surface-hover rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} />\n </button>\n <div>\n <h1 className=\"text-xl font-bold text-foreground flex items-center gap-2\">\n <Radio size={22} className=\"text-accent-primary\" />\n {tr('channel.createChannel', 'Create Channel')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-0.5\">\n {tr('channel.createChannelSubtitle', 'Set up a new communication channel')}\n </p>\n </div>\n </div>\n </div>\n\n {/* ── Error ────────────────────────────────────────────────────── */}\n {error && (\n <div className=\"flex items-center gap-3 p-3 bg-error-500/10 border border-status-error-border/30 rounded-lg\">\n <AlertCircle size={16} className=\"text-status-error-text shrink-0\" />\n <p className=\"text-sm text-error-400\">{error}</p>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* ── Row 1: Name + Description | Status + Provider (2-col) ─── */}\n <div className=\"grid gap-5 lg:grid-cols-2\">\n {/* Left: Name & Description */}\n <div className=\"p-5 bg-surface border border-border rounded-lg space-y-4\">\n <h2 className=\"text-sm font-semibold text-foreground uppercase tracking-wider\">\n {tr('channel.basicInformation', 'Basic Information')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.name', 'Name')} <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n aria-label={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n type=\"text\"\n value={formData.name}\n onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 placeholder:text-muted-foreground\"\n placeholder={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.description', 'Description')}\n </label>\n <textarea\n aria-label={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n value={formData.description}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, description: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none placeholder:text-muted-foreground\"\n placeholder={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n />\n </div>\n </div>\n\n {/* Right: Status + Provider / Adapter */}\n <div className=\"p-5 bg-surface border border-border rounded-lg space-y-4\">\n <h2 className=\"text-sm font-semibold text-foreground uppercase tracking-wider\">\n {tr('channel.providerStatus', 'Provider & Status')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.providerAdapter', 'Provider / Adapter')}\n </label>\n <select\n value={formData.adapterType}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n adapterType: e.target.value as AdapterType | '',\n }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-foreground\"\n >\n <option value=\"\">\n {tr('channel.selectProvider', 'Select a provider (optional)')}\n </option>\n {availableAdapters.map((a) => (\n <option key={a.value} value={a.value}>\n {a.label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {selectedPlatform\n ? tr(\n 'channel.providersAvailableForPlatform',\n `${availableAdapters.length} providers available for ${selectedPlatform.label}`\n )\n : tr('channel.selectPlatformFirst', 'Select a platform first')}\n </p>\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.status', 'Status')}\n </label>\n <select\n value={formData.status}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, status: e.target.value as ChannelStatus }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-foreground\"\n >\n <option value=\"ACTIVE\">{tr('channel.status.active', 'Active')}</option>\n <option value=\"INACTIVE\">{tr('channel.status.inactive', 'Inactive')}</option>\n </select>\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {formData.status === 'ACTIVE'\n ? tr('channel.statusHint.active', 'Ready to send immediately')\n : tr('channel.statusHint.inactive', 'Configure before activating')}\n </p>\n </div>\n\n <div>\n <label\n htmlFor=\"create-channel-priority\"\n className=\"block text-sm font-medium text-foreground mb-1.5\"\n >\n {tr('channel.priority', 'Priority')}\n </label>\n <input\n id=\"create-channel-priority\"\n aria-label={tr('channel.priority', 'Priority')}\n type=\"number\"\n value={formData.priority}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n priority: parseInt(e.target.value) || 0,\n }))\n }\n min={0}\n max={100}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50\"\n />\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {tr('channel.priorityHint', 'Lower = higher priority (0-100)')}\n </p>\n </div>\n </div>\n </div>\n </div>\n\n {/* ── Row 2: Platform selection (full width) ─────────────────── */}\n <div className=\"p-5 bg-surface border border-border rounded-lg space-y-4\">\n <h2 className=\"text-sm font-semibold text-foreground uppercase tracking-wider\">\n {tr('channel.platform', 'Platform')} <span className=\"text-status-error-text\">*</span>\n </h2>\n\n <div className=\"grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5\">\n {PLATFORM_OPTIONS.map((platform) => {\n const Icon = platform.icon;\n const isSelected = formData.platform === platform.value;\n return (\n <button\n key={platform.value}\n type=\"button\"\n onClick={() => handlePlatformChange(platform.value)}\n className={`relative flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all text-center ${\n isSelected\n ? `${platform.color} border-current ring-1 ring-current`\n : 'border-border hover:bg-surface-hover text-muted-foreground hover:text-foreground'\n }`}\n >\n {isSelected && (\n <CheckCircle2 size={14} className=\"absolute top-1.5 right-1.5 text-current\" />\n )}\n <Icon size={20} />\n <span className=\"text-xs font-semibold\">{platform.label}</span>\n <span className=\"text-[10px] leading-tight opacity-70\">\n {platform.description}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* ── Row 3: Advanced (collapsible) — platformConfig */}\n <div className=\"p-5 bg-surface border border-border rounded-lg\">\n <button\n type=\"button\"\n onClick={() => setShowAdvanced(!showAdvanced)}\n className=\"w-full flex items-center justify-between text-left\"\n >\n <h2 className=\"text-sm font-semibold text-foreground uppercase tracking-wider\">\n {tr('channel.advancedOptions', 'Advanced Options')}\n </h2>\n <span className=\"text-xs text-muted-foreground\">\n {showAdvanced ? tr('channel.hide', 'Hide') : tr('channel.show', 'Show')}\n </span>\n </button>\n\n {showAdvanced && (\n <div className=\"mt-4 grid gap-4 lg:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.platformConfig', 'Platform Config')}{' '}\n <span className=\"text-[10px] text-muted-foreground\">(JSON)</span>\n </label>\n <textarea\n aria-label={tr('channel.platformConfig', 'Platform Config')}\n value={formData.platformConfig}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, platformConfig: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none font-mono placeholder:text-muted-foreground\"\n placeholder={tr(\n 'channel.platformConfigPlaceholder',\n '{\"fromEmail\": \"noreply@example.com\"}'\n )}\n />\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {tr('channel.platformConfigHint', 'Platform-specific configuration as JSON')}\n </p>\n </div>\n </div>\n )}\n </div>\n\n {/* ── Actions ────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between pt-2\">\n <button\n type=\"button\"\n onClick={() => navigate('/channels/list')}\n className=\"px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-surface-hover border border-border rounded-lg transition-colors\"\n >\n {tr('channel.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || !formData.name.trim()}\n className=\"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg-hover rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isSubmitting ? <Loader2 size={16} className=\"animate-spin\" /> : <Save size={16} />}\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n </div>\n </form>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreateChannelPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/CreateChannelPage.tsx"],"sourcesContent":["/**\n * Create Channel Page\n *\n * Form fields match the exact CreateChannelInput from global-channel-svc:\n * name (required), platform (required), organizationId (auto), description,\n * adapterType, status, priority, platformConfig\n *\n * organizationId is injected from shell context — not shown in form.\n * Credentials belong to createConnection mutation — not part of channel creation.\n */\n\nimport { useState } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { OptionalFields } from '@burdenoff/fe-libs/ui';\nimport {\n Radio,\n ArrowLeft,\n Save,\n Loader2,\n AlertCircle,\n Mail,\n Smartphone,\n Bell,\n Inbox,\n Globe,\n Hash,\n Users,\n MessageCircle,\n Send,\n MessageSquare,\n CheckCircle2,\n} from 'lucide-react';\nimport { useChannel } from '../../../providers/ChannelProvider';\nimport {\n useChannels,\n type ChannelPlatform,\n type AdapterType,\n type ChannelStatus,\n type CreateChannelInput,\n} from '../../../hooks/useChannels';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\n\n// ============================================================================\n// PLATFORM CONFIG\n// ============================================================================\n\n// NOTE: AdapterType values MUST match the backend Prisma schema exactly.\n// See: global-channel-svc/prisma/schema/schema.prisma (enum AdapterType)\nconst PLATFORM_OPTIONS: {\n value: ChannelPlatform;\n label: string;\n description: string;\n icon: typeof Mail;\n color: string;\n adapters: { value: AdapterType; label: string }[];\n}[] = [\n {\n value: 'EMAIL',\n label: 'Email',\n description: 'Send emails via various providers',\n icon: Mail,\n color: 'text-accent-blue bg-accent-blue-subtle border-accent-blue-subtle',\n adapters: [\n { value: 'AWS_SES', label: 'AWS SES' },\n { value: 'AZURE_ACS', label: 'Azure Communication Services' },\n ],\n },\n {\n value: 'SMS',\n label: 'SMS',\n description: 'Send text messages',\n icon: Smartphone,\n color: 'text-status-success-text bg-status-success-bg-subtle border-status-success-bg-subtle',\n adapters: [\n { value: 'AWS_SNS', label: 'AWS SNS' },\n { value: 'TWILIO_SMS', label: 'Twilio SMS' },\n ],\n },\n {\n value: 'PUSH',\n label: 'Push',\n description: 'Mobile & web push',\n icon: Bell,\n color: 'text-accent-purple bg-accent-purple-subtle border-accent-purple-subtle',\n adapters: [{ value: 'FCM', label: 'Firebase Cloud Messaging' }],\n },\n {\n value: 'INAPP',\n label: 'In-App',\n description: 'In-app notifications',\n icon: Inbox,\n color: 'text-accent-indigo bg-accent-indigo-subtle border-accent-indigo-subtle',\n adapters: [{ value: 'INAPP_NATIVE', label: 'Native In-App' }],\n },\n {\n value: 'SLACK',\n label: 'Slack',\n description: 'Workspace notifications',\n icon: Hash,\n color: 'text-accent-pink bg-accent-pink-subtle border-accent-pink-subtle',\n adapters: [{ value: 'SLACK_BOT', label: 'Slack Bot' }],\n },\n {\n value: 'TEAMS',\n label: 'Teams',\n description: 'Teams notifications',\n icon: Users,\n color: 'text-accent-cyan bg-accent-cyan-subtle border-accent-cyan-subtle',\n adapters: [{ value: 'TEAMS_BOT', label: 'Teams Bot' }],\n },\n {\n value: 'DISCORD',\n label: 'Discord',\n description: 'Server notifications',\n icon: MessageCircle,\n color: 'text-accent-violet bg-accent-violet-subtle border-accent-violet-subtle',\n adapters: [{ value: 'DISCORD_BOT', label: 'Discord Bot' }],\n },\n {\n value: 'WHATSAPP',\n label: 'WhatsApp',\n description: 'Business messaging',\n icon: Send,\n color: 'text-accent-emerald bg-accent-emerald-subtle border-accent-emerald-subtle',\n adapters: [\n { value: 'WHATSAPP_CLOUD', label: 'WhatsApp Cloud API' },\n { value: 'WHATSAPP_TWILIO', label: 'WhatsApp via Twilio' },\n ],\n },\n {\n value: 'WEBCHAT',\n label: 'Webchat',\n description: 'Chat widgets',\n icon: MessageSquare,\n color: 'text-accent-teal bg-accent-teal-subtle border-accent-teal-subtle',\n adapters: [], // No WEBCHAT adapters in backend Prisma schema yet\n },\n {\n value: 'WEBHOOK',\n label: 'Webhook',\n description: 'HTTP webhooks',\n icon: Globe,\n color: 'text-accent-orange bg-accent-orange-subtle border-accent-orange-subtle',\n adapters: [{ value: 'WEBHOOK_HTTP', label: 'HTTP Webhook' }],\n },\n];\n\n// ============================================================================\n// COMPONENT\n// ============================================================================\n\nexport function CreateChannelPage() {\n const navigate = useNavigate();\n const { organizationId } = useChannel();\n const { createChannel } = useChannels();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Form state — matches CreateChannelInput exactly\n const [formData, setFormData] = useState<{\n name: string; // required\n platform: ChannelPlatform; // required\n description: string; // optional\n adapterType: AdapterType | ''; // required (auto-selected based on platform)\n status: ChannelStatus; // optional — default ACTIVE on server\n priority: number; // optional — default 100 on server\n platformConfig: string; // optional — JSON string\n }>({\n name: '',\n platform: 'EMAIL',\n description: '',\n adapterType: 'AWS_SES', // Default to AWS SES for Email platform\n status: 'ACTIVE',\n priority: 100,\n platformConfig: '',\n });\n\n const availableAdapters =\n PLATFORM_OPTIONS.find((p) => p.value === formData.platform)?.adapters || [];\n\n const selectedPlatform = PLATFORM_OPTIONS.find((p) => p.value === formData.platform);\n\n const handlePlatformChange = (platform: ChannelPlatform) => {\n setFormData((prev) => ({ ...prev, platform, adapterType: '' }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n if (!formData.name.trim()) {\n setError(tr('channel.validation.nameRequired', 'Channel name is required'));\n return;\n }\n\n if (!organizationId) {\n setError(\n tr(\n 'channel.validation.organizationRequired',\n 'Organization context is required. Please ensure an organization is selected.'\n )\n );\n return;\n }\n\n // Validate platformConfig JSON if provided\n let parsedPlatformConfig: Record<string, unknown> | undefined;\n if (formData.platformConfig.trim()) {\n try {\n parsedPlatformConfig = JSON.parse(formData.platformConfig);\n } catch {\n setError(tr('channel.validation.platformConfigJson', 'Platform Config must be valid JSON'));\n return;\n }\n }\n\n setIsSubmitting(true);\n try {\n const input: CreateChannelInput = {\n name: formData.name.trim(),\n platform: formData.platform,\n organizationId,\n description: formData.description.trim() || undefined,\n adapterType: (formData.adapterType as AdapterType) || undefined,\n status: formData.status,\n priority: formData.priority,\n platformConfig: parsedPlatformConfig,\n };\n\n const result = await createChannel(input);\n if (result) {\n navigate(`../${result.id}`);\n } else {\n setError(tr('channel.createFailed', 'Failed to create channel'));\n }\n } catch (err: unknown) {\n setError(\n err instanceof Error ? err.message : tr('channel.createFailed', 'Failed to create channel')\n );\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // ── Render ────────────────────────────────────────────────────────────\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_CREATE}>\n <div className=\"p-3 sm:p-6 space-y-6 max-w-5xl mx-auto\">\n {/* ── Header ───────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n className=\"p-2 text-muted-foreground hover:text-foreground hover:bg-surface-hover rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} />\n </button>\n <div>\n <h1 className=\"text-xl font-bold text-foreground flex items-center gap-2\">\n <Radio size={22} className=\"text-accent-primary\" />\n {tr('channel.createChannel', 'Create Channel')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-0.5\">\n {tr('channel.createChannelSubtitle', 'Set up a new communication channel')}\n </p>\n </div>\n </div>\n </div>\n\n {/* ── Error ────────────────────────────────────────────────────── */}\n {error && (\n <div className=\"flex items-center gap-3 p-3 bg-error-500/10 border border-status-error-border/30 rounded-lg\">\n <AlertCircle size={16} className=\"text-status-error-text shrink-0\" />\n <p className=\"text-sm text-error-400\">{error}</p>\n </div>\n )}\n\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* ── Row 1: Name + Description | Status + Provider (2-col) ─── */}\n <div className=\"grid gap-5 lg:grid-cols-2\">\n {/* Left: Name & Description */}\n <div className=\"p-5 bg-surface border border-border rounded-lg space-y-4\">\n <h2 className=\"text-sm font-semibold text-foreground uppercase tracking-wider\">\n {tr('channel.basicInformation', 'Basic Information')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.name', 'Name')} <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n aria-label={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n type=\"text\"\n value={formData.name}\n onChange={(e) => setFormData((prev) => ({ ...prev, name: e.target.value }))}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 placeholder:text-muted-foreground\"\n placeholder={tr('channel.namePlaceholder', 'e.g., Primary Email, SMS Alerts')}\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.description', 'Description')}\n </label>\n <textarea\n aria-label={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n value={formData.description}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, description: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none placeholder:text-muted-foreground\"\n placeholder={tr(\n 'channel.descriptionPlaceholder',\n \"Optional description of this channel's purpose\"\n )}\n />\n </div>\n </div>\n\n {/* Right: Status + Provider / Adapter */}\n <div className=\"p-5 bg-surface border border-border rounded-lg space-y-4\">\n <h2 className=\"text-sm font-semibold text-foreground uppercase tracking-wider\">\n {tr('channel.providerStatus', 'Provider & Status')}\n </h2>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.providerAdapter', 'Provider / Adapter')}\n </label>\n <select\n value={formData.adapterType}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n adapterType: e.target.value as AdapterType | '',\n }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-foreground\"\n >\n <option value=\"\">\n {tr('channel.selectProvider', 'Select a provider (optional)')}\n </option>\n {availableAdapters.map((a) => (\n <option key={a.value} value={a.value}>\n {a.label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {selectedPlatform\n ? tr(\n 'channel.providersAvailableForPlatform',\n `${availableAdapters.length} providers available for ${selectedPlatform.label}`\n )\n : tr('channel.selectPlatformFirst', 'Select a platform first')}\n </p>\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.status', 'Status')}\n </label>\n <select\n value={formData.status}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, status: e.target.value as ChannelStatus }))\n }\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-foreground\"\n >\n <option value=\"ACTIVE\">{tr('channel.status.active', 'Active')}</option>\n <option value=\"INACTIVE\">{tr('channel.status.inactive', 'Inactive')}</option>\n </select>\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {formData.status === 'ACTIVE'\n ? tr('channel.statusHint.active', 'Ready to send immediately')\n : tr('channel.statusHint.inactive', 'Configure before activating')}\n </p>\n </div>\n\n <div>\n <label\n htmlFor=\"create-channel-priority\"\n className=\"block text-sm font-medium text-foreground mb-1.5\"\n >\n {tr('channel.priority', 'Priority')}\n </label>\n <input\n id=\"create-channel-priority\"\n aria-label={tr('channel.priority', 'Priority')}\n type=\"number\"\n value={formData.priority}\n onChange={(e) =>\n setFormData((prev) => ({\n ...prev,\n priority: parseInt(e.target.value) || 0,\n }))\n }\n min={0}\n max={100}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50\"\n />\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {tr('channel.priorityHint', 'Lower = higher priority (0-100)')}\n </p>\n </div>\n </div>\n </div>\n </div>\n\n {/* ── Row 2: Platform selection (full width) ─────────────────── */}\n <div className=\"p-5 bg-surface border border-border rounded-lg space-y-4\">\n <h2 className=\"text-sm font-semibold text-foreground uppercase tracking-wider\">\n {tr('channel.platform', 'Platform')} <span className=\"text-status-error-text\">*</span>\n </h2>\n\n <div className=\"grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-5\">\n {PLATFORM_OPTIONS.map((platform) => {\n const Icon = platform.icon;\n const isSelected = formData.platform === platform.value;\n return (\n <button\n key={platform.value}\n type=\"button\"\n onClick={() => handlePlatformChange(platform.value)}\n className={`relative flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all text-center ${\n isSelected\n ? `${platform.color} border-current ring-1 ring-current`\n : 'border-border hover:bg-surface-hover text-muted-foreground hover:text-foreground'\n }`}\n >\n {isSelected && (\n <CheckCircle2 size={14} className=\"absolute top-1.5 right-1.5 text-current\" />\n )}\n <Icon size={20} />\n <span className=\"text-xs font-semibold\">{platform.label}</span>\n <span className=\"text-[10px] leading-tight opacity-70\">\n {platform.description}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* ── Row 3: Advanced (collapsible) — webhook config / delivery / retry policy (platformConfig) */}\n <OptionalFields title={tr('channel.advancedOptions', 'Advanced settings')}>\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1.5\">\n {tr('channel.platformConfig', 'Platform Config')}{' '}\n <span className=\"text-[10px] text-muted-foreground\">(JSON)</span>\n </label>\n <textarea\n aria-label={tr('channel.platformConfig', 'Platform Config')}\n value={formData.platformConfig}\n onChange={(e) =>\n setFormData((prev) => ({ ...prev, platformConfig: e.target.value }))\n }\n rows={3}\n className=\"w-full px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 resize-none font-mono placeholder:text-muted-foreground\"\n placeholder={tr(\n 'channel.platformConfigPlaceholder',\n '{\"fromEmail\": \"noreply@example.com\"}'\n )}\n />\n <p className=\"mt-1 text-[11px] text-muted-foreground\">\n {tr('channel.platformConfigHint', 'Platform-specific configuration as JSON')}\n </p>\n </div>\n </div>\n </OptionalFields>\n\n {/* ── Actions ────────────────────────────────────────────────── */}\n <div className=\"flex items-center justify-between pt-2\">\n <button\n type=\"button\"\n onClick={() => navigate('/channels/list')}\n className=\"px-4 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-surface-hover border border-border rounded-lg transition-colors\"\n >\n {tr('channel.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting || !formData.name.trim()}\n className=\"inline-flex items-center gap-2 px-5 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg-hover rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isSubmitting ? <Loader2 size={16} className=\"animate-spin\" /> : <Save size={16} />}\n {tr('channel.createChannel', 'Create Channel')}\n </button>\n </div>\n </form>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":";;;;;;;;;;;AAkDA,IAAM,IAOA;CACJ;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CACR;GAAE,OAAO;GAAW,OAAO;GAAW,EACtC;GAAE,OAAO;GAAa,OAAO;GAAgC,CAC9D;EACF;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CACR;GAAE,OAAO;GAAW,OAAO;GAAW,EACtC;GAAE,OAAO;GAAc,OAAO;GAAc,CAC7C;EACF;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CAAC;GAAE,OAAO;GAAO,OAAO;GAA4B,CAAC;EAChE;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CAAC;GAAE,OAAO;GAAgB,OAAO;GAAiB,CAAC;EAC9D;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CAAC;GAAE,OAAO;GAAa,OAAO;GAAa,CAAC;EACvD;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CAAC;GAAE,OAAO;GAAa,OAAO;GAAa,CAAC;EACvD;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CAAC;GAAE,OAAO;GAAe,OAAO;GAAe,CAAC;EAC3D;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CACR;GAAE,OAAO;GAAkB,OAAO;GAAsB,EACxD;GAAE,OAAO;GAAmB,OAAO;GAAuB,CAC3D;EACF;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,EAAE;EACb;CACD;EACE,OAAO;EACP,OAAO;EACP,aAAa;EACb,MAAM;EACN,OAAO;EACP,UAAU,CAAC;GAAE,OAAO;GAAgB,OAAO;GAAgB,CAAC;EAC7D;CACF;AAMD,SAAgB,IAAoB;CAClC,IAAM,IAAW,GAAa,EACxB,EAAE,sBAAmB,GAAY,EACjC,EAAE,qBAAkB,GAAa,EACjC,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAA6B;EACpD,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAG9B,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAO,KAAY,EAAwB,KAAK,EAGjD,CAAC,GAAU,KAAe,EAQ7B;EACD,MAAM;EACN,UAAU;EACV,aAAa;EACb,aAAa;EACb,QAAQ;EACR,UAAU;EACV,gBAAgB;EACjB,CAAC,EAEI,IACJ,EAAiB,MAAM,MAAM,EAAE,UAAU,EAAS,SAAS,EAAE,YAAY,EAAE,EAEvE,IAAmB,EAAiB,MAAM,MAAM,EAAE,UAAU,EAAS,SAAS,EAE9E,KAAwB,MAA8B;AAC1D,KAAa,OAAU;GAAE,GAAG;GAAM;GAAU,aAAa;GAAI,EAAE;;AA8DjE,QACE,kBAAC,GAAD;EAAiB,YAAY,EAAoB;YAC/C,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAS,GAAG;OAC3B,WAAU;iBAEV,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA;OAChB,CAAA,EACT,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAd,CACE,kBAAC,GAAD;QAAO,MAAM;QAAI,WAAU;QAAwB,CAAA,EAClD,EAAG,yBAAyB,iBAAiB,CAC3C;UACL,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,iCAAiC,qCAAqC;OACxE,CAAA,CACA,EAAA,CAAA,CACF;;KACF,CAAA;IAGL,KACC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAa,MAAM;MAAI,WAAU;MAAoC,CAAA,EACrE,kBAAC,KAAD;MAAG,WAAU;gBAA0B;MAAU,CAAA,CAC7C;;IAGR,kBAAC,QAAD;KAAM,UA5FS,OAAO,MAAuB;AAIjD,UAHA,EAAE,gBAAgB,EAClB,EAAS,KAAK,EAEV,CAAC,EAAS,KAAK,MAAM,EAAE;AACzB,SAAS,EAAG,mCAAmC,2BAA2B,CAAC;AAC3E;;AAGF,UAAI,CAAC,GAAgB;AACnB,SACE,EACE,2CACA,+EACD,CACF;AACD;;MAIF,IAAI;AACJ,UAAI,EAAS,eAAe,MAAM,CAChC,KAAI;AACF,WAAuB,KAAK,MAAM,EAAS,eAAe;cACpD;AACN,SAAS,EAAG,yCAAyC,qCAAqC,CAAC;AAC3F;;AAIJ,QAAgB,GAAK;AACrB,UAAI;OAYF,IAAM,IAAS,MAAM,EAXa;QAChC,MAAM,EAAS,KAAK,MAAM;QAC1B,UAAU,EAAS;QACnB;QACA,aAAa,EAAS,YAAY,MAAM,IAAI,KAAA;QAC5C,aAAc,EAAS,eAA+B,KAAA;QACtD,QAAQ,EAAS;QACjB,UAAU,EAAS;QACnB,gBAAgB;QACjB,CAEwC;AACzC,OAAI,IACF,EAAS,MAAM,EAAO,KAAK,GAE3B,EAAS,EAAG,wBAAwB,2BAA2B,CAAC;eAE3D,GAAc;AACrB,SACE,aAAe,QAAQ,EAAI,UAAU,EAAG,wBAAwB,2BAA2B,CAC5F;gBACO;AACR,SAAgB,GAAM;;;KAsCU,WAAU;eAAxC;MAEE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CAEE,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAG,4BAA4B,oBAAoB;UACjD,CAAA;SAEL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;UAAO,WAAU;oBAAjB;WACG,EAAG,gBAAgB,OAAO;WAAC;WAAC,kBAAC,QAAD;YAAM,WAAU;sBAAyB;YAAQ,CAAA;WACxE;aACR,kBAAC,SAAD;UACE,cAAY,EAAG,2BAA2B,kCAAkC;UAC5E,MAAK;UACL,OAAO,EAAS;UAChB,WAAW,MAAM,GAAa,OAAU;WAAE,GAAG;WAAM,MAAM,EAAE,OAAO;WAAO,EAAE;UAC3E,WAAU;UACV,aAAa,EAAG,2BAA2B,kCAAkC;UAC7E,CAAA,CACE,EAAA,CAAA;SAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;UAAO,WAAU;oBACd,EAAG,uBAAuB,cAAc;UACnC,CAAA,EACR,kBAAC,YAAD;UACE,cAAY,EACV,kCACA,iDACD;UACD,OAAO,EAAS;UAChB,WAAW,MACT,GAAa,OAAU;WAAE,GAAG;WAAM,aAAa,EAAE,OAAO;WAAO,EAAE;UAEnE,MAAM;UACN,WAAU;UACV,aAAa,EACX,kCACA,iDACD;UACD,CAAA,CACE,EAAA,CAAA;SACF;WAGN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAG,0BAA0B,oBAAoB;UAC/C,CAAA;SAEL,kBAAC,OAAD,EAAA,UAAA;UACE,kBAAC,SAAD;WAAO,WAAU;qBACd,EAAG,2BAA2B,qBAAqB;WAC9C,CAAA;UACR,kBAAC,UAAD;WACE,OAAO,EAAS;WAChB,WAAW,MACT,GAAa,OAAU;YACrB,GAAG;YACH,aAAa,EAAE,OAAO;YACvB,EAAE;WAEL,WAAU;qBARZ,CAUE,kBAAC,UAAD;YAAQ,OAAM;sBACX,EAAG,0BAA0B,+BAA+B;YACtD,CAAA,EACR,EAAkB,KAAK,MACtB,kBAAC,UAAD;YAAsB,OAAO,EAAE;sBAC5B,EAAE;YACI,EAFI,EAAE,MAEN,CACT,CACK;;UACT,kBAAC,KAAD;WAAG,WAAU;qBACV,IACG,EACE,yCACA,GAAG,EAAkB,OAAO,2BAA2B,EAAiB,QACzE,GACD,EAAG,+BAA+B,0BAA0B;WAC9D,CAAA;UACA,EAAA,CAAA;SAEN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD,EAAA,UAAA;WACE,kBAAC,SAAD;YAAO,WAAU;sBACd,EAAG,kBAAkB,SAAS;YACzB,CAAA;WACR,kBAAC,UAAD;YACE,OAAO,EAAS;YAChB,WAAW,MACT,GAAa,OAAU;aAAE,GAAG;aAAM,QAAQ,EAAE,OAAO;aAAwB,EAAE;YAE/E,WAAU;sBALZ,CAOE,kBAAC,UAAD;aAAQ,OAAM;uBAAU,EAAG,yBAAyB,SAAS;aAAU,CAAA,EACvE,kBAAC,UAAD;aAAQ,OAAM;uBAAY,EAAG,2BAA2B,WAAW;aAAU,CAAA,CACtE;;WACT,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAS,WAAW,WACjB,EAAG,6BAA6B,4BAA4B,GAC5D,EAAG,+BAA+B,8BAA8B;YAClE,CAAA;WACA,EAAA,CAAA,EAEN,kBAAC,OAAD,EAAA,UAAA;WACE,kBAAC,SAAD;YACE,SAAQ;YACR,WAAU;sBAET,EAAG,oBAAoB,WAAW;YAC7B,CAAA;WACR,kBAAC,SAAD;YACE,IAAG;YACH,cAAY,EAAG,oBAAoB,WAAW;YAC9C,MAAK;YACL,OAAO,EAAS;YAChB,WAAW,MACT,GAAa,OAAU;aACrB,GAAG;aACH,UAAU,SAAS,EAAE,OAAO,MAAM,IAAI;aACvC,EAAE;YAEL,KAAK;YACL,KAAK;YACL,WAAU;YACV,CAAA;WACF,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAG,wBAAwB,kCAAkC;YAC5D,CAAA;WACA,EAAA,CAAA,CACF;;SACF;UACF;;MAGN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAd;SACG,EAAG,oBAAoB,WAAW;SAAC;SAAC,kBAAC,QAAD;UAAM,WAAU;oBAAyB;UAAQ,CAAA;SACnF;WAEL,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAiB,KAAK,MAAa;SAClC,IAAM,IAAO,EAAS,MAChB,IAAa,EAAS,aAAa,EAAS;AAClD,gBACE,kBAAC,UAAD;UAEE,MAAK;UACL,eAAe,EAAqB,EAAS,MAAM;UACnD,WAAW,gGACT,IACI,GAAG,EAAS,MAAM,uCAClB;oBAPR;WAUG,KACC,kBAAC,GAAD;YAAc,MAAM;YAAI,WAAU;YAA4C,CAAA;WAEhF,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;WAClB,kBAAC,QAAD;YAAM,WAAU;sBAAyB,EAAS;YAAa,CAAA;WAC/D,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAS;YACL,CAAA;WACA;YAjBF,EAAS,MAiBP;UAEX;QACE,CAAA,CACF;;MAGN,kBAAC,GAAD;OAAgB,OAAO,EAAG,2BAA2B,oBAAoB;iBACvE,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD,EAAA,UAAA;SACE,kBAAC,SAAD;UAAO,WAAU;oBAAjB;WACG,EAAG,0BAA0B,kBAAkB;WAAE;WAClD,kBAAC,QAAD;YAAM,WAAU;sBAAoC;YAAa,CAAA;WAC3D;;SACR,kBAAC,YAAD;UACE,cAAY,EAAG,0BAA0B,kBAAkB;UAC3D,OAAO,EAAS;UAChB,WAAW,MACT,GAAa,OAAU;WAAE,GAAG;WAAM,gBAAgB,EAAE,OAAO;WAAO,EAAE;UAEtE,MAAM;UACN,WAAU;UACV,aAAa,EACX,qCACA,2CACD;UACD,CAAA;SACF,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAG,8BAA8B,0CAA0C;UAC1E,CAAA;SACA,EAAA,CAAA;QACF,CAAA;OACS,CAAA;MAGjB,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAS,iBAAiB;QACzC,WAAU;kBAET,EAAG,kBAAkB,SAAS;QACxB,CAAA,EACT,kBAAC,UAAD;QACE,MAAK;QACL,UAAU,KAAgB,CAAC,EAAS,KAAK,MAAM;QAC/C,WAAU;kBAHZ,CAKG,IAAe,kBAAC,GAAD;SAAS,MAAM;SAAI,WAAU;SAAiB,CAAA,GAAG,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAClF,EAAG,yBAAyB,iBAAiB,CACvC;UACL;;MACD;;IACH;;EACU,CAAA"}
|
|
@@ -232,7 +232,7 @@ function T() {
|
|
|
232
232
|
}), T("channel.refresh", "Refresh")]
|
|
233
233
|
}),
|
|
234
234
|
/* @__PURE__ */ S(n, {
|
|
235
|
-
permission: r.
|
|
235
|
+
permission: r.CHANNEL_DELIVERY_EXPORT,
|
|
236
236
|
children: /* @__PURE__ */ C("button", {
|
|
237
237
|
type: "button",
|
|
238
238
|
onClick: () => void re(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeliveryHistoryPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/DeliveryHistoryPage.tsx"],"sourcesContent":["/**\n * Delivery History Page\n *\n * Displays message delivery attempts for a specific channel with retry capability\n */\n\nimport { useState, useEffect, useCallback, useRef } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n RefreshCw,\n Send,\n RotateCcw,\n Download,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\nimport { useChannel, type DeliveryStatus, type DeliveryAttempt } from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportDeliveryAttempts } from '../hooks/useExport';\n\nconst STATUS_CONFIG: Record<\n DeliveryStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n PENDING: { label: 'Pending', color: 'text-muted-foreground bg-muted', icon: Clock },\n QUEUED: { label: 'Queued', color: 'text-accent-primary bg-accent-primary-subtle', icon: Clock },\n SENDING: { label: 'Sending', color: 'text-accent-primary bg-accent-primary/10', icon: Send },\n SENT: {\n label: 'Sent',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n DELIVERED: {\n label: 'Delivered',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n FAILED: {\n label: 'Failed',\n color: 'text-status-error-text bg-status-error-bg-subtle',\n icon: XCircle,\n },\n BOUNCED: {\n label: 'Bounced',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: AlertCircle,\n },\n REJECTED: {\n label: 'Rejected',\n color: 'text-status-error-text bg-status-error-bg-subtle',\n icon: XCircle,\n },\n RATE_LIMITED: {\n label: 'Rate Limited',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: Clock,\n },\n RETRYING: {\n label: 'Retrying',\n color: 'text-accent-primary bg-accent-primary/10',\n icon: RefreshCw,\n },\n OPENED: {\n label: 'Opened',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n CLICKED: {\n label: 'Clicked',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n};\n\nexport function DeliveryHistoryPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportDeliveries,\n isExporting: isExportingDeliveries,\n exportError: exportDeliveriesError,\n } = useExportDeliveryAttempts(organizationId ?? undefined, channelId);\n const [isLoadingAttempts, setIsLoadingAttempts] = useState(true);\n const [attempts, setAttempts] = useState<DeliveryAttempt[]>([]);\n const [retryingId, setRetryingId] = useState<string | null>(null);\n const [retryError, setRetryError] = useState<string | null>(null);\n const [limit, setLimit] = useState(25);\n const [total, setTotal] = useState(0);\n // Cursor stack: index 0 is always undefined (first page), subsequent entries are cursors\n const [cursorStack, setCursorStack] = useState<(string | undefined)[]>([undefined]);\n const [currentPageIdx, setCurrentPageIdx] = useState(0);\n\n const { channel, isLoading, error, fetchDeliveryAttempts, retryDelivery } = useChannel(channelId);\n\n const loadAttempts = useCallback(\n async (cursor?: string) => {\n if (!channelId) return;\n setIsLoadingAttempts(true);\n setRetryError(null);\n try {\n const result = await fetchDeliveryAttempts(limit, cursor);\n setAttempts(result.items);\n setTotal(result.total);\n return result.nextCursor;\n } finally {\n setIsLoadingAttempts(false);\n }\n },\n [channelId, fetchDeliveryAttempts, limit]\n );\n\n const loadAttemptsRef = useRef(loadAttempts);\n const cursorStackRef = useRef(cursorStack);\n useEffect(() => {\n loadAttemptsRef.current = loadAttempts;\n cursorStackRef.current = cursorStack;\n });\n const isFirstPageRender = useRef(true);\n\n useEffect(() => {\n void Promise.resolve().then(() => {\n setCursorStack([undefined]);\n setCurrentPageIdx(0);\n void loadAttemptsRef.current(undefined);\n });\n }, [limit]);\n\n useEffect(() => {\n if (isFirstPageRender.current) {\n isFirstPageRender.current = false;\n return;\n }\n void Promise.resolve().then(() =>\n loadAttemptsRef.current(cursorStackRef.current[currentPageIdx])\n );\n }, [currentPageIdx]);\n\n // Poll for delivery attempt updates every 60 s on first page only\n useEffect(() => {\n if (!channelId || currentPageIdx !== 0) return;\n const timer = setInterval(() => {\n void loadAttempts(undefined);\n }, 60_000);\n return () => clearInterval(timer);\n }, [channelId, currentPageIdx, loadAttempts]);\n\n const handleNextPage = useCallback(async () => {\n const nextCursor = await loadAttempts(cursorStack[currentPageIdx]);\n if (nextCursor) {\n setCursorStack((prev) => {\n const next = [...prev];\n if (!next[currentPageIdx + 1]) {\n next[currentPageIdx + 1] = nextCursor;\n }\n return next;\n });\n }\n setCurrentPageIdx((p) => p + 1);\n }, [cursorStack, currentPageIdx, loadAttempts]);\n\n const handlePrevPage = useCallback(() => {\n const prevIdx = currentPageIdx - 1;\n setCurrentPageIdx(prevIdx);\n void loadAttempts(cursorStack[prevIdx]);\n }, [cursorStack, currentPageIdx, loadAttempts]);\n\n const hasNextPage = attempts.length === limit;\n const hasPrevPage = currentPageIdx > 0;\n\n const handleRetry = async (attemptId: string) => {\n setRetryingId(attemptId);\n setRetryError(null);\n try {\n const result = await retryDelivery(attemptId);\n if (!result.success) {\n setRetryError(result.error || 'Retry failed');\n } else {\n await loadAttempts(cursorStack[currentPageIdx]);\n }\n } finally {\n setRetryingId(null);\n }\n };\n\n const getStatusConfig = (status: DeliveryStatus) => {\n return STATUS_CONFIG[status] || STATUS_CONFIG.PENDING;\n };\n\n // Loading state\n if (isLoading) {\n return (\n <output\n className=\"flex items-center justify-center min-h-screen\"\n aria-label={tr('channel.loading', 'Loading')}\n >\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" aria-hidden=\"true\" />\n </output>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {error || 'Channel not found'}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n Go Back\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_DELIVERY_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n aria-label={tr('channel.backToChannel', 'Back to channel')}\n className=\"p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} aria-hidden=\"true\" />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-foreground\">\n {tr('channel.deliveryHistory', 'Delivery History')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {channel.name} - Message deliveries\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-foreground\"\n >\n <option value={10}>10 results</option>\n <option value={25}>25 results</option>\n <option value={50}>50 results</option>\n <option value={100}>100 results</option>\n </select>\n <button\n type=\"button\"\n onClick={() => void loadAttempts(cursorStack[currentPageIdx])}\n disabled={isLoadingAttempts}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors disabled:opacity-50\"\n >\n <RefreshCw size={16} className={isLoadingAttempts ? 'animate-spin' : ''} />\n {tr('channel.refresh', 'Refresh')}\n </button>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_ADMIN}>\n <button\n type=\"button\"\n onClick={() => void exportDeliveries()}\n disabled={isExportingDeliveries}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-fg border border-border hover:bg-bg-elevated rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n title={tr('channel.exportCSV', 'Export to CSV')}\n >\n {isExportingDeliveries ? (\n <Loader2 size={16} className=\"animate-spin\" />\n ) : (\n <Download size={16} />\n )}\n {tr('channel.export', 'Export')}\n </button>\n </PermissionGuard>\n </div>\n </div>\n\n {/* Export Error Banner */}\n {exportDeliveriesError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportDeliveriesError}</p>\n </div>\n )}\n\n {/* Error Banner */}\n {retryError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{retryError}</p>\n <button\n type=\"button\"\n onClick={() => setRetryError(null)}\n className=\"text-status-error-text hover:text-status-error-text\"\n >\n {tr('channel.dismiss', 'Dismiss')}\n </button>\n </div>\n )}\n\n {/* Info Banner */}\n <div className=\"p-4 bg-surface border border-border rounded-lg\">\n <p className=\"text-sm text-muted-foreground\">\n Delivery history shows all message attempts sent through this channel. Data is retained\n for 7 days. Click retry on a failed delivery to attempt sending again.\n </p>\n </div>\n\n {/* Deliveries List */}\n <div className=\"bg-surface border border-border rounded-lg overflow-hidden\">\n <div className=\"overflow-x-auto min-w-full\">\n <table className=\"w-full min-w-[700px]\">\n <thead className=\"bg-muted/50 border-b border-border\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.status', 'Status')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.content', 'Content')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.sentAt', 'Sent At')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.retries', 'Retries')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.error', 'Error')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.actions', 'Actions')}\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {isLoadingAttempts ? (\n <tr>\n <td colSpan={6} className=\"px-4 py-12 text-center\">\n <Loader2 size={24} className=\"animate-spin text-accent-primary mx-auto\" />\n <p className=\"text-sm text-muted-foreground mt-2\">\n Loading delivery attempts…\n </p>\n </td>\n </tr>\n ) : attempts.length === 0 ? (\n <tr>\n <td colSpan={6} className=\"px-4 py-12 text-center\">\n <div className=\"flex flex-col items-center gap-2\">\n <Send size={32} className=\"text-muted-foreground\" />\n <p className=\"text-sm font-medium text-foreground\">\n {tr('channel.noDeliveryAttempts', 'No delivery attempts yet')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'channel.sendMessageToSeeHistory',\n 'Send a message through this channel to see delivery history'\n )}\n </p>\n </div>\n </td>\n </tr>\n ) : (\n attempts.map((delivery) => {\n const statusConfig = getStatusConfig(delivery.status as DeliveryStatus);\n const StatusIcon = statusConfig.icon;\n const canRetry =\n delivery.status === 'FAILED' ||\n delivery.status === 'BOUNCED' ||\n delivery.status === 'REJECTED';\n return (\n <tr key={delivery.id} className=\"hover:bg-muted/30 transition-colors\">\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <StatusIcon size={14} className={statusConfig.color.split(' ')[0]} />\n <span className={`text-sm font-medium ${statusConfig.color}`}>\n {statusConfig.label}\n </span>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm text-foreground truncate max-w-xs\">\n {delivery.content}\n </td>\n <td className=\"px-4 py-3 text-sm text-muted-foreground\">\n {delivery.sentAt ? new Date(delivery.sentAt).toLocaleString() : '-'}\n </td>\n <td className=\"px-4 py-3 text-sm text-muted-foreground\">\n {delivery.retryCount}\n </td>\n <td className=\"px-4 py-3 text-sm text-status-error-text max-w-xs truncate\">\n {delivery.errorMessage || '-'}\n </td>\n <td className=\"px-4 py-3\">\n {canRetry && (\n <PermissionGuard\n permission={CHANNEL_PERMISSIONS.CHANNEL_DELIVERY_EXECUTE}\n >\n <button\n type=\"button\"\n onClick={() => handleRetry(delivery.id)}\n disabled={retryingId === delivery.id}\n className=\"flex items-center gap-1 px-2 py-1 text-xs font-medium text-accent-primary hover:bg-accent-primary/10 rounded transition-colors disabled:opacity-50\"\n title={tr('channel.retryDelivery', 'Retry this delivery')}\n >\n <RotateCcw\n size={12}\n className={retryingId === delivery.id ? 'animate-spin' : ''}\n />\n {retryingId === delivery.id\n ? tr('channel.retrying', 'Retrying...')\n : tr('channel.retry', 'Retry')}\n </button>\n </PermissionGuard>\n )}\n </td>\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n </div>\n\n {/* Pagination footer */}\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {total > 0 && (\n <>\n {tr('channel.page', 'Page')}{' '}\n <span className=\"font-medium\">{currentPageIdx + 1}</span>\n {' · '}\n {tr('channel.totalRecords', 'Total')}: <span className=\"font-medium\">{total}</span>\n </>\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrevPage}\n disabled={!hasPrevPage || isLoadingAttempts}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft size={16} />\n {tr('channel.previous', 'Previous')}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleNextPage()}\n disabled={!hasNextPage || isLoadingAttempts}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DeliveryHistoryPage.js","names":[],"sources":["../../../../../src/channel/modules/channels/pages/DeliveryHistoryPage.tsx"],"sourcesContent":["/**\n * Delivery History Page\n *\n * Displays message delivery attempts for a specific channel with retry capability\n */\n\nimport { useState, useEffect, useCallback, useRef } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n ArrowLeft,\n Loader2,\n AlertCircle,\n CheckCircle2,\n XCircle,\n Clock,\n RefreshCw,\n Send,\n RotateCcw,\n Download,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\nimport { useChannel, type DeliveryStatus, type DeliveryAttempt } from '../../../hooks/useChannels';\nimport { useChannel as useChannelContext } from '../../../providers/ChannelProvider';\nimport { PermissionGuard } from '../../../components/PermissionGuard';\nimport { CHANNEL_PERMISSIONS } from '../../../constants/permissions';\nimport { useExportDeliveryAttempts } from '../hooks/useExport';\n\nconst STATUS_CONFIG: Record<\n DeliveryStatus,\n { label: string; color: string; icon: typeof CheckCircle2 }\n> = {\n PENDING: { label: 'Pending', color: 'text-muted-foreground bg-muted', icon: Clock },\n QUEUED: { label: 'Queued', color: 'text-accent-primary bg-accent-primary-subtle', icon: Clock },\n SENDING: { label: 'Sending', color: 'text-accent-primary bg-accent-primary/10', icon: Send },\n SENT: {\n label: 'Sent',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n DELIVERED: {\n label: 'Delivered',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n FAILED: {\n label: 'Failed',\n color: 'text-status-error-text bg-status-error-bg-subtle',\n icon: XCircle,\n },\n BOUNCED: {\n label: 'Bounced',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: AlertCircle,\n },\n REJECTED: {\n label: 'Rejected',\n color: 'text-status-error-text bg-status-error-bg-subtle',\n icon: XCircle,\n },\n RATE_LIMITED: {\n label: 'Rate Limited',\n color: 'text-status-warning-text bg-status-warning-bg-subtle',\n icon: Clock,\n },\n RETRYING: {\n label: 'Retrying',\n color: 'text-accent-primary bg-accent-primary/10',\n icon: RefreshCw,\n },\n OPENED: {\n label: 'Opened',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n CLICKED: {\n label: 'Clicked',\n color: 'text-status-success-text bg-status-success-bg-subtle',\n icon: CheckCircle2,\n },\n};\n\nexport function DeliveryHistoryPage() {\n const { channelId } = useParams<{ channelId: string }>();\n const navigate = useNavigate();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n const { organizationId } = useChannelContext();\n const {\n triggerExport: exportDeliveries,\n isExporting: isExportingDeliveries,\n exportError: exportDeliveriesError,\n } = useExportDeliveryAttempts(organizationId ?? undefined, channelId);\n const [isLoadingAttempts, setIsLoadingAttempts] = useState(true);\n const [attempts, setAttempts] = useState<DeliveryAttempt[]>([]);\n const [retryingId, setRetryingId] = useState<string | null>(null);\n const [retryError, setRetryError] = useState<string | null>(null);\n const [limit, setLimit] = useState(25);\n const [total, setTotal] = useState(0);\n // Cursor stack: index 0 is always undefined (first page), subsequent entries are cursors\n const [cursorStack, setCursorStack] = useState<(string | undefined)[]>([undefined]);\n const [currentPageIdx, setCurrentPageIdx] = useState(0);\n\n const { channel, isLoading, error, fetchDeliveryAttempts, retryDelivery } = useChannel(channelId);\n\n const loadAttempts = useCallback(\n async (cursor?: string) => {\n if (!channelId) return;\n setIsLoadingAttempts(true);\n setRetryError(null);\n try {\n const result = await fetchDeliveryAttempts(limit, cursor);\n setAttempts(result.items);\n setTotal(result.total);\n return result.nextCursor;\n } finally {\n setIsLoadingAttempts(false);\n }\n },\n [channelId, fetchDeliveryAttempts, limit]\n );\n\n const loadAttemptsRef = useRef(loadAttempts);\n const cursorStackRef = useRef(cursorStack);\n useEffect(() => {\n loadAttemptsRef.current = loadAttempts;\n cursorStackRef.current = cursorStack;\n });\n const isFirstPageRender = useRef(true);\n\n useEffect(() => {\n void Promise.resolve().then(() => {\n setCursorStack([undefined]);\n setCurrentPageIdx(0);\n void loadAttemptsRef.current(undefined);\n });\n }, [limit]);\n\n useEffect(() => {\n if (isFirstPageRender.current) {\n isFirstPageRender.current = false;\n return;\n }\n void Promise.resolve().then(() =>\n loadAttemptsRef.current(cursorStackRef.current[currentPageIdx])\n );\n }, [currentPageIdx]);\n\n // Poll for delivery attempt updates every 60 s on first page only\n useEffect(() => {\n if (!channelId || currentPageIdx !== 0) return;\n const timer = setInterval(() => {\n void loadAttempts(undefined);\n }, 60_000);\n return () => clearInterval(timer);\n }, [channelId, currentPageIdx, loadAttempts]);\n\n const handleNextPage = useCallback(async () => {\n const nextCursor = await loadAttempts(cursorStack[currentPageIdx]);\n if (nextCursor) {\n setCursorStack((prev) => {\n const next = [...prev];\n if (!next[currentPageIdx + 1]) {\n next[currentPageIdx + 1] = nextCursor;\n }\n return next;\n });\n }\n setCurrentPageIdx((p) => p + 1);\n }, [cursorStack, currentPageIdx, loadAttempts]);\n\n const handlePrevPage = useCallback(() => {\n const prevIdx = currentPageIdx - 1;\n setCurrentPageIdx(prevIdx);\n void loadAttempts(cursorStack[prevIdx]);\n }, [cursorStack, currentPageIdx, loadAttempts]);\n\n const hasNextPage = attempts.length === limit;\n const hasPrevPage = currentPageIdx > 0;\n\n const handleRetry = async (attemptId: string) => {\n setRetryingId(attemptId);\n setRetryError(null);\n try {\n const result = await retryDelivery(attemptId);\n if (!result.success) {\n setRetryError(result.error || 'Retry failed');\n } else {\n await loadAttempts(cursorStack[currentPageIdx]);\n }\n } finally {\n setRetryingId(null);\n }\n };\n\n const getStatusConfig = (status: DeliveryStatus) => {\n return STATUS_CONFIG[status] || STATUS_CONFIG.PENDING;\n };\n\n // Loading state\n if (isLoading) {\n return (\n <output\n className=\"flex items-center justify-center min-h-screen\"\n aria-label={tr('channel.loading', 'Loading')}\n >\n <Loader2 size={32} className=\"animate-spin text-accent-primary\" aria-hidden=\"true\" />\n </output>\n );\n }\n\n // Error state\n if (error || !channel) {\n return (\n <div className=\"p-6\">\n <div className=\"flex items-center gap-3 p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <div>\n <p className=\"text-sm font-medium text-status-error-text\">\n {error || 'Channel not found'}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n className=\"ml-auto px-3 py-1 text-sm text-status-error-text hover:bg-status-error-bg-subtle rounded transition-colors\"\n >\n Go Back\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_DELIVERY_READ}>\n <div className=\"space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`/channels/${channelId}`)}\n aria-label={tr('channel.backToChannel', 'Back to channel')}\n className=\"p-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors\"\n >\n <ArrowLeft size={20} aria-hidden=\"true\" />\n </button>\n <div>\n <h1 className=\"text-xl sm:text-2xl font-bold text-foreground\">\n {tr('channel.deliveryHistory', 'Delivery History')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {channel.name} - Message deliveries\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <select\n value={limit}\n onChange={(e) => setLimit(Number(e.target.value))}\n className=\"px-3 py-2 text-sm bg-surface border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-action-primary-bg/50 text-foreground\"\n >\n <option value={10}>10 results</option>\n <option value={25}>25 results</option>\n <option value={50}>50 results</option>\n <option value={100}>100 results</option>\n </select>\n <button\n type=\"button\"\n onClick={() => void loadAttempts(cursorStack[currentPageIdx])}\n disabled={isLoadingAttempts}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors disabled:opacity-50\"\n >\n <RefreshCw size={16} className={isLoadingAttempts ? 'animate-spin' : ''} />\n {tr('channel.refresh', 'Refresh')}\n </button>\n <PermissionGuard permission={CHANNEL_PERMISSIONS.CHANNEL_DELIVERY_EXPORT}>\n <button\n type=\"button\"\n onClick={() => void exportDeliveries()}\n disabled={isExportingDeliveries}\n className=\"flex items-center gap-2 px-4 py-2 text-sm font-medium text-fg border border-border hover:bg-bg-elevated rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n title={tr('channel.exportCSV', 'Export to CSV')}\n >\n {isExportingDeliveries ? (\n <Loader2 size={16} className=\"animate-spin\" />\n ) : (\n <Download size={16} />\n )}\n {tr('channel.export', 'Export')}\n </button>\n </PermissionGuard>\n </div>\n </div>\n\n {/* Export Error Banner */}\n {exportDeliveriesError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{exportDeliveriesError}</p>\n </div>\n )}\n\n {/* Error Banner */}\n {retryError && (\n <div className=\"p-4 bg-status-error-bg-subtle border border-status-error-border rounded-lg flex items-center gap-3\">\n <AlertCircle size={20} className=\"text-status-error-text\" />\n <p className=\"text-sm text-status-error-text flex-1\">{retryError}</p>\n <button\n type=\"button\"\n onClick={() => setRetryError(null)}\n className=\"text-status-error-text hover:text-status-error-text\"\n >\n {tr('channel.dismiss', 'Dismiss')}\n </button>\n </div>\n )}\n\n {/* Info Banner */}\n <div className=\"p-4 bg-surface border border-border rounded-lg\">\n <p className=\"text-sm text-muted-foreground\">\n Delivery history shows all message attempts sent through this channel. Data is retained\n for 7 days. Click retry on a failed delivery to attempt sending again.\n </p>\n </div>\n\n {/* Deliveries List */}\n <div className=\"bg-surface border border-border rounded-lg overflow-hidden\">\n <div className=\"overflow-x-auto min-w-full\">\n <table className=\"w-full min-w-[700px]\">\n <thead className=\"bg-muted/50 border-b border-border\">\n <tr>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.status', 'Status')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.content', 'Content')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.sentAt', 'Sent At')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.retries', 'Retries')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.error', 'Error')}\n </th>\n <th className=\"px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {tr('channel.actions', 'Actions')}\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border\">\n {isLoadingAttempts ? (\n <tr>\n <td colSpan={6} className=\"px-4 py-12 text-center\">\n <Loader2 size={24} className=\"animate-spin text-accent-primary mx-auto\" />\n <p className=\"text-sm text-muted-foreground mt-2\">\n Loading delivery attempts…\n </p>\n </td>\n </tr>\n ) : attempts.length === 0 ? (\n <tr>\n <td colSpan={6} className=\"px-4 py-12 text-center\">\n <div className=\"flex flex-col items-center gap-2\">\n <Send size={32} className=\"text-muted-foreground\" />\n <p className=\"text-sm font-medium text-foreground\">\n {tr('channel.noDeliveryAttempts', 'No delivery attempts yet')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {tr(\n 'channel.sendMessageToSeeHistory',\n 'Send a message through this channel to see delivery history'\n )}\n </p>\n </div>\n </td>\n </tr>\n ) : (\n attempts.map((delivery) => {\n const statusConfig = getStatusConfig(delivery.status as DeliveryStatus);\n const StatusIcon = statusConfig.icon;\n const canRetry =\n delivery.status === 'FAILED' ||\n delivery.status === 'BOUNCED' ||\n delivery.status === 'REJECTED';\n return (\n <tr key={delivery.id} className=\"hover:bg-muted/30 transition-colors\">\n <td className=\"px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <StatusIcon size={14} className={statusConfig.color.split(' ')[0]} />\n <span className={`text-sm font-medium ${statusConfig.color}`}>\n {statusConfig.label}\n </span>\n </div>\n </td>\n <td className=\"px-4 py-3 text-sm text-foreground truncate max-w-xs\">\n {delivery.content}\n </td>\n <td className=\"px-4 py-3 text-sm text-muted-foreground\">\n {delivery.sentAt ? new Date(delivery.sentAt).toLocaleString() : '-'}\n </td>\n <td className=\"px-4 py-3 text-sm text-muted-foreground\">\n {delivery.retryCount}\n </td>\n <td className=\"px-4 py-3 text-sm text-status-error-text max-w-xs truncate\">\n {delivery.errorMessage || '-'}\n </td>\n <td className=\"px-4 py-3\">\n {canRetry && (\n <PermissionGuard\n permission={CHANNEL_PERMISSIONS.CHANNEL_DELIVERY_EXECUTE}\n >\n <button\n type=\"button\"\n onClick={() => handleRetry(delivery.id)}\n disabled={retryingId === delivery.id}\n className=\"flex items-center gap-1 px-2 py-1 text-xs font-medium text-accent-primary hover:bg-accent-primary/10 rounded transition-colors disabled:opacity-50\"\n title={tr('channel.retryDelivery', 'Retry this delivery')}\n >\n <RotateCcw\n size={12}\n className={retryingId === delivery.id ? 'animate-spin' : ''}\n />\n {retryingId === delivery.id\n ? tr('channel.retrying', 'Retrying...')\n : tr('channel.retry', 'Retry')}\n </button>\n </PermissionGuard>\n )}\n </td>\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n </div>\n\n {/* Pagination footer */}\n <div className=\"flex items-center justify-between\">\n <p className=\"text-sm text-muted-foreground\">\n {total > 0 && (\n <>\n {tr('channel.page', 'Page')}{' '}\n <span className=\"font-medium\">{currentPageIdx + 1}</span>\n {' · '}\n {tr('channel.totalRecords', 'Total')}: <span className=\"font-medium\">{total}</span>\n </>\n )}\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={handlePrevPage}\n disabled={!hasPrevPage || isLoadingAttempts}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft size={16} />\n {tr('channel.previous', 'Previous')}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleNextPage()}\n disabled={!hasNextPage || isLoadingAttempts}\n className=\"flex items-center gap-1 px-3 py-1.5 text-sm border border-border rounded-lg hover:bg-muted transition-colors disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n {tr('channel.next', 'Next')}\n <ChevronRight size={16} />\n </button>\n </div>\n </div>\n </div>\n </PermissionGuard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -193,7 +193,7 @@ function O() {
|
|
|
193
193
|
}), O("channel.refresh", "Refresh")]
|
|
194
194
|
}),
|
|
195
195
|
/* @__PURE__ */ T(a, {
|
|
196
|
-
permission: o.
|
|
196
|
+
permission: o.CHANNEL_WEBHOOK_EXPORT,
|
|
197
197
|
children: /* @__PURE__ */ E("button", {
|
|
198
198
|
type: "button",
|
|
199
199
|
onClick: () => void A(),
|