diff --git a/docs/outreach/.~lock.NYT-OpEd-Amoral-Intelligence.docx# b/docs/outreach/.~lock.NYT-OpEd-Amoral-Intelligence.docx# deleted file mode 100644 index 3b21ac73..00000000 --- a/docs/outreach/.~lock.NYT-OpEd-Amoral-Intelligence.docx# +++ /dev/null @@ -1 +0,0 @@ -,theflow,the-flow,24.10.2025 08:46,file:///home/theflow/.config/libreoffice/4; \ No newline at end of file diff --git a/public/admin/contact-management.html b/public/admin/contact-management.html new file mode 100644 index 00000000..17b882da --- /dev/null +++ b/public/admin/contact-management.html @@ -0,0 +1,117 @@ + + + + + + Contact Management | Tractatus Admin + + + + + +
+ +
+ + +
+

Contact Management

+

Manage contact form submissions and track response times

+
+ + +
+
+
Total Contacts
+
-
+
+
+
New
+
-
+
+
+
Assigned
+
-
+
+
+
Responded
+
-
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+

Contact Submissions

+
+
+ +
+
+ +
+ + + + + + + + diff --git a/public/admin/crm-dashboard.html b/public/admin/crm-dashboard.html new file mode 100644 index 00000000..23ca9dbe --- /dev/null +++ b/public/admin/crm-dashboard.html @@ -0,0 +1,144 @@ + + + + + + CRM Dashboard | Tractatus Admin + + + + + +
+ +
+ + +
+

Multi-Project CRM

+

Unified relationship management across all projects

+
+ + +
+
+
Total Contacts
+
-
+
Across all projects
+
+
+
Organizations
+
-
+
Multiple types
+
+
+
SLA Compliance
+
-
+
Response rate
+
+
+
SLA Breaches
+
-
+
Pending responses
+
+
+ + +
+
+

🚨 SLA Alerts

+
+
+ +
+
+ + +
+ +
+ +
+ + +
+ +
+
+

Unified Contacts

+
+ + +
+
+
+ +
+
+ + + + + + + + + +
+
+ +
+ + + + + diff --git a/public/admin/unified-inbox.html b/public/admin/unified-inbox.html new file mode 100644 index 00000000..3be3c351 --- /dev/null +++ b/public/admin/unified-inbox.html @@ -0,0 +1,116 @@ + + + + + + Unified Inbox | Tractatus Admin + + + + + +
+ +
+ + +
+

Unified Inbox

+

All communications in one place: contacts, media inquiries, and case submissions

+
+ + +
+
+
Total Items
+
-
+
+
+
New / Pending
+
-
+
+
+
Assigned / Triaged
+
-
+
+
+
Responded
+
-
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+

Inbox Items

+
+
+ +
+
+ +
+ + + + + + + + diff --git a/public/case-submission.html b/public/case-submission.html index a3fc5c22..7959b6b5 100644 --- a/public/case-submission.html +++ b/public/case-submission.html @@ -194,7 +194,7 @@
-

diff --git a/public/js/admin/contact-management.js b/public/js/admin/contact-management.js new file mode 100644 index 00000000..df61a70e --- /dev/null +++ b/public/js/admin/contact-management.js @@ -0,0 +1,447 @@ +/** + * Contact Management Admin Page + * View and manage contact form submissions + */ + +let allContacts = []; +let currentContact = null; + +// Initialize page +document.addEventListener('DOMContentLoaded', async () => { + await loadStats(); + await loadContacts(); + setupEventListeners(); +}); + +/** + * Load statistics + */ +async function loadStats() { + try { + const token = localStorage.getItem('admin_token'); + const response = await fetch('/api/contact/admin/stats', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error('Failed to load stats'); + + const data = await response.json(); + const stats = data.stats; + + document.getElementById('stat-total').textContent = stats.total; + document.getElementById('stat-new').textContent = stats.by_status.new; + document.getElementById('stat-assigned').textContent = stats.by_status.assigned; + document.getElementById('stat-responded').textContent = stats.by_status.responded; + + } catch (error) { + console.error('Error loading stats:', error); + } +} + +/** + * Load contacts + */ +async function loadContacts() { + try { + const token = localStorage.getItem('admin_token'); + const status = document.getElementById('filter-status').value; + const type = document.getElementById('filter-type').value; + const priority = document.getElementById('filter-priority').value; + + let url = '/api/contact/admin/list?limit=50'; + if (status) url += `&status=${status}`; + if (type) url += `&type=${type}`; + if (priority) url += `&priority=${priority}`; + + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error('Failed to load contacts'); + + const data = await response.json(); + allContacts = data.contacts; + + renderContacts(); + + } catch (error) { + console.error('Error loading contacts:', error); + showError('Failed to load contacts'); + } +} + +/** + * Render contacts list + */ +function renderContacts() { + const container = document.getElementById('contacts-container'); + + if (allContacts.length === 0) { + container.innerHTML = \` +

