Restyled concoleui notification tab

This commit is contained in:
2026-10-29 13:13:42 +02:00
parent 6ea2ba9975
commit c3ef1c56c2
@@ -73,6 +73,8 @@ import { useToast } from '../components/ui/Toast';
import { ExportButton } from '../components/ui/ExportButton';
import { ExportColumn } from '../utils/exportUtils';
import { useTableSort, SortableTh } from '../utils/tableSort';
import { MarkdownViewer } from '../components/notifications/MarkdownViewer';
import { isHtmlContent, stripMarkdown } from '../lib/markdownUtils';
import { UserRoleBadge } from '../components/ui/StatusBadge';
import { useAuth } from '../auth/AuthProvider';
import {
@@ -439,6 +441,9 @@ export const SettingsPage: React.FC = () => {
const [selectedNotificationForDetail, setSelectedNotificationForDetail] = useState<Notification | null>(null);
const [isNotificationDetailModalOpen, setIsNotificationDetailModalOpen] = useState(false);
const [copiedNotificationPayload, setCopiedNotificationPayload] = useState(false);
const [isNotificationRenderModalOpen, setIsNotificationRenderModalOpen] = useState(false);
const [renderedViewTab, setRenderedViewTab] = useState<'rendered' | 'source'>('rendered');
const [copiedRenderedContent, setCopiedRenderedContent] = useState(false);
const effectiveNotificationOrgId =
notificationOrgFilter || (organisations[0]?.id ? String(organisations[0].id) : '1');
@@ -455,7 +460,7 @@ export const SettingsPage: React.FC = () => {
});
const notificationExportColumns: ExportColumn<Notification>[] = [
{ header: 'Notification ID', key: 'id' },
{ header: 'ID', key: 'id' },
{ header: 'Created Date', key: 'createdAt', format: (val) => formatDate(val) },
{ header: 'Sent Date', key: 'sentAt', format: (val) => (val ? formatDate(val) : 'Pending/Queued') },
{ header: 'Platform / Channel', key: 'platform', format: (val) => getNotificationPlatformLabel(val) },
@@ -463,7 +468,14 @@ export const SettingsPage: React.FC = () => {
{ header: 'Subject', key: 'subject', format: (val) => val || '—' },
{ header: 'Recipient UUID', key: 'recipient' },
{ header: 'Destination', key: 'destination', format: (val) => val || '—' },
{ header: 'Target Organisation', key: 'organisationId', format: (val) => `Org #${val}` },
{
header: 'Target Organisation',
key: 'organisationId',
format: (val) => {
const org = organisations.find((o) => String(o.id) === String(val));
return org?.name || `Org #${val}`;
},
},
{ header: 'Message Content', key: 'message', format: (val) => val || '—' },
{ header: 'Error Diagnostics', key: 'error', format: (val) => val || 'None' },
];
@@ -483,13 +495,16 @@ export const SettingsPage: React.FC = () => {
const recip = (item.recipient || '').toLowerCase();
const dest = (item.destination || '').toLowerCase();
const err = (item.error || '').toLowerCase();
const org = organisations.find((o) => String(o.id) === String(item.organisationId));
const orgName = (org?.name || '').toLowerCase();
return (
id.includes(q) ||
subj.includes(q) ||
msg.includes(q) ||
recip.includes(q) ||
dest.includes(q) ||
err.includes(q)
err.includes(q) ||
orgName.includes(q)
);
});
@@ -498,7 +513,13 @@ export const SettingsPage: React.FC = () => {
sortColumn: notifSortCol,
sortDirection: notifSortDir,
handleSort: handleNotifSort,
} = useTableSort(filteredNotifications, 'createdAt', 'desc');
} = useTableSort(filteredNotifications, 'createdAt', 'desc', {
organisationId: (item: Notification) => {
const org = organisations.find((o) => String(o.id) === String(item.organisationId));
return org?.name || `Organisation #${item.organisationId}`;
},
destination: (item: Notification) => item.destination || '',
});
const resolveInvitePlatform = (inv: Invite): NotificationPlatform => {
if (inv.platform !== undefined && inv.platform !== null) {
@@ -1664,11 +1685,11 @@ export const SettingsPage: React.FC = () => {
</div>
<div className="overflow-x-auto min-w-0 w-full">
<table className="w-full text-left border-collapse text-xs min-w-[900px]">
<table className="w-full text-left border-collapse text-xs min-w-[760px]">
<thead>
<tr className="border-b border-slate-800 bg-slate-900/40 text-slate-400 font-medium">
<SortableTh column="id" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Notification ID
<SortableTh column="id" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-3 w-20">
ID
</SortableTh>
<SortableTh column="platform" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Channel / Platform
@@ -1676,11 +1697,8 @@ export const SettingsPage: React.FC = () => {
<SortableTh column="status" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Status
</SortableTh>
<SortableTh column="subject" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Subject &amp; Message Payload
</SortableTh>
<SortableTh column="recipient" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Recipient &amp; Destination
<SortableTh column="destination" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Destination
</SortableTh>
<SortableTh column="organisationId" currentColumn={notifSortCol} direction={notifSortDir} onSort={handleNotifSort} className="py-3.5 px-4">
Target Org
@@ -1694,14 +1712,14 @@ export const SettingsPage: React.FC = () => {
<tbody className="divide-y divide-slate-800/50">
{loadingNotifications ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<td colSpan={7} className="py-12 text-center text-slate-500">
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2 text-blue-400" />
Loading communication records for Organisation #{effectiveNotificationOrgId}...
</td>
</tr>
) : sortedNotifications.length === 0 ? (
<tr>
<td colSpan={8} className="py-12 text-center text-slate-500">
<td colSpan={7} className="py-12 text-center text-slate-500">
<Bell className="w-8 h-8 text-slate-600 mx-auto mb-2" />
No notification records returned for Organisation #{effectiveNotificationOrgId}.
</td>
@@ -1709,9 +1727,11 @@ export const SettingsPage: React.FC = () => {
) : (
sortedNotifications.map((item) => {
const statusBadge = getNotificationStatusBadge(item.status);
const org = organisations.find((o) => String(o.id) === String(item.organisationId));
const orgName = org?.name || (item.organisationId ? `Organisation #${item.organisationId}` : '—');
return (
<tr key={item.id} className="hover:bg-slate-800/40 transition-colors">
<td className="py-3 px-4 font-mono text-slate-300 font-semibold text-[11px]">
<td className="py-3 px-3 font-mono text-slate-300 font-semibold text-[11px] w-20 whitespace-nowrap">
#{item.id}
</td>
<td className="py-3 px-4">
@@ -1726,22 +1746,13 @@ export const SettingsPage: React.FC = () => {
{statusBadge.label}
</span>
</td>
<td className="py-3 px-4 max-w-xs">
<div className="text-white font-medium truncate">{item.subject || 'Statutory Notice'}</div>
<div className="text-slate-400 text-[11px] truncate">{item.message || '—'}</div>
</td>
<td className="py-3 px-4">
<div className="font-mono text-slate-300 text-[11px] truncate max-w-[180px]" title={item.recipient}>
{item.recipient}
</div>
{item.destination && (
<div className="font-mono text-slate-400 text-[10px] truncate max-w-[180px]" title={item.destination}>
{item.destination}
</div>
)}
<span className="font-mono text-slate-300 text-[11px] truncate max-w-[200px] block" title={item.destination || '—'}>
{item.destination || '—'}
</span>
</td>
<td className="py-3 px-4 text-slate-400 text-[11px]">
Org #{item.organisationId}
<td className="py-3 px-4 text-slate-300 text-xs font-medium">
<span title={`Organisation #${item.organisationId}`}>{orgName}</span>
</td>
<td className="py-3 px-4 font-mono text-slate-400 text-[11px]">
{formatDate(item.createdAt)}
@@ -3001,8 +3012,19 @@ export const SettingsPage: React.FC = () => {
{/* Message */}
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase mb-1">Message Content:</span>
<div className="p-3 rounded-lg bg-slate-900 border border-slate-800 text-slate-200 leading-relaxed font-sans text-xs whitespace-pre-wrap">
<div className="flex items-center justify-between mb-1">
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Message Content:</span>
<button
type="button"
onClick={() => setIsNotificationRenderModalOpen(true)}
className="inline-flex items-center gap-1.5 px-2.5 py-1 text-[11px] font-semibold text-blue-400 hover:text-white bg-blue-500/10 hover:bg-blue-600 border border-blue-500/30 hover:border-blue-500 rounded-lg transition cursor-pointer shadow-xs"
title="Pop up rendered message content"
>
<Eye className="w-3.5 h-3.5" />
<span>Read Rendered Contents</span>
</button>
</div>
<div className="p-3 rounded-lg bg-slate-900 border border-slate-800 text-slate-200 leading-relaxed font-sans text-xs whitespace-pre-wrap max-h-48 overflow-y-auto">
{selectedNotificationForDetail.message || '—'}
</div>
</div>
@@ -3026,8 +3048,10 @@ export const SettingsPage: React.FC = () => {
{/* Organisation & Tenant Grid */}
<div className="grid grid-cols-2 gap-3 border-t border-slate-800/80 pt-2.5">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Organisation ID:</span>
<span className="text-white font-medium">Org #{selectedNotificationForDetail.organisationId}</span>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Target Organisation:</span>
<span className="text-white font-medium">
{organisations.find((o) => String(o.id) === String(selectedNotificationForDetail.organisationId))?.name || `Org #${selectedNotificationForDetail.organisationId}`}
</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Tenant Scope:</span>
@@ -3109,13 +3133,22 @@ export const SettingsPage: React.FC = () => {
</div>
</div>
<div className="pt-2 flex justify-end gap-2">
<div className="pt-2 flex justify-between items-center gap-2">
<button
type="button"
onClick={() => setIsNotificationRenderModalOpen(true)}
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl transition cursor-pointer shadow-lg shadow-blue-600/25"
>
<Eye className="w-3.5 h-3.5" />
<span>Read Rendered Contents</span>
</button>
<button
type="button"
onClick={() => {
setIsNotificationDetailModalOpen(false);
setSelectedNotificationForDetail(null);
setCopiedNotificationPayload(false);
setIsNotificationRenderModalOpen(false);
}}
className="px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 rounded-xl transition cursor-pointer"
>
@@ -3125,6 +3158,209 @@ export const SettingsPage: React.FC = () => {
</div>
)}
</Modal>
{/* ============================================================== */}
{/* Modal: Rendered Notification Contents Preview */}
{/* ============================================================== */}
<Modal
isOpen={isNotificationRenderModalOpen}
onClose={() => setIsNotificationRenderModalOpen(false)}
title={`Rendered Notification: #${selectedNotificationForDetail?.id || ''}`}
subtitle={
selectedNotificationForDetail
? `${getNotificationPlatformLabel(selectedNotificationForDetail.platform)} Delivery View • ${
selectedNotificationForDetail.platform === NotificationPlatform.InApp ? 'Markdown Format' : 'HTML Format'
}`
: 'Rendered message output'
}
maxWidth="4xl"
zIndex="z-[10000]"
>
{selectedNotificationForDetail && (() => {
const isEmail = selectedNotificationForDetail.platform === NotificationPlatform.Email;
const isHtml = isEmail || isHtmlContent(selectedNotificationForDetail.message || '');
const targetOrg = organisations.find(
(o) => String(o.id) === String(selectedNotificationForDetail.organisationId)
);
const orgName = targetOrg?.name || `Organisation #${selectedNotificationForDetail.organisationId}`;
return (
<div className="space-y-4 text-xs">
{/* Header Info Bar */}
<div className="p-3.5 bg-[#0d121f] rounded-xl border border-slate-800 flex flex-wrap items-center justify-between gap-2.5">
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-blue-500/10 text-blue-400 border border-blue-500/20">
{getNotificationPlatformLabel(selectedNotificationForDetail.platform)}
</span>
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-purple-500/10 text-purple-400 border border-purple-500/20">
{isHtml ? 'HTML Format' : 'Markdown Format'}
</span>
<span className="text-slate-400 text-[11px]">
Target Org: <strong className="text-white font-medium">{orgName}</strong>
</span>
{selectedNotificationForDetail.destination && (
<span className="text-slate-400 text-[11px] font-mono">
Destination: <strong className="text-slate-200">{selectedNotificationForDetail.destination}</strong>
</span>
)}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center bg-slate-900 border border-slate-700/80 rounded-lg p-0.5">
<button
type="button"
onClick={() => setRenderedViewTab('rendered')}
className={`px-2.5 py-1 rounded text-[11px] font-semibold transition cursor-pointer ${
renderedViewTab === 'rendered'
? 'bg-blue-600 text-white shadow-xs'
: 'text-slate-400 hover:text-white'
}`}
>
Rendered
</button>
<button
type="button"
onClick={() => setRenderedViewTab('source')}
className={`px-2.5 py-1 rounded text-[11px] font-semibold transition cursor-pointer ${
renderedViewTab === 'source'
? 'bg-blue-600 text-white shadow-xs'
: 'text-slate-400 hover:text-white'
}`}
>
Raw Source
</button>
</div>
<button
type="button"
onClick={() => {
const textToCopy =
renderedViewTab === 'source'
? selectedNotificationForDetail.message || ''
: isHtml
? selectedNotificationForDetail.message || ''
: stripMarkdown(selectedNotificationForDetail.message || '');
navigator.clipboard.writeText(textToCopy);
setCopiedRenderedContent(true);
setTimeout(() => setCopiedRenderedContent(false), 2000);
}}
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white text-[11px] font-medium border border-slate-700 transition cursor-pointer"
title="Copy message contents"
>
{copiedRenderedContent ? (
<>
<Check className="w-3 h-3 text-emerald-400" />
<span className="text-emerald-300">Copied</span>
</>
) : (
<>
<Copy className="w-3 h-3" />
<span>Copy</span>
</>
)}
</button>
</div>
</div>
{/* Subject */}
{selectedNotificationForDetail.subject && (
<div className="px-4 py-2.5 bg-slate-900/60 rounded-xl border border-slate-800">
<span className="text-[10px] uppercase font-semibold text-slate-500 block">Subject</span>
<div className="text-sm font-semibold text-white">
{selectedNotificationForDetail.subject}
</div>
</div>
)}
{/* Body Content */}
{renderedViewTab === 'source' ? (
<div className="space-y-1">
<pre className="p-4 rounded-xl bg-[#070a12] border border-slate-800 text-slate-200 font-mono text-xs overflow-x-auto max-h-[480px] whitespace-pre-wrap leading-relaxed">
{selectedNotificationForDetail.message || '—'}
</pre>
</div>
) : isHtml ? (
<div className="space-y-2">
<div className="flex items-center justify-between text-[11px] text-slate-400 px-1">
<span className="flex items-center gap-1.5 text-blue-400">
<FileText className="w-3.5 h-3.5" />
Rendered Email HTML
</span>
<span className="text-[10px] text-slate-500">Sandboxed HTML Canvas</span>
</div>
<div className="rounded-xl overflow-hidden border border-slate-700/80 bg-white shadow-xl">
<iframe
title={`Rendered HTML for Notification #${selectedNotificationForDetail.id}`}
srcDoc={
selectedNotificationForDetail.message?.trim().startsWith('<!DOCTYPE') ||
selectedNotificationForDetail.message?.trim().startsWith('<html')
? selectedNotificationForDetail.message
: `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
margin: 20px;
padding: 0;
color: #1e293b;
line-height: 1.6;
background-color: #ffffff;
font-size: 14px;
}
a { color: #2563eb; text-decoration: underline; }
h1, h2, h3, h4 { color: #0f172a; margin-top: 1.2em; margin-bottom: 0.5em; }
table { width: 100%; border-collapse: collapse; margin: 16px 0; }
th { background: #f1f5f9; padding: 8px 12px; text-align: left; border: 1px solid #cbd5e1; font-weight: 600; font-size: 12px; }
td { padding: 8px 12px; border: 1px solid #cbd5e1; font-size: 13px; }
blockquote { border-left: 4px solid #3b82f6; padding-left: 12px; margin: 12px 0; color: #475569; font-style: italic; }
code { background: #f1f5f9; padding: 2px 4px; border-radius: 4px; font-family: monospace; font-size: 12px; }
pre { background: #0f172a; color: #f8fafc; padding: 12px; border-radius: 6px; overflow-x: auto; }
</style>
</head>
<body>
${selectedNotificationForDetail.message || '<p style="color: #64748b; font-style: italic;">No message body provided</p>'}
</body>
</html>`
}
className="w-full h-[460px] border-0"
sandbox="allow-same-origin allow-popups"
/>
</div>
</div>
) : (
<div className="space-y-2">
<div className="flex items-center justify-between text-[11px] text-slate-400 px-1">
<span className="flex items-center gap-1.5 text-blue-400">
<FileText className="w-3.5 h-3.5" />
Rendered In-App Markdown
</span>
<span className="text-[10px] text-slate-500">Semantic Markdown</span>
</div>
<MarkdownViewer
content={selectedNotificationForDetail.message || ''}
subject={selectedNotificationForDetail.subject || undefined}
showBrandingHeader={true}
className="max-h-[480px] overflow-y-auto"
/>
</div>
)}
<div className="pt-2 flex justify-end">
<button
type="button"
onClick={() => setIsNotificationRenderModalOpen(false)}
className="px-4 py-2 text-xs font-semibold text-white bg-slate-800 hover:bg-slate-700 rounded-xl transition cursor-pointer"
>
Back to Inspection
</button>
</div>
</div>
);
})()}
</Modal>
</div>
);
};