tractatus/public/js/components/navbar-admin.js
TheFlow edb1540631 feat(crm): complete Phase 3 multi-project CRM + critical bug fixes
Phase 3 Multi-Project CRM Implementation:
- Add UnifiedContact model for cross-project contact linking
- Add Organization model with domain-based auto-detection
- Add ActivityTimeline model for comprehensive interaction tracking
- Add SLATracking model for 24-hour response commitment
- Add ResponseTemplate model with variable substitution
- Add CRM controller with 8 API endpoints
- Add Inbox controller for unified communications
- Add CRM dashboard frontend with tabs (Contacts, Orgs, SLA, Templates)
- Add Contact Management interface (Phase 1)
- Add Unified Inbox interface (Phase 2)
- Integrate CRM routes into main API

Critical Bug Fixes:
- Fix newsletter DELETE button (event handler context issue)
- Fix case submission invisible button (invalid CSS class)
- Fix Chart.js CSP violation (add cdn.jsdelivr.net to policy)
- Fix Chart.js SRI integrity hash mismatch

Technical Details:
- Email-based contact deduplication across projects
- Automatic organization linking via email domain
- Cross-project activity timeline aggregation
- SLA breach detection and alerting system
- Template rendering with {placeholder} substitution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 18:10:14 +13:00

231 lines
12 KiB
JavaScript