+

No contacts found matching the selected filters.

+
+ \`; + return; + } + + container.innerHTML = allContacts.map(contact => \` +
+
+
+
+

\${escapeHtml(contact.contact.name)}

+ \${getTypeBadge(contact.type)} + \${getPriorityBadge(contact.priority)} + \${getStatusBadge(contact.status)} +
+

+ Email: \${escapeHtml(contact.contact.email)} + \${contact.contact.organization ? \` | Org: \${escapeHtml(contact.contact.organization)}\` : ''} +

+ \${contact.inquiry.subject ? \`

\${escapeHtml(contact.inquiry.subject)}

\` : ''} +

\${escapeHtml(contact.inquiry.message)}

+
+
+
\${formatDate(contact.created_at)}
+ \${contact.response.sent_at ? \`
Responded \${formatDate(contact.response.sent_at)}
\` : ''} +
+
+
+ \`).join(''); + + // Add click listeners + container.querySelectorAll('[data-contact-id]').forEach(el => { + el.addEventListener('click', () => { + const contactId = el.getAttribute('data-contact-id'); + showContactDetail(contactId); + }); + }); +} + +// ... rest of file continues in next command + +/** + * Show contact detail modal + */ +async function showContactDetail(contactId) { + try { + const token = localStorage.getItem('admin_token'); + const response = await fetch(\`/api/contact/admin/\${contactId}\`, { + headers: { + 'Authorization': \`Bearer \${token}\` + } + }); + + if (!response.ok) throw new Error('Failed to load contact'); + + const data = await response.json(); + currentContact = data.contact; + + renderContactDetail(currentContact); + + document.getElementById('contact-detail-modal').classList.remove('hidden'); + + } catch (error) { + console.error('Error loading contact detail:', error); + alert('Failed to load contact details'); + } +} + +/** + * Render contact detail + */ +function renderContactDetail(contact) { + const content = document.getElementById('contact-detail-content'); + + content.innerHTML = \` +
+ +
+

Contact Information

+
+
+ Name: +
\${escapeHtml(contact.contact.name)}
+
+
+ Email: + +
+ \${contact.contact.organization ? \` +
+ Organization: +
\${escapeHtml(contact.contact.organization)}
+
+ \` : ''} + \${contact.contact.phone ? \` +
+ Phone: +
\${escapeHtml(contact.contact.phone)}
+
+ \` : ''} +
+
+ + +
+

Inquiry

+
+
+ \${getTypeBadge(contact.type)} + \${getPriorityBadge(contact.priority)} + \${getStatusBadge(contact.status)} +
+ \${contact.inquiry.subject ? \` +
+ Subject: +
\${escapeHtml(contact.inquiry.subject)}
+
+ \` : ''} +
+ Message: +
\${escapeHtml(contact.inquiry.message)}
+
+
+
+ + +
+

Metadata

+
+
Submitted: \${formatDate(contact.created_at)}
+
Source: \${contact.source}
+ \${contact.metadata.source_page ? \`
Source Page: \${contact.metadata.source_page}
\` : ''} + \${contact.metadata.ip ? \`
IP: \${contact.metadata.ip}
\` : ''} +
+
+ + + \${contact.response.sent_at ? \` +
+

Response

+
+
Responded: \${formatDate(contact.response.sent_at)}
+ \${contact.response.content ? \`
\${escapeHtml(contact.response.content)}
\` : ''} +
+
+ \` : ''} + + +
+ \${contact.status === 'new' ? \` + + \` : ''} + \${contact.status === 'assigned' ? \` + + \` : ''} + \${contact.status !== 'closed' ? \` + + \` : ''} + +
+
+ \`; +} + +/** + * Update contact status + */ +async function updateContactStatus(contactId, newStatus) { + try { + const token = localStorage.getItem('admin_token'); + const response = await fetch(\`/api/contact/admin/\${contactId}\`, { + method: 'PUT', + headers: { + 'Authorization': \`Bearer \${token}\`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ status: newStatus }) + }); + + if (!response.ok) throw new Error('Failed to update status'); + + await loadStats(); + await loadContacts(); + closeDetailModal(); + + } catch (error) { + console.error('Error updating status:', error); + alert('Failed to update status'); + } +} + +/** + * Mark as responded + */ +async function markAsResponded(contactId) { + const responseContent = prompt('Enter response summary (optional):'); + + try { + const token = localStorage.getItem('admin_token'); + const response = await fetch(\`/api/contact/admin/\${contactId}/respond\`, { + method: 'POST', + headers: { + 'Authorization': \`Bearer \${token}\`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ content: responseContent || 'Responded via email' }) + }); + + if (!response.ok) throw new Error('Failed to mark as responded'); + + await loadStats(); + await loadContacts(); + closeDetailModal(); + + } catch (error) { + console.error('Error marking as responded:', error); + alert('Failed to mark as responded'); + } +} + +/** + * Delete contact + */ +async function deleteContact(contactId) { + if (!confirm('Are you sure you want to delete this contact? This cannot be undone.')) { + return; + } + + try { + const token = localStorage.getItem('admin_token'); + const response = await fetch(\`/api/contact/admin/\${contactId}\`, { + method: 'DELETE', + headers: { + 'Authorization': \`Bearer \${token}\` + } + }); + + if (!response.ok) throw new Error('Failed to delete contact'); + + await loadStats(); + await loadContacts(); + closeDetailModal(); + + } catch (error) { + console.error('Error deleting contact:', error); + alert('Failed to delete contact'); + } +} + +/** + * Setup event listeners + */ +function setupEventListeners() { + document.getElementById('filter-status').addEventListener('change', loadContacts); + document.getElementById('filter-type').addEventListener('change', loadContacts); + document.getElementById('filter-priority').addEventListener('change', loadContacts); + document.getElementById('refresh-btn').addEventListener('click', () => { + loadStats(); + loadContacts(); + }); + + document.getElementById('close-detail-modal').addEventListener('click', closeDetailModal); + document.getElementById('close-detail-btn').addEventListener('click', closeDetailModal); + + document.getElementById('contact-detail-modal').addEventListener('click', (e) => { + if (e.target.id === 'contact-detail-modal') { + closeDetailModal(); + } + }); +} + +/** + * Close detail modal + */ +function closeDetailModal() { + document.getElementById('contact-detail-modal').classList.add('hidden'); + currentContact = null; +} + +/** + * Utility: Get type badge + */ +function getTypeBadge(type) { + const colors = { + general: 'bg-gray-100 text-gray-800', + partnership: 'bg-purple-100 text-purple-800', + technical: 'bg-blue-100 text-blue-800', + feedback: 'bg-green-100 text-green-800' + }; + return \`\${type}\`; +} + +/** + * Utility: Get priority badge + */ +function getPriorityBadge(priority) { + const colors = { + low: 'bg-gray-100 text-gray-600', + normal: 'bg-blue-100 text-blue-800', + high: 'bg-red-100 text-red-800' + }; + return \`\${priority}\`; +} + +/** + * Utility: Get status badge + */ +function getStatusBadge(status) { + const colors = { + new: 'bg-orange-100 text-orange-800', + assigned: 'bg-blue-100 text-blue-800', + responded: 'bg-green-100 text-green-800', + closed: 'bg-gray-100 text-gray-600' + }; + return \`\${status}\`; +} + +/** + * Utility: Format date + */ +function formatDate(dateString) { + if (!dateString) return 'N/A'; + const date = new Date(dateString); + return date.toLocaleString('en-NZ', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +/** + * Utility: Escape HTML + */ +function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * Utility: Show error + */ +function showError(message) { + const container = document.getElementById('contacts-container'); + container.innerHTML = \` +
+

\${escapeHtml(message)}

+
+ \`; +} diff --git a/public/js/admin/newsletter-management.js b/public/js/admin/newsletter-management.js index 10524914..456376c5 100644 --- a/public/js/admin/newsletter-management.js +++ b/public/js/admin/newsletter-management.js @@ -155,11 +155,18 @@ function renderSubscribers(subscriptions) { // Add event listeners to buttons tbody.querySelectorAll('.view-details-btn').forEach(btn => { - btn.addEventListener('click', () => viewDetails(btn.dataset.id)); + btn.addEventListener('click', function() { + const id = this.getAttribute('data-id'); + viewDetails(id); + }); }); tbody.querySelectorAll('.delete-subscriber-btn').forEach(btn => { - btn.addEventListener('click', () => deleteSubscriber(btn.dataset.id, btn.dataset.email)); + btn.addEventListener('click', function() { + const id = this.getAttribute('data-id'); + const email = this.getAttribute('data-email'); + deleteSubscriber(id, email); + }); }); } diff --git a/public/js/admin/unified-inbox.js b/public/js/admin/unified-inbox.js new file mode 100644 index 00000000..0ee607a5 --- /dev/null +++ b/public/js/admin/unified-inbox.js @@ -0,0 +1,521 @@ +/** + * Unified Inbox Admin Page + * View and manage all communications: contacts, media inquiries, case submissions + */ + +let allItems = []; +let currentItem = null; + +// Initialize page +document.addEventListener('DOMContentLoaded', async () => { + await loadStats(); + await loadInbox(); + setupEventListeners(); +}); + +/** + * Load statistics + */ +async function loadStats() { + try { + const token = localStorage.getItem('admin_token'); + const response = await fetch('/api/inbox/stats', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error('Failed to load stats'); + + const data = await response.json(); + const stats = data.stats; + + document.getElementById('stat-total').textContent = stats.total.all; + document.getElementById('stat-new').textContent = stats.total.new; + document.getElementById('stat-assigned').textContent = stats.total.assigned; + document.getElementById('stat-responded').textContent = stats.total.responded; + + } catch (error) { + console.error('Error loading stats:', error); + } +} + +/** + * Load inbox items + */ +async function loadInbox() { + try { + const token = localStorage.getItem('admin_token'); + const type = document.getElementById('filter-type').value; + const status = document.getElementById('filter-status').value; + const priority = document.getElementById('filter-priority').value; + + let url = '/api/inbox?limit=50'; + if (type && type !== 'all') url += `&type=${type}`; + if (status) url += `&status=${status}`; + + const response = await fetch(url, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error('Failed to load inbox'); + + const data = await response.json(); + allItems = data.items; + + // Apply client-side priority filter if needed + if (priority) { + allItems = allItems.filter(item => item._priority === priority); + } + + renderInbox(); + + } catch (error) { + console.error('Error loading inbox:', error); + showError('Failed to load inbox'); + } +} + +/** + * Render inbox items list + */ +function renderInbox() { + const container = document.getElementById('inbox-container'); + + if (allItems.length === 0) { + container.innerHTML = ` +
+

