/** * Project Manager - Multi-Project Governance Dashboard * Handles CRUD operations, filtering, and variable management for projects */ // Auth check const token = localStorage.getItem('admin_token'); const user = JSON.parse(localStorage.getItem('admin_user') || '{}'); if (!token) { window.location.href = '/admin/login.html'; } // Display admin name document.getElementById('admin-name').textContent = user.email || 'Admin'; // Logout document.getElementById('logout-btn').addEventListener('click', () => { localStorage.removeItem('admin_token'); localStorage.removeItem('admin_user'); window.location.href = '/admin/login.html'; }); /** * API request helper with automatic auth header injection */ async function apiRequest(endpoint, options = {}) { const response = await fetch(endpoint, { ...options, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers } }); if (response.status === 401) { localStorage.removeItem('admin_token'); window.location.href = '/admin/login.html'; return; } return response.json(); } // State management let projects = []; let filters = { status: 'true', database: '', sortBy: 'name' }; /** * Load and display dashboard statistics */ async function loadStatistics() { try { const response = await apiRequest('/api/admin/projects'); if (!response.success) { console.error('Invalid stats response:', response); return; } const allProjects = response.projects || []; const activeProjects = allProjects.filter(p => p.active); // Count total variables const totalVariables = allProjects.reduce((sum, p) => sum + (p.variableCount || 0), 0); // Count unique databases const databases = new Set(); allProjects.forEach(p => { if (p.techStack?.database) { databases.add(p.techStack.database); } }); document.getElementById('stat-total').textContent = allProjects.length; document.getElementById('stat-active').textContent = activeProjects.length; document.getElementById('stat-variables').textContent = totalVariables; document.getElementById('stat-databases').textContent = databases.size; } catch (error) { console.error('Failed to load statistics:', error); showToast('Failed to load statistics', 'error'); } } /** * Load and render projects based on current filters */ async function loadProjects() { const container = document.getElementById('projects-grid'); try { // Show loading state container.innerHTML = `

Loading projects...

`; // Build query parameters const params = new URLSearchParams(); if (filters.status) params.append('active', filters.status); if (filters.database) params.append('database', filters.database); const response = await apiRequest(`/api/admin/projects?${params.toString()}`); if (!response.success) { throw new Error('Failed to load projects'); } projects = response.projects || []; // Apply client-side sorting projects.sort((a, b) => { switch (filters.sortBy) { case 'name': return a.name.localeCompare(b.name); case 'id': return a.id.localeCompare(b.id); case 'variableCount': return (b.variableCount || 0) - (a.variableCount || 0); case 'updatedAt': return new Date(b.updatedAt) - new Date(a.updatedAt); default: return 0; } }); // Update results count document.getElementById('filter-results').textContent = `Showing ${projects.length} project${projects.length !== 1 ? 's' : ''}`; // Render projects if (projects.length === 0) { container.innerHTML = `

No projects found

Try adjusting your filters or create a new project.

`; return; } // Render project cards container.innerHTML = projects.map(project => renderProjectCard(project)).join(''); } catch (error) { console.error('Failed to load projects:', error); container.innerHTML = `

Failed to load projects. Please try again.

`; showToast('Failed to load projects', 'error'); } } /** * Render a single project as an HTML card */ function renderProjectCard(project) { const statusBadge = project.active ? 'Active' : 'Inactive'; const techStackBadges = []; if (project.techStack?.framework) { techStackBadges.push(`${escapeHtml(project.techStack.framework)}`); } if (project.techStack?.database) { techStackBadges.push(`${escapeHtml(project.techStack.database)}`); } if (project.techStack?.frontend && techStackBadges.length < 3) { techStackBadges.push(`${escapeHtml(project.techStack.frontend)}`); } const variableCount = project.variableCount || 0; return `

${escapeHtml(project.name)}

${escapeHtml(project.id)}

${statusBadge}
${project.description ? `

${escapeHtml(project.description)}

` : ''} ${techStackBadges.length > 0 ? `
${techStackBadges.join('')}
` : ''}
${variableCount} var${variableCount !== 1 ? 's' : ''}
${project.repositoryUrl ? `
Repo
` : ''}
`; } // Filter handlers function applyFilters() { loadProjects(); } document.getElementById('filter-status')?.addEventListener('change', (e) => { filters.status = e.target.value; applyFilters(); }); document.getElementById('filter-database')?.addEventListener('change', (e) => { filters.database = e.target.value; applyFilters(); }); document.getElementById('sort-by')?.addEventListener('change', (e) => { filters.sortBy = e.target.value; applyFilters(); }); // Clear filters document.getElementById('clear-filters-btn')?.addEventListener('click', () => { filters = { status: 'true', database: '', sortBy: 'name' }; document.getElementById('filter-status').value = 'true'; document.getElementById('filter-database').value = ''; document.getElementById('sort-by').value = 'name'; applyFilters(); }); // CRUD operations async function viewProject(projectId) { if (window.projectEditor) { window.projectEditor.openView(projectId); } else { showToast('Project editor not loaded', 'error'); } } async function editProject(projectId) { if (window.projectEditor) { window.projectEditor.openEdit(projectId); } else { showToast('Project editor not loaded', 'error'); } } async function manageVariables(projectId) { if (window.projectEditor) { window.projectEditor.openVariables(projectId); } else { showToast('Project editor not loaded', 'error'); } } async function deleteProject(projectId, projectName) { if (!confirm(`Delete project "${projectName}"?\n\nThis will:\n- Deactivate the project (soft delete)\n- Deactivate all associated variables\n\nTo permanently delete, use the API with ?hard=true`)) { return; } try { const response = await apiRequest(`/api/admin/projects/${projectId}`, { method: 'DELETE' }); if (response.success) { showToast('Project deleted successfully', 'success'); loadProjects(); loadStatistics(); } else { showToast(response.message || 'Failed to delete project', 'error'); } } catch (error) { console.error('Delete error:', error); showToast('Failed to delete project', 'error'); } } // New project button document.getElementById('new-project-btn')?.addEventListener('click', () => { if (window.projectEditor) { window.projectEditor.openCreate(); } else { showToast('Project editor not loaded', 'error'); } }); /** * Show a toast notification message */ function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); const colors = { success: 'bg-green-500', error: 'bg-red-500', warning: 'bg-yellow-500', info: 'bg-blue-500' }; const toast = document.createElement('div'); toast.className = `${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-2 transition-all duration-300 ease-in-out`; toast.style.opacity = '0'; toast.style.transform = 'translateX(100px)'; toast.innerHTML = ` ${escapeHtml(message)} `; container.appendChild(toast); // Trigger animation setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(0)'; }, 10); // Auto-remove after 5 seconds setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100px)'; setTimeout(() => toast.remove(), 300); }, 5000); } // Utility functions function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Event delegation for data-action buttons (CSP compliance) document.addEventListener('click', (e) => { const button = e.target.closest('[data-action]'); if (!button) return; const action = button.dataset.action; const arg0 = button.dataset.arg0; const arg1 = button.dataset.arg1; switch (action) { case 'viewProject': viewProject(arg0); break; case 'manageVariables': manageVariables(arg0); break; case 'editProject': editProject(arg0); break; case 'deleteProject': deleteProject(arg0, arg1); break; case 'remove-parent': button.parentElement.remove(); break; } }); // Initialize on page load loadStatistics(); loadProjects();