Overall Progress
${overallProgress.toFixed(1)}%
`;
};
const renderProjectSelector = () => {
projectSelector.innerHTML = '';
state.projects.forEach(p => {
const option = document.createElement('option');
option.value = p.id;
option.textContent = p.name;
if (p.id === state.selectedProjectId) {
option.selected = true;
}
projectSelector.appendChild(option);
});
};
const renderProjectDetails = (projectId) => {
const project = state.projects.find(p => p.id === projectId);
if (!project) {
projectDetailsContainer.innerHTML = '
Please select a project.
';
return;
}
const completedTasks = project.tasks.filter(t => t.status === 'Completed').length;
const progress = project.tasks.length > 0 ? (completedTasks / project.tasks.length) * 100 : 0;
let taskRows = '';
project.tasks.forEach(task => {
const statusColors = {
"Not Started": "bg-red-100 text-red-800",
"In Progress": "bg-yellow-100 text-yellow-800",
"Completed": "bg-green-100 text-green-800"
};
taskRows += `
| ${task.name} |
${task.start} |
${task.end} |
${task.status} |
`;
});
projectDetailsContainer.innerHTML = `
${project.name}
Budget
${formatCurrency(project.budget)}
Tasks
${project.tasks.length}
Progress
${progress.toFixed(1)}%
Task List
| Task |
Start Date |
End Date |
Status |
${taskRows}
Project Timeline
`;
renderGanttChart(project);
};
const renderGanttChart = (project) => {
const container = document.getElementById(`gantt-chart-${project.id}`);
if (!container || project.tasks.length === 0) return;
const allDates = project.tasks.flatMap(t => [parseDate(t.start), parseDate(t.end)]);
const minDate = new Date(Math.min.apply(null, allDates));
const maxDate = new Date(Math.max.apply(null, allDates));
minDate.setDate(minDate.getDate() - 2); // Add padding
maxDate.setDate(maxDate.getDate() + 2); // Add padding
const totalDays = dayDiff(minDate, maxDate);
container.innerHTML = ''; // Clear previous chart
// Render Header
const header = document.createElement('div');
header.className = 'gantt-header-days';
header.style.gridTemplateColumns = `repeat(${totalDays}, minmax(0, 1fr))`;
for (let i = 0; i < totalDays; i++) {
const day = new Date(minDate);
day.setDate(minDate.getDate() + i);
const dayEl = document.createElement('div');
dayEl.className = 'text-center';
dayEl.textContent = `${day.getMonth()+1}/${day.getDate()}`;
header.appendChild(dayEl);
}
container.appendChild(header);
// Render Task Rows and Bars
const chartBody = document.createElement('div');
chartBody.style.height = `${project.tasks.length * 40}px`; // 40px per task
chartBody.className = 'relative';
project.tasks.forEach((task, index) => {
const taskRow = document.createElement('div');
taskRow.className = 'gantt-row h-10 flex items-center pl-2 text-sm';
taskRow.textContent = task.name;
container.appendChild(taskRow);
const startDate = parseDate(task.start);
const endDate = parseDate(task.end);
const startOffset = dayDiff(minDate, startDate);
const duration = dayDiff(startDate, endDate) + 1;
const bar = document.createElement('div');
const statusColors = {
"Completed": "bg-green-600",
"In Progress": "bg-blue-600",
"Not Started": "bg-gray-400"
};
bar.className = `gantt-bar ${statusColors[task.status]}`;
bar.style.top = `${index * 40 + 5}px`;
bar.style.left = `calc(${(startOffset / totalDays) * 100}% + 2px)`;
bar.style.width = `calc(${(duration / totalDays) * 100}% - 4px)`;
bar.textContent = task.name;
bar.title = `${task.name} (${task.status})`;
chartBody.appendChild(bar);
});
container.appendChild(chartBody);
};
const renderConfigurator = () => {
configProjectsContainer.innerHTML = '';
state.projects.forEach(p => {
let taskInputs = '';
p.tasks.forEach((t, taskIndex) => {
taskInputs += `
`;
});
const projectBlock = `
`;
configProjectsContainer.innerHTML += projectBlock;
});
addConfigEventListeners();
};
// --- EVENT HANDLERS ---
const handleProjectSelect = (e) => {
state.selectedProjectId = parseInt(e.target.value);
renderProjectDetails(state.selectedProjectId);
};
const handleConfigChange = (e) => {
const projectId = parseInt(e.target.dataset.projectId);
const field = e.target.dataset.field;
const project = state.projects.find(p => p.id === projectId);
if (!project) return;
if (e.target.classList.contains('config-project-input')) {
project[field] = field === 'name' ? e.target.value : parseFloat(e.target.value) || 0;
} else if (e.target.classList.contains('config-task-input')) {
const taskId = parseInt(e.target.dataset.taskId);
const task = project.tasks.find(t => t.id === taskId);
if (task) {
task[field] = e.target.value;
}
}
renderAll();
};
const handleAddProject = () => {
const newId = state.projects.length > 0 ? Math.max(...state.projects.map(p => p.id)) + 1 : 1;
state.projects.push({ id: newId, name: "New Project", budget: 0, tasks: [] });
renderAll();
};
const handleRemoveProject = (e) => {
const projectId = parseInt(e.target.dataset.projectId);
state.projects = state.projects.filter(p => p.id !== projectId);
if (state.selectedProjectId === projectId && state.projects.length > 0) {
state.selectedProjectId = state.projects[0].id;
} else if (state.projects.length === 0) {
state.selectedProjectId = null;
}
renderAll();
};
const handleAddTask = (e) => {
const projectId = parseInt(e.target.dataset.projectId);
const project = state.projects.find(p => p.id === projectId);
if (project) {
const newTaskId = project.tasks.length > 0 ? Math.max(...project.tasks.map(t => t.id)) + 1 : projectId * 100 + 1;
const today = new Date().toISOString().split('T')[0];
project.tasks.push({ id: newTaskId, name: "New Task", start: today, end: today, status: "Not Started" });
}
renderAll();
};
const handleRemoveTask = (e) => {
const projectId = parseInt(e.target.dataset.projectId);
const taskId = parseInt(e.target.dataset.taskId);
const project = state.projects.find(p => p.id === projectId);
if (project) {
project.tasks = project.tasks.filter(t => t.id !== taskId);
}
renderAll();
};
const addConfigEventListeners = () => {
document.querySelectorAll('.config-project-input, .config-task-input').forEach(input => input.addEventListener('change', handleConfigChange));
document.querySelectorAll('.remove-project-btn').forEach(btn => btn.addEventListener('click', handleRemoveProject));
document.querySelectorAll('.add-task-btn').forEach(btn => btn.addEventListener('click', handleAddTask));
document.querySelectorAll('.remove-task-btn').forEach(btn => btn.addEventListener('click', handleRemoveTask));
};
const handleDownloadPdf = () => {
const { jsPDF } = window.jspdf;
const pdfContent = document.getElementById('pdf-content');
// Temporarily show all project details for PDF
const originalContent = projectDetailsContainer.innerHTML;
projectDetailsContainer.innerHTML = '';
state.projects.forEach(p => {
const projectDiv = document.createElement('div');
projectDetailsContainer.appendChild(projectDiv);
renderProjectDetails(p.id);
projectDiv.innerHTML = projectDetailsContainer.innerHTML;
// Add a page break element for styling in PDF
const pageBreak = document.createElement('div');
pageBreak.style.pageBreakAfter = 'always';
projectDetailsContainer.appendChild(pageBreak);
});
// Hide non-PDF elements
document.querySelectorAll('.no-print').forEach(el => el.style.display = 'none');
html2canvas(pdfContent, { scale: 2, useCORS: true, backgroundColor: '#ffffff' }).then(canvas => {
// Restore original view
projectDetailsContainer.innerHTML = originalContent;
renderProjectDetails(state.selectedProjectId);
document.querySelectorAll('.no-print').forEach(el => el.style.display = '');
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const ratio = canvasWidth / canvasHeight;
let imgWidth = pdfWidth - 20;
let imgHeight = imgWidth / ratio;
let heightLeft = imgHeight;
let position = 10;
pdf.addImage(imgData, 'PNG', 10, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - 20);
while (heightLeft > 0) {
position = -heightLeft - 10;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 10, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - 20);
}
pdf.save('Multiple-Project-Dashboard.pdf');
});
};
// --- TABBING LOGIC ---
let currentTabIndex = 0;
const updateTabButtons = () => {
prevTabBtn.disabled = currentTabIndex === 0;
nextTabBtn.disabled = currentTabIndex === tabContents.length - 1;
};
const switchTab = (index) => {
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
tabButtons[index].classList.add('active');
tabContents[index].classList.add('active');
currentTabIndex = index;
updateTabButtons();
};
tabButtons.forEach((button, index) => button.addEventListener('click', () => switchTab(index)));
prevTabBtn.addEventListener('click', () => { if (currentTabIndex > 0) switchTab(currentTabIndex - 1); });
nextTabBtn.addEventListener('click', () => { if (currentTabIndex < tabContents.length - 1) switchTab(currentTabIndex + 1); });
// --- INITIALIZATION ---
if (projectSelector && addProjectBtn && downloadPdfBtn) {
projectSelector.addEventListener('change', handleProjectSelect);
addProjectBtn.addEventListener('click', handleAddProject);
downloadPdfBtn.addEventListener('click', handleDownloadPdf);
renderAll();
updateTabButtons();
} else {
console.error("Essential dashboard elements could not be found.");
}
});