No items found matching the selected filters.

+
+ `; + return; + } + + container.innerHTML = allItems.map(item => renderInboxItem(item)).join(''); + + // Add click listeners + container.querySelectorAll('[data-item-id]').forEach(el => { + el.addEventListener('click', () => { + const itemId = el.getAttribute('data-item-id'); + const itemType = el.getAttribute('data-item-type'); + showItemDetail(itemId, itemType); + }); + }); +} + +/** + * Render single inbox item + */ +function renderInboxItem(item) { + const typeBadge = getTypeBadge(item._type); + const statusBadge = getStatusBadge(item._displayStatus); + const priorityBadge = getPriorityBadge(item._priority); + + // Extract display data based on type + let title, subtitle, preview; + + if (item._type === 'contact') { + title = item.contact.name; + subtitle = item.contact.email; + preview = item.inquiry.subject || item.inquiry.message; + } else if (item._type === 'media') { + title = item.contact.name; + subtitle = `${item.contact.outlet} • ${item.contact.email}`; + preview = item.inquiry.subject; + } else if (item._type === 'case') { + title = item.submitter.name; + subtitle = item.submitter.email; + preview = item.case_study.title; + } + + return ` +
+
+
+
+

${escapeHtml(title)}

+ ${typeBadge} + ${priorityBadge} + ${statusBadge} +
+

