Project Manager: ${escapeHTML(projectData.manager)}
Start Date: ${escapeHTML(projectData.startDate)}
Total Estimated Duration: ${totalDays} working days (approx. ${totalWeeks} weeks)
Phase Breakdown
| # |
Phase Name |
Start Day |
End Day |
Duration (Days) |
${timeline.map(p => `
| ${p.order} |
${escapeHTML(p.name)} |
Day ${p.startDay} |
Day ${p.endDay} |
${p.durationDays} |
`).join('')}
Detailed Task List
${timeline.map(p => `
-
Phase ${p.order}: ${escapeHTML(p.name)} (${p.durationDays} Days)
${p.tasks.length > 0 ? p.tasks.map(t => `
- ${escapeHTML(t.name)} (${t.duration} days)
`).join('') : '- No specific tasks defined. Using default phase duration.
'}
`).join('')}
`;
};
const getTableDataForPDF = (timeline) => {
const head = [['#', 'Phase Name', 'Start Day', 'End Day', 'Duration (Days)', 'Tasks']];
const body = timeline.map(p => [
p.order,
p.name,
`Day ${p.startDay}`,
`Day ${p.endDay}`,
p.durationDays,
p.tasks.map(t => `${t.name} (${t.duration} days)`).join('\n') || 'N/A'
]);
return { head, body };
};
const downloadPDF = () => {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
showMessage(messageBoxTask, 'Error: jsPDF library not loaded.', 'error');
return;
}
const { timeline, totalDays } = calculateTimeline();
const { head, body } = getTableDataForPDF(timeline);
const doc = new jsPDF('l', 'mm', 'a4'); // Landscape for better table fit
const pageWidth = doc.internal.pageSize.getWidth();
const totalWeeks = Math.ceil(totalDays / 5);
// 1. Title
doc.setFontSize(22);
doc.setFont(undefined, 'bold');
doc.setTextColor(44, 62, 80);
doc.text(`Project Timeline: ${projectData.name || 'Untitled Project'}`, pageWidth / 2, 15, { align: 'center' });
// 2. Overview
doc.setFontSize(10);
doc.setFont(undefined, 'normal');
doc.setTextColor(52, 73, 94);
let yPos = 25;
doc.text(`Manager: ${projectData.manager || 'N/A'}`, 15, yPos);
doc.text(`Start Date: ${projectData.startDate || 'N/A'}`, 15, yPos + 5);
doc.text(`Total Duration: ${totalDays} days (approx. ${totalWeeks} weeks)`, 15, yPos + 10);
yPos += 20;
// 3. Table
doc.autoTable({
head: head,
body: body,
startY: yPos,
theme: 'grid',
headStyles: {
fillColor: [0, 123, 255],
textColor: [255, 255, 255],
fontSize: 10,
fontStyle: 'bold'
},
styles: {
fontSize: 9,
cellPadding: 2,
valign: 'top',
lineColor: [200, 200, 200],
lineWidth: 0.1
},
columnStyles: {
0: { cellWidth: 10 },
1: { cellWidth: 40, fontStyle: 'bold' },
2: { cellWidth: 20 },
3: { cellWidth: 20 },
4: { cellWidth: 25 },
5: { cellWidth: 'auto' }
}
});
doc.save('project_timeline.pdf');
};
const downloadTxt = () => {
const { timeline, totalDays } = calculateTimeline();
const totalWeeks = Math.ceil(totalDays / 5);
let content = `PROJECT TIMELINE PLAN\n`;
content += `========================================\n\n`;
content += `Project: ${projectData.name || 'Untitled Project'}\n`;
content += `Manager: ${projectData.manager || 'N/A'}\n`;
content += `Start Date: ${projectData.startDate || 'N/A'}\n`;
content += `Total Duration: ${totalDays} working days (approx. ${totalWeeks} weeks)\n\n`;
content += `PHASE BREAKDOWN\n`;
content += `----------------------------------------\n`;
timeline.forEach(p => {
content += `\nPHASE ${p.order}: ${p.name.toUpperCase()} (Days ${p.startDay} - ${p.endDay} | ${p.durationDays} Days)\n`;
if (p.tasks.length > 0) {
p.tasks.forEach(t => {
content += ` - ${t.name} (${t.duration} days)\n`;
});
} else {
content += ` - No specific tasks defined.\n`;
}
});
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `timeline_${projectData.name.replace(/ /g, '_') || 'plan'}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
};
// --- Event Listeners ---
// Tab Buttons
tabButtons.forEach((btn, index) => {
btn.addEventListener('click', () => showTab(index + 1));
});
// Next/Prev Navigation
nextBtn.addEventListener('click', () => showTab(currentTab + 1));
prevBtn.addEventListener('click', () => showTab(currentTab - 1));
// Tab 2 Actions (Phases)
addPhaseBtn.addEventListener('click', () => addPhase()); // Call with no arguments for manual input
phaseTbody.addEventListener('click', (e) => {
if (e.target.dataset.removePhaseId) {
removePhase(parseInt(e.target.dataset.removePhaseId));
}
});
phaseTbody.addEventListener('blur', (e) => {
if (e.target.tagName === 'TD' && e.target.isContentEditable) {
const id = parseInt(e.target.dataset.id);
const field = e.target.dataset.field;
updatePhase(id, field, e.target.textContent);
}
}, true);
// Tab 3 Actions (Tasks)
addTaskBtn.addEventListener('click', addTask);
taskTbody.addEventListener('click', (e) => {
if (e.target.dataset.removeTaskId) {
const phaseId = parseInt(e.target.dataset.removePhaseId);
const taskId = parseInt(e.target.dataset.removeTaskId);
removeTask(phaseId, taskId);
}
});
taskTbody.addEventListener('blur', (e) => {
if (e.target.tagName === 'TD' && e.target.isContentEditable) {
const phaseId = parseInt(e.target.dataset.phaseId);
const taskId = parseInt(e.target.dataset.taskId);
const field = e.target.dataset.field;
updateTask(phaseId, taskId, field, e.target.textContent);
}
}, true);
// Tab 4 Actions
downloadPdfBtn.addEventListener('click', downloadPDF);
downloadTxtBtn.addEventListener('click', downloadTxt);
// --- Initialization ---
// Pre-populate with sample data
projectData.name = "Q4 E-commerce Redesign";
projectData.manager = "Jane Doe";
projectData.startDate = "2025-11-01";
// Use the addPhase function but pass the initialization data as an argument object
addPhase({ name: "Discovery & UX", duration: 15, tasks: [] });
addPhase({ name: "Development & Integration", duration: 30, tasks: [] });
addPhase({ name: "UAT & Launch Prep", duration: 10, tasks: [] });
// The phase IDs will be 0, 1, 2 due to phaseIdCounter incrementing inside addPhase
// Update sample tasks (Note: this is an imperfect way to pass nested data, but it preserves functionality)
// Correcting the initial assignment of task data based on the original logic
projectData.phases[0].tasks = [
{ id: taskIdCounter++, name: "Stakeholder Interviews", duration: 3 },
{ id: taskIdCounter++, name: "Finalize Wireframes", duration: 7 }
];
projectData.phases[1].tasks = [
{ id: taskIdCounter++, name: "Front-end coding", duration: 15 },
{ id: taskIdCounter++, name: "Backend API implementation", duration: 10 },
{ id: taskIdCounter++, name: "Payment gateway integration", duration: 5 }
];
projectNameInput.value = projectData.name;
projectManagerInput.value = projectData.manager;
projectStartInput.value = projectData.startDate;
// Initial render calls
renderPhases();
renderTasks();
showTab(1); // Set initial state
});