${totalBugs}
`;
document.getElementById('summary-card-vulnerabilities').innerHTML = `
Vulnerabilities
${totalVulnerabilities}
`;
}
function renderProjectSummaryTable() {
const tableBody = document.getElementById('project-summary-table');
if (!tableBody) return;
tableBody.innerHTML = projects.map(p => `
| ${p.name} |
${p.coverage}% |
${p.bugs} |
${p.vulnerabilities} |
${p.smells} |
${p.duplication}% |
`).join('');
}
function renderCharts() {
const projectNames = projects.map(p => p.name);
const coverageData = projects.map(p => p.coverage);
const totalBugs = projects.reduce((sum, p) => sum + p.bugs, 0);
const totalVulnerabilities = projects.reduce((sum, p) => sum + p.vulnerabilities, 0);
const totalSmells = projects.reduce((sum, p) => sum + p.smells, 0);
// Destroy existing charts before re-rendering
if (charts.coverage) charts.coverage.destroy();
if (charts.issues) charts.issues.destroy();
// Coverage Chart (Bar)
const coverageCtx = document.getElementById('coverageChart')?.getContext('2d');
if (coverageCtx) {
charts.coverage = new Chart(coverageCtx, {
type: 'bar',
data: {
labels: projectNames,
datasets: [{
label: 'Code Coverage (%)',
data: coverageData,
backgroundColor: 'rgba(59, 130, 246, 0.6)',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true, max: 100 } },
plugins: { legend: { display: false } }
}
});
}
// Issues Chart (Doughnut)
const issuesCtx = document.getElementById('issuesChart')?.getContext('2d');
if (issuesCtx) {
charts.issues = new Chart(issuesCtx, {
type: 'doughnut',
data: {
labels: ['Bugs', 'Vulnerabilities', 'Code Smells'],
datasets: [{
label: 'Issues Breakdown',
data: [totalBugs, totalVulnerabilities, totalSmells],
backgroundColor: [
'rgba(239, 68, 68, 0.7)',
'rgba(245, 158, 11, 0.7)',
'rgba(107, 114, 128, 0.7)'
],
borderColor: [
'rgba(239, 68, 68, 1)',
'rgba(245, 158, 11, 1)',
'rgba(107, 114, 128, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } }
}
});
}
}
function renderConfigTable() {
const tableBody = document.getElementById('config-table');
if (!tableBody) return;
tableBody.innerHTML = projects.map((p, index) => `
|
|
|
|
|
|
|
`).join('');
}
// --- EVENT HANDLING & ACTIONS ---
window.switchTab = (tabName) => {
activeTab = tabName;
updateTabStyles();
updateNavButtons();
};
function updateTabStyles() {
const dashboardTab = document.getElementById('tab-content-dashboard');
const configTab = document.getElementById('tab-content-config');
const dashboardBtn = document.getElementById('tab-btn-dashboard');
const configBtn = document.getElementById('tab-btn-config');
if (activeTab === 'dashboard') {
dashboardTab.classList.remove('hidden');
configTab.classList.add('hidden');
dashboardBtn.classList.add('tab-active');
dashboardBtn.classList.remove('tab-inactive');
configBtn.classList.add('tab-inactive');
configBtn.classList.remove('tab-active');
} else {
dashboardTab.classList.add('hidden');
configTab.classList.remove('hidden');
dashboardBtn.classList.add('tab-inactive');
dashboardBtn.classList.remove('tab-active');
configBtn.classList.add('tab-active');
configBtn.classList.remove('tab-inactive');
}
}
window.navigateTabs = (direction) => {
if (direction === 'next' && activeTab === 'dashboard') {
switchTab('config');
} else if (direction === 'prev' && activeTab === 'config') {
switchTab('dashboard');
}
};
function updateNavButtons() {
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
if (activeTab === 'dashboard') {
prevBtn.disabled = true;
prevBtn.classList.add('opacity-50', 'cursor-not-allowed');
nextBtn.disabled = false;
nextBtn.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
prevBtn.disabled = false;
prevBtn.classList.remove('opacity-50', 'cursor-not-allowed');
nextBtn.disabled = true;
nextBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
}
window.addProject = () => {
const newId = projects.length > 0 ? Math.max(...projects.map(p => p.id)) + 1 : 1;
projects.push({
id: newId,
name: 'New Project',
coverage: 0,
bugs: 0,
vulnerabilities: 0,
smells: 0,
duplication: 0
});
saveData();
updateUI();
};
window.updateProject = (index, field, value) => {
if (projects[index]) {
const parsedValue = (field === 'name') ? value : parseFloat(value);
if (!isNaN(parsedValue) || field === 'name') {
projects[index][field] = parsedValue;
saveData();
renderDashboard(); // Only need to re-render dashboard
}
}
};
window.deleteProject = (index) => {
projects.splice(index, 1);
saveData();
updateUI();
};
window.downloadPDF = async () => {
const { jsPDF } = window.jspdf;
const dashboardContent = document.getElementById('dashboard-pdf-content');
const pdfButtonContainer = document.getElementById('pdf-button-container');
if (!dashboardContent || !pdfButtonContainer) return;
// Temporarily hide the download button so it's not in the PDF
pdfButtonContainer.style.display = 'none';
try {
const canvas = await html2canvas(dashboardContent, {
scale: 2, // Higher scale for better quality
useCORS: true,
logging: false,
backgroundColor: '#f9fafb' // Match body background
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [canvas.width, canvas.height]
});
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
pdf.save('Code_Quality_Dashboard.pdf');
} catch (error) {
console.error("Failed to generate PDF:", error);
// In a real app, show a user-friendly error message here
} finally {
// Show the button again
pdfButtonContainer.style.display = 'block';
}
};
function attachEventListeners() {
// Event listeners for tabs and nav buttons are handled by inline onclick
// This is sufficient for this self-contained tool as per spec.
}
// --- START THE APP ---
init();
});