${escapeHtml(subtitle)}

+

${escapeHtml(preview)}

+
+
+
${formatDate(item._created)}
+
+
+
+ `; +} + +/** + * Show item detail modal + */ +async function showItemDetail(itemId, itemType) { + try { + const token = localStorage.getItem('admin_token'); + let endpoint; + + if (itemType === 'contact') { + endpoint = `/api/contact/admin/${itemId}`; + } else if (itemType === 'media') { + endpoint = `/api/media/inquiries/${itemId}`; + } else if (itemType === 'case') { + endpoint = `/api/cases/submissions/${itemId}`; + } + + const response = await fetch(endpoint, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) throw new Error('Failed to load item'); + + const data = await response.json(); + currentItem = { ...data.contact || data.inquiry || data.submission, _type: itemType }; + + renderItemDetail(currentItem); + + document.getElementById('item-detail-modal').classList.remove('hidden'); + + } catch (error) { + console.error('Error loading item detail:', error); + alert('Failed to load item details'); + } +} + +/** + * Render item detail based on type + */ +function renderItemDetail(item) { + const content = document.getElementById('item-detail-content'); + + if (item._type === 'contact') { + content.innerHTML = renderContactDetail(item); + } else if (item._type === 'media') { + content.innerHTML = renderMediaDetail(item); + } else if (item._type === 'case') { + content.innerHTML = renderCaseDetail(item); + } +} + +/** + * Render contact detail + */ +function renderContactDetail(contact) { + return ` +
+
+

