/**
* 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 = `
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
? '
${escapeHtml(project.name)}
${escapeHtml(project.id)}
${statusBadge}
${project.description ? `
${escapeHtml(project.description)}
` : ''}
${techStackBadges.length > 0 ? `
${techStackBadges.join('')}
` : ''}
${variableCount} var${variableCount !== 1 ? 's' : ''}
${project.repositoryUrl ? `
` : ''}
`;
}
// 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 = `