import { useEffect, useMemo, useState } from "react"; import { Bell, Check, ExternalLink, RefreshCw, Send, XCircle } from "lucide-react"; import { AdminIconButton, Button, DismissibleAlert, SegmentedControl, SelectionList, SelectionListItem, StatusBadge, hasScope, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { deliverPendingNotifications, listNotifications, updateNotification, type NotificationMessage } from "../../api/notifications"; import { safeNotificationActionUrl } from "../../security/actionUrl"; type StatusFilter = | "all" | "pending" | "queued" | "sending" | "accepted" | "paused" | "sent" | "failed" | "skipped" | "cancelled"; const statusFilters: StatusFilter[] = [ "all", "pending", "queued", "sending", "accepted", "paused", "sent", "failed", "skipped", "cancelled" ]; export default function NotificationCenterPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) { const [notifications, setNotifications] = useState([]); const [selectedId, setSelectedId] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const canRead = hasScope(auth, "notifications:notification:read"); const canWrite = hasScope(auth, "notifications:notification:write"); const canDispatch = hasScope(auth, "notifications:delivery:dispatch"); const selected = useMemo(() => notifications.find((item) => item.id === selectedId) || notifications[0] || null, [notifications, selectedId]); const unreadCount = notifications.filter((item) => !item.read_at && !["cancelled", "skipped"].includes(item.status)).length; useEffect(() => { if (!canRead) { setLoading(false); return; } void load(); }, [canRead, settings.apiBaseUrl, settings.apiKey, settings.accessToken, statusFilter]); async function load() { setLoading(true); setError(""); try { const response = await listNotifications(settings, { status: statusFilter === "all" ? undefined : statusFilter, limit: 200 }); setNotifications(response.notifications); setSelectedId((current) => current && response.notifications.some((item) => item.id === current) ? current : response.notifications[0]?.id ?? ""); } catch (err) { setError(errorMessage(err)); } finally { setLoading(false); } } async function markSelected(status: "read" | "acknowledged" | "cancelled") { if (!selected || !canWrite) return; setBusy(true); setError(""); try { const next = await updateNotification(settings, selected.id, { status }); setNotifications((items) => items.map((item) => item.id === next.id ? next : item)); notifyNotificationsChanged(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } async function runDelivery() { if (!canDispatch) return; setBusy(true); setError(""); try { await deliverPendingNotifications(settings, 50); await load(); notifyNotificationsChanged(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } if (!canRead) { return (

Notifications

You do not have permission to read notifications in this tenant.

); } return (
{selected?.subject || selected?.event_kind || "Notification center"}
{canDispatch ? ( ) : null}
{error ? {error} : null} {selected ? : (

Notifications

Select a notification to inspect delivery state, source context, and content.

)}
); } function NotificationDetails({ notification }: { notification: NotificationMessage }) { const actionUrl = safeNotificationActionUrl(notification.action_url); return (
{notification.channel} {formatDate(notification.created_at)}

{notification.subject || notification.event_kind}

{notification.body_text ?

{notification.body_text}

:

No message body was provided.

} {actionUrl ? ( Open related item ) : null}

Source and delivery

Source
{notification.source_module} / {notification.source_resource_type}
Resource
{notification.source_resource_id || "None"}
Recipient
{notification.recipient_label || notification.recipient || notification.recipient_id || "None"}
Priority
{notification.priority}
Queued
{formatDate(notification.queued_at)}
Sent
{formatDate(notification.sent_at)}
Read
{formatDate(notification.read_at)}
Attempts
{notification.attempt_count}
{notification.last_error ?

{notification.last_error}

: null}

Delivery attempts

{notification.attempts.length === 0 ?

No delivery attempt has been recorded yet.

: null} {notification.attempts.map((attempt) => (
{attempt.provider || attempt.channel} {formatStatus(attempt.status)} {formatDate(attempt.started_at)} - {formatDate(attempt.finished_at)} {attempt.error ?

{attempt.error}

: null}
))}
); } function formatStatus(value: string): string { return value.replace(/_/g, " "); } function formatDate(value?: string | null): string { if (!value) return "Not set"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date); } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : "Request failed"; } function notifyNotificationsChanged(): void { window.dispatchEvent(new CustomEvent("govoplan:notifications-changed")); }