Contact Information

+
+
+ Name: +
${escapeHtml(contact.contact.name)}
+
+
+ Email: + +
+ ${contact.contact.organization ? ` +
+ Organization: +
${escapeHtml(contact.contact.organization)}
+
+ ` : ''} +
+
+ +
+

Inquiry

+
+ ${contact.inquiry.subject ? ` +
+ Subject: +
${escapeHtml(contact.inquiry.subject)}
+
+ ` : ''} +
+ Message: +
${escapeHtml(contact.inquiry.message)}
+
+
+
+ + +
+ `; +} + +/** + * Render media inquiry detail + */ +function renderMediaDetail(media) { + return ` +
+
+

Media Contact

+
+
+ Name: +
${escapeHtml(media.contact.name)}
+
+
+ Email: + +
+
+ Outlet: +
${escapeHtml(media.contact.outlet)}
+
+ ${media.contact.phone ? ` +
+ Phone: +
${escapeHtml(media.contact.phone)}
+
+ ` : ''} +
+
+ +
+

Inquiry

+
+
+ Subject: +
${escapeHtml(media.inquiry.subject)}
+
+
+ Message: +
${escapeHtml(media.inquiry.message)}
+
+ ${media.inquiry.deadline ? ` +
+ Deadline: +
${formatDate(media.inquiry.deadline)}
+
+ ` : ''} +
+
+ + ${media.ai_triage ? ` +
+

AI Triage

+
+
Urgency: ${getUrgencyBadge(media.ai_triage.urgency)}
+ ${media.ai_triage.claude_summary ? ` +
+ Summary: +
${escapeHtml(media.ai_triage.claude_summary)}
+
+ ` : ''} +
+
+ ` : ''} + + +
+ `; +} + +/** + * Render case submission detail + */ +function renderCaseDetail(caseItem) { + return ` +
+
+

Submitter

+
+
+ Name: +
${escapeHtml(caseItem.submitter.name)}
+
+
+ Email: + +
+ ${caseItem.submitter.organization ? ` +
+ Organization: +
${escapeHtml(caseItem.submitter.organization)}
+
+ ` : ''} +
+
+ +
+

Case Study