/**
* Tractatus Admin Navbar Component
* Hamburger menu with organized admin page groups
*/
(function() {
'use strict';
class AdminNavbar {
constructor() {
this.mobileMenuOpen = false;
this.adminUser = JSON.parse(localStorage.getItem('admin_user') || '{}');
this.adminName = this.adminUser.name || this.adminUser.email || 'Admin';
this.init();
}
init() {
const container = document.getElementById('admin-navbar');
if (!container) return;
const pageTitle = container.dataset.pageTitle || 'Admin';
const pageIcon = container.dataset.pageIcon || 'default';
this.render(container, pageTitle, pageIcon);
this.attachEventListeners();
}
getIcon(iconType) {
const icons = {
default: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>',
calendar: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>',
dashboard: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>',
blog: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>',
analytics: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>'
};
return icons[iconType] || icons.default;
}
render(container, pageTitle, pageIcon) {
const iconSvg = this.getIcon(pageIcon);
container.innerHTML = `
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- Left: Logo + Page Title -->
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center">
<div class="h-8 w-8 bg-blue-600 rounded-lg flex items-center justify-center">
<svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
${iconSvg}
</svg>
</div>
<span class="ml-3 text-xl font-bold text-gray-900">${pageTitle}</span>
</div>
</div>
<!-- Right: User + Hamburger Menu -->
<div class="flex items-center gap-3">
<span class="text-sm text-gray-600 hidden sm:inline">${this.adminName}</span>
<button id="admin-menu-btn" class="text-gray-600 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded p-2" aria-label="Toggle admin menu">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
<!-- Admin Menu Drawer -->
<div id="admin-menu" class="hidden fixed inset-0 z-[9999]">
<!-- Backdrop -->
<div id="admin-menu-backdrop" class="absolute inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity"></div>
<!-- Menu Panel -->
<div id="admin-menu-panel" class="absolute right-0 top-0 bottom-0 w-80 max-w-[85vw] bg-white shadow-2xl transform transition-transform duration-300 ease-out overflow-y-auto">
<div class="flex justify-between items-center px-5 h-16 border-b border-gray-200 sticky top-0 bg-white z-10">
<div class="flex items-center space-x-2">
<div class="h-6 w-6 bg-blue-600 rounded flex items-center justify-center">
<svg class="h-4 w-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
</div>
<span class="font-bold text-gray-900">Admin</span>
</div>
<button id="admin-menu-close-btn" class="text-gray-600 hover:text-gray-900 p-2 rounded hover:bg-gray-100 transition" aria-label="Close menu">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav class="p-5 space-y-3">
<!-- Primary Tools -->
<div class="pb-3 mb-3 border-b border-gray-200">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">Primary Tools</p>
<a href="/admin/calendar.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition font-medium">
<span class="text-sm">📅 Task Calendar</span>
</a>
<a href="/admin/dashboard.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition font-medium">
<span class="text-sm">📊 Dashboard</span>
</a>
<a href="/admin/audit-analytics.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition font-medium">
<span class="text-sm">📈 Audit Analytics</span>
</a>
</div>
<!-- Content Management -->
<div class="pb-3 mb-3 border-b border-gray-200">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">Content Management</p>
<a href="/admin/blog-curation.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📝 Blog Curation</span>
</a>
<a href="/admin/editorial-guidelines.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📰 Editorial Guidelines</span>
</a>
<a href="/admin/newsletter-management.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📧 Newsletter</span>
</a>
</div>
<!-- CRM & Communications -->
<div class="pb-3 mb-3 border-b border-gray-200">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">CRM & Communications</p>
<a href="/admin/crm-dashboard.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">🎯 CRM Dashboard</span>
</a>
<a href="/admin/unified-inbox.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📥 Unified Inbox</span>
</a>
<a href="/admin/contact-management.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📬 Contact Management</span>
</a>
<a href="/admin/case-moderation.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📋 Case Submissions</span>
</a>
<a href="/admin/media-triage.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📰 Media Inquiries</span>
</a>
</div>
<!-- System & Framework -->
<div class="pb-3 mb-3 border-b border-gray-200">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">System & Framework</p>
<a href="/admin/credential-vault.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">🔐 Credential Vault</span>
</a>
<a href="/admin/rule-manager.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">🔧 Rule Manager</span>
</a>
<a href="/admin/hooks-dashboard.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">🔒 Hooks Dashboard</span>
</a>
<a href="/admin/project-manager.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📁 Project Manager</span>
</a>
<a href="/admin/claude-md-migrator.html" class="block px-3 py-2.5 text-gray-700 hover:bg-blue-50 hover:text-blue-700 rounded-lg transition">
<span class="text-sm">📄 CLAUDE.md Migrator</span>
</a>
</div>
<!-- Account -->
<div>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 px-3">Account</p>
<div class="px-3 py-2.5 text-gray-600 text-sm">
<div class="font-medium">${this.adminName}</div>
<div class="text-xs text-gray-500">${this.adminUser.email || ''}</div>
</div>
<button id="admin-logout-btn" class="w-full text-left px-3 py-2.5 text-red-600 hover:bg-red-50 rounded-lg transition font-medium">
<span class="text-sm">🚪 Logout</span>
</button>
</div>
</nav>
</div>
</div>
</nav>
`;
}
attachEventListeners() {
const menuBtn = document.getElementById('admin-menu-btn');
const menu = document.getElementById('admin-menu');
const menuPanel = document.getElementById('admin-menu-panel');
const closeBtn = document.getElementById('admin-menu-close-btn');
const backdrop = document.getElementById('admin-menu-backdrop');
const logoutBtn = document.getElementById('admin-logout-btn');
if (menuBtn && menu && menuPanel) {
menuBtn.addEventListener('click', () => this.openMenu(menu, menuPanel));
closeBtn.addEventListener('click', () => this.closeMenu(menu, menuPanel));
backdrop.addEventListener('click', () => this.closeMenu(menu, menuPanel));
}
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
localStorage.removeItem('admin_token');
localStorage.removeItem('admin_user');
window.location.href = '/admin/login.html';
});
}
}
openMenu(menu, panel) {
menu.classList.remove('hidden');
// Force reflow
panel.offsetHeight;
panel.classList.remove('translate-x-full');
this.mobileMenuOpen = true;
}
closeMenu(menu, panel) {
panel.classList.add('translate-x-full');
setTimeout(() => {
menu.classList.add('hidden');
}, 300);
this.mobileMenuOpen = false;
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new AdminNavbar());
} else {
new AdminNavbar();
}
})();