/**
* 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 ? `
` : ''}
${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();
}
})();