+
+
+ Title: +
${escapeHtml(caseItem.case_study.title)}
+
+
+ Description: +
${escapeHtml(caseItem.case_study.description)}
+
+
+ Failure Mode: +
${escapeHtml(caseItem.case_study.failure_mode)}
+
+
+
+ + ${caseItem.ai_review && caseItem.ai_review.relevance_score ? ` +
+

AI Review

+
+
Relevance Score: ${Math.round(caseItem.ai_review.relevance_score * 100)}%
+ ${caseItem.ai_review.recommended_category ? ` +
Recommended Category: ${escapeHtml(caseItem.ai_review.recommended_category)}
+ ` : ''} +
+
+ ` : ''} + + +
+ `; +} + +/** + * Setup event listeners + */ +function setupEventListeners() { + document.getElementById('filter-type').addEventListener('change', loadInbox); + document.getElementById('filter-status').addEventListener('change', loadInbox); + document.getElementById('filter-priority').addEventListener('change', loadInbox); + document.getElementById('refresh-btn').addEventListener('click', () => { + loadStats(); + loadInbox(); + }); + + document.getElementById('close-detail-modal').addEventListener('click', closeDetailModal); + document.getElementById('close-detail-btn').addEventListener('click', closeDetailModal); + + document.getElementById('item-detail-modal').addEventListener('click', (e) => { + if (e.target.id === 'item-detail-modal') { + closeDetailModal(); + } + }); +} + +/** + * Close detail modal + */ +function closeDetailModal() { + document.getElementById('item-detail-modal').classList.add('hidden'); + currentItem = null; +} + +/** + * Utility: Get type badge + */ +function getTypeBadge(type) { + const colors = { + contact: 'bg-blue-100 text-blue-800', + media: 'bg-purple-100 text-purple-800', + case: 'bg-green-100 text-green-800' + }; + const labels = { + contact: '📬 Contact', + media: '📰 Media', + case: '📋 Case' + }; + return `${labels[type] || type}`; +} + +/** + * Utility: Get priority badge + */ +function getPriorityBadge(priority) { + const colors = { + low: 'bg-gray-100 text-gray-600', + normal: 'bg-blue-100 text-blue-800', + medium: 'bg-blue-100 text-blue-800', + high: 'bg-red-100 text-red-800' + }; + return `${priority}`; +} + +/** + * Utility: Get status badge + */ +function getStatusBadge(status) { + const colors = { + new: 'bg-orange-100 text-orange-800', + pending: 'bg-orange-100 text-orange-800', + assigned: 'bg-blue-100 text-blue-800', + triaged: 'bg-blue-100 text-blue-800', + responded: 'bg-green-100 text-green-800', + closed: 'bg-gray-100 text-gray-600', + approved: 'bg-green-100 text-green-800', + rejected: 'bg-red-100 text-red-800', + needs_info: 'bg-yellow-100 text-yellow-800' + }; + return `${status}`; +} + +/** + * Utility: Get urgency badge + */ +function getUrgencyBadge(urgency) { + const colors = { + low: 'bg-gray-100 text-gray-800', + medium: 'bg-blue-100 text-blue-800', + high: 'bg-red-100 text-red-800' + }; + return `${urgency}`; +} + +/** + * Utility: Format date + */ +function formatDate(dateString) { + if (!dateString) return 'N/A'; + const date = new Date(dateString); + return date.toLocaleString('en-NZ', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +/** + * Utility: Escape HTML + */ +function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +/** + * Utility: Show error + */ +function showError(message) { + const container = document.getElementById('inbox-container'); + container.innerHTML = ` +
+

${escapeHtml(message)}

+
+ `; +} diff --git a/public/js/components/navbar-admin.js b/public/js/components/navbar-admin.js index ad98bf68..246fcf4a 100644 --- a/public/js/components/navbar-admin.js +++ b/public/js/components/navbar-admin.js @@ -125,6 +125,12 @@

CRM & Communications

+ + 🎯 CRM Dashboard + + + 📥 Unified Inbox + 📬 Contact Management diff --git a/public/koha/transparency.html b/public/koha/transparency.html index 643186fe..8f61b8f2 100644 --- a/public/koha/transparency.html +++ b/public/koha/transparency.html @@ -8,7 +8,7 @@ - +