/** * Case Study Moderation Admin Interface * Handles review and moderation of community-submitted case studies */ (function() { 'use strict'; // State let submissions = []; let currentFilters = { status: 'pending', failureMode: '', score: '', sortBy: 'submitted_at' }; let currentSubmission = null; let pendingAction = null; // DOM Elements const elements = { // Stats statTotal: document.getElementById('stat-total'), statPending: document.getElementById('stat-pending'), statApproved: document.getElementById('stat-approved'), statRejected: document.getElementById('stat-rejected'), // Filters filterStatus: document.getElementById('filter-status'), filterFailureMode: document.getElementById('filter-failure-mode'), filterScore: document.getElementById('filter-score'), sortBy: document.getElementById('sort-by'), clearFiltersBtn: document.getElementById('clear-filters-btn'), filterResults: document.getElementById('filter-results'), // Submissions submissionsContainer: document.getElementById('submissions-container'), // Details Modal detailsModal: document.getElementById('details-modal'), closeDetailsModal: document.getElementById('close-details-modal'), detailsModalCloseBtn: document.getElementById('details-modal-close-btn'), detailsModalContent: document.getElementById('details-modal-content'), requestInfoBtn: document.getElementById('request-info-btn'), rejectBtn: document.getElementById('reject-btn'), approveBtn: document.getElementById('approve-btn'), // Action Modal actionModal: document.getElementById('action-modal'), closeActionModal: document.getElementById('close-action-modal'), actionModalTitle: document.getElementById('action-modal-title'), actionModalContent: document.getElementById('action-modal-content'), actionNotes: document.getElementById('action-notes'), actionNotesLabel: document.getElementById('action-notes-label'), actionModalCancelBtn: document.getElementById('action-modal-cancel-btn'), actionModalSubmitBtn: document.getElementById('action-modal-submit-btn'), // Auth adminName: document.getElementById('admin-name'), logoutBtn: document.getElementById('logout-btn') }; /** * Initialize the application */ async function init() { // Check authentication const token = localStorage.getItem('admin_token'); if (!token) { window.location.href = '/admin/login.html'; return; } // Set admin name const adminEmail = localStorage.getItem('admin_email'); if (elements.adminName && adminEmail) { elements.adminName.textContent = adminEmail; } // Attach event listeners attachEventListeners(); // Load data await loadSubmissions(); await loadStats(); } /** * Attach event listeners */ function attachEventListeners() { // Filters if (elements.filterStatus) { elements.filterStatus.addEventListener('change', handleFilterChange); } if (elements.filterFailureMode) { elements.filterFailureMode.addEventListener('change', handleFilterChange); } if (elements.filterScore) { elements.filterScore.addEventListener('change', handleFilterChange); } if (elements.sortBy) { elements.sortBy.addEventListener('change', handleFilterChange); } if (elements.clearFiltersBtn) { elements.clearFiltersBtn.addEventListener('click', clearFilters); } // Details Modal if (elements.closeDetailsModal) { elements.closeDetailsModal.addEventListener('click', closeDetailsModal); } if (elements.detailsModalCloseBtn) { elements.detailsModalCloseBtn.addEventListener('click', closeDetailsModal); } if (elements.requestInfoBtn) { elements.requestInfoBtn.addEventListener('click', () => openActionModal('request_info')); } if (elements.rejectBtn) { elements.rejectBtn.addEventListener('click', () => openActionModal('reject')); } if (elements.approveBtn) { elements.approveBtn.addEventListener('click', () => openActionModal('approve')); } // Action Modal if (elements.closeActionModal) { elements.closeActionModal.addEventListener('click', closeActionModal); } if (elements.actionModalCancelBtn) { elements.actionModalCancelBtn.addEventListener('click', closeActionModal); } if (elements.actionModalSubmitBtn) { elements.actionModalSubmitBtn.addEventListener('click', handleActionSubmit); } // Logout if (elements.logoutBtn) { elements.logoutBtn.addEventListener('click', () => { localStorage.removeItem('admin_token'); localStorage.removeItem('admin_email'); window.location.href = '/admin/login.html'; }); } } /** * Load submissions from API */ async function loadSubmissions() { try { const token = localStorage.getItem('admin_token'); const params = new URLSearchParams(); if (currentFilters.status) params.append('status', currentFilters.status); if (currentFilters.failureMode) params.append('failure_mode', currentFilters.failureMode); if (currentFilters.score) params.append('score', currentFilters.score); if (currentFilters.sortBy) params.append('sort', currentFilters.sortBy); const response = await fetch(`/api/cases/submissions?${params.toString()}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.status === 401) { localStorage.removeItem('admin_token'); window.location.href = '/admin/login.html'; return; } const data = await response.json(); if (data.success) { submissions = data.submissions || []; renderSubmissions(); } else { showToast('Failed to load submissions', 'error'); } } catch (error) { console.error('Error loading submissions:', error); showToast('Error loading submissions', 'error'); } } /** * Load statistics */ async function loadStats() { try { const token = localStorage.getItem('admin_token'); const response = await fetch('/api/cases/submissions/stats', { headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); if (data.success) { const stats = data.stats || {}; if (elements.statTotal) elements.statTotal.textContent = stats.total || 0; if (elements.statPending) elements.statPending.textContent = stats.pending || 0; if (elements.statApproved) elements.statApproved.textContent = stats.approved || 0; if (elements.statRejected) elements.statRejected.textContent = stats.rejected || 0; } } catch (error) { console.error('Error loading stats:', error); } } /** * Render submissions list */ function renderSubmissions() { if (!elements.submissionsContainer) return; // Update filter results count if (elements.filterResults) { const count = submissions.length; elements.filterResults.textContent = `Showing ${count} submission${count !== 1 ? 's' : ''}`; } // No submissions if (submissions.length === 0) { elements.submissionsContainer.innerHTML = `

No submissions found

Try adjusting your filters

`; return; } // Render submission cards const submissionsHTML = submissions.map(submission => { const statusColors = { 'pending': 'bg-yellow-100 text-yellow-800', 'approved': 'bg-green-100 text-green-800', 'rejected': 'bg-red-100 text-red-800', 'needs_info': 'bg-blue-100 text-blue-800' }; const statusColor = statusColors[submission.moderation?.status] || 'bg-gray-100 text-gray-800'; const relevanceScore = submission.ai_review?.relevance_score; const scoreColor = relevanceScore >= 0.7 ? 'text-green-600' : relevanceScore >= 0.4 ? 'text-yellow-600' : 'text-red-600'; const submittedDate = new Date(submission.submitted_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); return `

${escapeHtml(submission.case_study?.title || 'Untitled')}

${submission.moderation?.status?.toUpperCase() || 'PENDING'} ${submission.case_study?.failure_mode?.replace(/_/g, ' ').toUpperCase() || 'OTHER'} ${relevanceScore !== undefined ? ` AI Score: ${(relevanceScore * 100).toFixed(0)}% ` : ''}
Submitted by: ${escapeHtml(submission.submitter?.name || 'Anonymous')} ${submission.submitter?.organization ? ` (${escapeHtml(submission.submitter.organization)})` : ''}

${escapeHtml(submission.case_study?.description || '').substring(0, 200)}...

Submitted: ${submittedDate} ${submission.moderation?.reviewed_at ? ` Reviewed: ${new Date(submission.moderation.reviewed_at).toLocaleDateString()} ` : ''}
`; }).join(''); elements.submissionsContainer.innerHTML = submissionsHTML; // Attach click handlers to submission cards (CSP-compliant) document.querySelectorAll('.submission-card').forEach(card => { card.addEventListener('click', () => { const submissionId = card.getAttribute('data-submission-id'); if (submissionId) { viewSubmission(submissionId); } }); }); } /** * View submission details */ async function viewSubmission(submissionId) { try { const token = localStorage.getItem('admin_token'); const response = await fetch(`/api/cases/submissions/${submissionId}`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); if (data.success) { currentSubmission = data.submission; renderSubmissionDetails(data.submission); openDetailsModal(); } else { showToast('Failed to load submission details', 'error'); } } catch (error) { console.error('Error loading submission:', error); showToast('Error loading submission details', 'error'); } } /** * Render submission details in modal */ function renderSubmissionDetails(submission) { if (!elements.detailsModalContent) return; const html = `

Status: ${submission.moderation?.status?.toUpperCase() || 'PENDING'}

${submission.moderation?.reviewed_at ? `

Reviewed: ${new Date(submission.moderation.reviewed_at).toLocaleString()}

` : ''}
${submission.ai_review?.relevance_score !== undefined ? `

AI Relevance Score

${(submission.ai_review.relevance_score * 100).toFixed(0)}%

` : ''}

Submitter Information

Name: ${escapeHtml(submission.submitter?.name || 'N/A')}

Email: ${escapeHtml(submission.submitter?.email || 'N/A')}

${submission.submitter?.organization ? `

Organization: ${escapeHtml(submission.submitter.organization)}

` : ''}

Public Attribution: ${submission.submitter?.public ? 'Yes' : 'No'}

Case Study Details

Title

${escapeHtml(submission.case_study?.title || 'N/A')}

Failure Mode

${submission.case_study?.failure_mode?.replace(/_/g, ' ').toUpperCase() || 'N/A'}

Description

${escapeHtml(submission.case_study?.description || 'N/A')}

Tractatus Applicability

${escapeHtml(submission.case_study?.tractatus_applicability || 'N/A')}
${submission.case_study?.evidence?.length > 0 ? `

Evidence Links

` : ''}
${submission.ai_review?.claude_analysis ? `

AI Analysis

${escapeHtml(submission.ai_review.claude_analysis)}

` : ''} ${submission.moderation?.review_notes ? `

Review Notes

${escapeHtml(submission.moderation.review_notes)}

` : ''} `; elements.detailsModalContent.innerHTML = html; // Show/hide action buttons based on status const isPending = submission.moderation?.status === 'pending' || submission.moderation?.status === 'needs_info'; if (elements.requestInfoBtn) elements.requestInfoBtn.style.display = isPending ? 'inline-block' : 'none'; if (elements.rejectBtn) elements.rejectBtn.style.display = isPending ? 'inline-block' : 'none'; if (elements.approveBtn) elements.approveBtn.style.display = isPending ? 'inline-block' : 'none'; } /** * Get status banner color */ function getStatusBannerColor(status) { const colors = { 'pending': 'bg-yellow-50 border border-yellow-200', 'approved': 'bg-green-50 border border-green-200', 'rejected': 'bg-red-50 border border-red-200', 'needs_info': 'bg-blue-50 border border-blue-200' }; return colors[status] || 'bg-gray-50 border border-gray-200'; } /** * Open action modal */ function openActionModal(action) { if (!currentSubmission) return; pendingAction = action; const titles = { 'approve': 'Approve Case Study', 'reject': 'Reject Case Study', 'request_info': 'Request More Information' }; const labels = { 'approve': 'Approval Notes', 'reject': 'Rejection Reason', 'request_info': 'Information Needed' }; const content = { 'approve': 'This case study will be approved and may be published. Add any notes for the submitter:', 'reject': 'Please provide a reason for rejecting this submission:', 'request_info': 'What additional information do you need from the submitter?' }; if (elements.actionModalTitle) { elements.actionModalTitle.textContent = titles[action] || 'Action'; } if (elements.actionModalContent) { elements.actionModalContent.innerHTML = `

${escapeHtml(currentSubmission.case_study?.title || 'Untitled')}

${content[action]}

`; } if (elements.actionNotesLabel) { elements.actionNotesLabel.textContent = labels[action] || 'Notes'; } if (elements.actionNotes) { elements.actionNotes.value = ''; } if (elements.actionModal) { elements.actionModal.classList.remove('hidden'); } } /** * Close action modal */ function closeActionModal() { if (elements.actionModal) { elements.actionModal.classList.add('hidden'); } pendingAction = null; } /** * Handle action submission */ async function handleActionSubmit() { if (!currentSubmission || !pendingAction) return; const notes = elements.actionNotes?.value.trim(); if (!notes) { showToast('Please enter notes', 'error'); return; } const endpoints = { 'approve': '/api/cases/submissions/' + currentSubmission._id + '/approve', 'reject': '/api/cases/submissions/' + currentSubmission._id + '/reject', 'request_info': '/api/cases/submissions/' + currentSubmission._id + '/request-info' }; // Build request body based on action (different endpoints expect different parameter names) const requestBody = {}; if (pendingAction === 'approve') { requestBody.notes = notes; } else if (pendingAction === 'reject') { requestBody.reason = notes; // Controller expects 'reason' for reject } else if (pendingAction === 'request_info') { requestBody.requested_info = notes; // Controller expects 'requested_info' } try { const token = localStorage.getItem('admin_token'); const response = await fetch(endpoints[pendingAction], { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); const data = await response.json(); if (data.success) { showToast(`Successfully ${pendingAction.replace('_', ' ')}d submission`, 'success'); closeActionModal(); closeDetailsModal(); await loadSubmissions(); await loadStats(); } else { showToast(data.message || 'Action failed', 'error'); } } catch (error) { console.error('Error:', error); showToast('Error processing action', 'error'); } } /** * Open details modal */ function openDetailsModal() { if (elements.detailsModal) { elements.detailsModal.classList.remove('hidden'); document.body.style.overflow = 'hidden'; } } /** * Close details modal */ function closeDetailsModal() { if (elements.detailsModal) { elements.detailsModal.classList.add('hidden'); document.body.style.overflow = ''; } currentSubmission = null; } /** * Handle filter changes */ function handleFilterChange() { currentFilters.status = elements.filterStatus?.value || ''; currentFilters.failureMode = elements.filterFailureMode?.value || ''; currentFilters.score = elements.filterScore?.value || ''; currentFilters.sortBy = elements.sortBy?.value || 'submitted_at'; loadSubmissions(); } /** * Clear all filters */ function clearFilters() { if (elements.filterStatus) elements.filterStatus.value = ''; if (elements.filterFailureMode) elements.filterFailureMode.value = ''; if (elements.filterScore) elements.filterScore.value = ''; if (elements.sortBy) elements.sortBy.value = 'submitted_at'; currentFilters = { status: '', failureMode: '', score: '', sortBy: 'submitted_at' }; loadSubmissions(); } /** * Show toast notification */ function showToast(message, type = 'info') { const toast = document.createElement('div'); const colors = { 'success': 'bg-green-500', 'error': 'bg-red-500', 'info': 'bg-blue-500' }; toast.className = `${colors[type] || colors.info} text-white px-6 py-3 rounded-lg shadow-lg`; toast.textContent = message; const container = document.getElementById('toast-container'); if (container) { container.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();