Online Project Task Dependencies Manager

Online Project Task Dependencies Manager

Plan, schedule, and visualize your project timeline and task dependencies.

Add or Edit Task

Task List

Task Name Duration Cost Actions

You need at least two tasks to define dependencies.

`; return; } tasks.forEach(task => { const otherTasks = tasks.filter(t => t.id !== task.id); let options = otherTasks.map(ot => `` ).join(''); const depRow = document.createElement('div'); depRow.className = 'grid grid-cols-1 md:grid-cols-3 gap-4 items-center border p-4 rounded-lg'; depRow.innerHTML = `

depends on:

`; dependencyListContainer.appendChild(depRow); }); // Attach event listeners to the new select elements document.querySelectorAll('.dependency-select').forEach(select => { select.addEventListener('change', (e) => { const taskId = parseInt(e.target.dataset.taskId); const selectedOptions = Array.from(e.target.selectedOptions).map(opt => parseInt(opt.value)); const taskToUpdate = tasks.find(t => t.id === taskId); if (taskToUpdate) { taskToUpdate.dependencies = selectedOptions; } }); }); }; const renderGanttChart = () => { if (!ganttChartSVG || !totalDurationEl) return; const { scheduledTasks, criticalPath, maxEndDate } = calculateGanttData(); if (scheduledTasks.length === 0) { ganttChartSVG.innerHTML = `No tasks to display. Please add tasks and define dependencies.`; totalDurationEl.textContent = '0 days'; return; } totalDurationEl.textContent = `${maxEndDate} days`; const margin = { top: 40, right: 20, bottom: 20, left: 150 }; const chartHeight = scheduledTasks.length * 40 + margin.top + margin.bottom; const chartWidth = ganttChartSVG.parentElement.clientWidth; ganttChartSVG.setAttribute('height', chartHeight); ganttChartSVG.innerHTML = ''; // Clear previous chart const xScale = (val) => margin.left + (val / maxEndDate) * (chartWidth - margin.left - margin.right); // Draw grid lines and day markers for (let i = 0; i <= maxEndDate; i++) { const x = xScale(i); const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); line.setAttribute('x1', x); line.setAttribute('x2', x); line.setAttribute('y1', margin.top); line.setAttribute('y2', chartHeight - margin.bottom); line.setAttribute('class', 'gantt-grid-line'); ganttChartSVG.appendChild(line); if (i % (Math.ceil(maxEndDate / 20)) === 0) { // Avoid cluttering the axis const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); text.setAttribute('x', x); text.setAttribute('y', margin.top - 10); text.setAttribute('text-anchor', 'middle'); text.setAttribute('class', 'gantt-axis-text'); text.textContent = `Day ${i}`; ganttChartSVG.appendChild(text); } } // Draw tasks scheduledTasks.forEach((task, index) => { const y = margin.top + index * 40; const isCritical = criticalPath.includes(task.id); // Task bar const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('x', xScale(task.start)); rect.setAttribute('y', y); rect.setAttribute('width', xScale(task.end) - xScale(task.start)); rect.setAttribute('height', 30); rect.setAttribute('rx', 3); rect.setAttribute('ry', 3); rect.setAttribute('class', `gantt-task-bar ${isCritical ? 'critical' : ''}`); ganttChartSVG.appendChild(rect); // Task label inside bar const textInBar = document.createElementNS('http://www.w3.org/2000/svg', 'text'); textInBar.setAttribute('x', xScale(task.start) + 5); textInBar.setAttribute('y', y + 20); textInBar.setAttribute('class', 'gantt-task-label'); textInBar.textContent = task.name; ganttChartSVG.appendChild(textInBar); // Task name on Y-axis const yAxisLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); yAxisLabel.setAttribute('x', margin.left - 10); yAxisLabel.setAttribute('y', y + 20); yAxisLabel.setAttribute('text-anchor', 'end'); yAxisLabel.setAttribute('class', 'gantt-axis-text'); yAxisLabel.textContent = task.name.length > 20 ? task.name.substring(0, 18) + '...' : task.name; ganttChartSVG.appendChild(yAxisLabel); }); }; const renderAll = () => { renderTaskList(); renderDependencyList(); renderGanttChart(); }; // --- CORE LOGIC --- // const calculateGanttData = () => { if (tasks.length === 0) { return { scheduledTasks: [], criticalPath: [], maxEndDate: 0 }; } // Deep copy to avoid modifying original tasks let processingTasks = JSON.parse(JSON.stringify(tasks)); // Add start/end properties processingTasks.forEach(t => { t.start = 0; t.end = 0; }); // Topological sort to handle dependencies let scheduledTasks = []; let queue = processingTasks.filter(t => t.dependencies.length === 0); let processingMap = new Map(processingTasks.map(t => [t.id, t])); while (queue.length > 0) { let current = queue.shift(); scheduledTasks.push(current); processingTasks.forEach(otherTask => { const depIndex = otherTask.dependencies.indexOf(current.id); if (depIndex > -1) { otherTask.dependencies.splice(depIndex, 1); if (otherTask.dependencies.length === 0) { queue.push(otherTask); } } }); } // Check for circular dependencies if (scheduledTasks.length !== tasks.length) { console.error("Circular dependency detected!"); // Handle this gracefully in UI ganttChartSVG.innerHTML = `Error: Circular dependency detected. Please review dependencies.`; return { scheduledTasks: [], criticalPath: [], maxEndDate: 0 }; } // Calculate Early Start (ES) and Early Finish (EF) scheduledTasks.forEach(task => { let maxDependencyEnd = 0; const originalTask = tasks.find(t => t.id === task.id); if (originalTask && originalTask.dependencies.length > 0) { originalTask.dependencies.forEach(depId => { const depTask = scheduledTasks.find(st => st.id === depId); if (depTask) { maxDependencyEnd = Math.max(maxDependencyEnd, depTask.end); } }); } task.start = maxDependencyEnd; task.end = task.start + task.duration; }); const maxEndDate = Math.max(0, ...scheduledTasks.map(t => t.end)); // Calculate Late Start (LS) and Late Finish (LF) for critical path scheduledTasks.forEach(t => { t.lf = maxEndDate; t.ls = maxEndDate; }); for (let i = scheduledTasks.length - 1; i >= 0; i--) { const task = scheduledTasks[i]; const originalTask = tasks.find(t => t.id === task.id); // Find tasks that depend on the current task const successors = tasks.filter(t => t.dependencies.includes(task.id)); if (successors.length === 0) { task.lf = maxEndDate; } else { let minSuccessorStart = Infinity; successors.forEach(succIdObj => { const succTask = scheduledTasks.find(st => st.id === succIdObj.id); if (succTask) { minSuccessorStart = Math.min(minSuccessorStart, succTask.ls); } }); task.lf = minSuccessorStart; } task.ls = task.lf - task.duration; } // Identify critical path (where ES === LS) const criticalPath = scheduledTasks.filter(t => t.start === t.ls).map(t => t.id); return { scheduledTasks, criticalPath, maxEndDate }; }; // --- EVENT HANDLERS --- // taskForm.addEventListener('submit', (e) => { e.preventDefault(); const id = parseInt(taskIdInput.value); const name = taskNameInput.value.trim(); const duration = parseInt(taskDurationInput.value); const cost = parseFloat(taskCostInput.value) || 0; if (!name || isNaN(duration) || duration <= 0) { alert('Please provide a valid task name and a positive duration.'); return; } if (id) { // Update existing task const task = tasks.find(t => t.id === id); if (task) { task.name = name; task.duration = duration; task.cost = cost; } } else { // Add new task const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1; tasks.push({ id: newId, name, duration, cost, dependencies: [] }); } taskForm.reset(); taskIdInput.value = ''; renderAll(); }); clearFormBtn.addEventListener('click', () => { taskForm.reset(); taskIdInput.value = ''; }); // Make edit/delete functions globally accessible from inline onclick window.editTask = (id) => { const task = tasks.find(t => t.id === id); if (task) { taskIdInput.value = task.id; taskNameInput.value = task.name; taskDurationInput.value = task.duration; taskCostInput.value = task.cost; taskNameInput.focus(); } }; window.deleteTask = (id) => { if (confirm('Are you sure you want to delete this task? This will also remove it from any dependencies.')) { tasks = tasks.filter(t => t.id !== id); // Also remove this id from any other task's dependency list tasks.forEach(task => { task.dependencies = task.dependencies.filter(depId => depId !== id); }); renderAll(); } }; downloadPdfBtn.addEventListener('click', () => { const { jsPDF } = window.jspdf; const ganttOutput = document.getElementById('gantt-chart-output'); const { scheduledTasks, criticalPath, maxEndDate } = calculateGanttData(); if (!ganttOutput || tasks.length === 0) { alert("There is no chart to download."); return; } const pdf = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' }); // Add Title pdf.setFontSize(18); pdf.text("Project Task Dependencies Report", 40, 40); pdf.setFontSize(12); pdf.text(`Total Project Duration: ${maxEndDate} days`, 40, 60); // Add Gantt Chart Image html2canvas(ganttOutput, { scale: 2 }).then(canvas => { const imgData = canvas.toDataURL('image/png'); const imgProps = pdf.getImageProperties(imgData); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width; pdf.addImage(imgData, 'PNG', 40, 80, pdfWidth - 80, pdfHeight - 80); // Add a new page for the task table pdf.addPage(); pdf.setFontSize(18); pdf.text("Task Details", 40, 40); // Add Task Table const tableData = scheduledTasks.map(task => { const deps = tasks.find(t => t.id === task.id)?.dependencies || []; const depNames = deps.map(depId => tasks.find(t => t.id === depId)?.name || '').join(', '); return [ task.id, task.name, task.duration, `$${task.cost.toLocaleString()}`, task.start, task.end, depNames ]; }); pdf.autoTable({ startY: 60, head: [['ID', 'Task Name', 'Duration', 'Cost', 'Start Day', 'End Day', 'Dependencies']], body: tableData, theme: 'grid', headStyles: { fillColor: [41, 128, 185] } }); pdf.save('Project_Dependencies_Report.pdf'); }); }); // --- TAB NAVIGATION LOGIC --- // const updateTabs = () => { tabContents.forEach(content => content.classList.add('hidden')); tabButtons.forEach(button => button.classList.remove('active')); const activeTabId = `${tabs[currentTab]}-tab`; const activeContent = document.getElementById(activeTabId); const activeButton = document.querySelector(`.tab-btn[data-tab="${tabs[currentTab]}"]`); if(activeContent) activeContent.classList.remove('hidden'); if(activeButton) activeButton.classList.add('active'); prevBtn.disabled = currentTab === 0; nextBtn.disabled = currentTab === tabs.length - 1; // Re-render relevant content when switching to a tab if (tabs[currentTab] === 'dependencies') { renderDependencyList(); } else if (tabs[currentTab] === 'gantt') { renderGanttChart(); } }; tabButtons.forEach((button, index) => { button.addEventListener('click', () => { currentTab = index; updateTabs(); }); }); prevBtn.addEventListener('click', () => { if (currentTab > 0) { currentTab--; updateTabs(); } }); nextBtn.addEventListener('click', () => { if (currentTab < tabs.length - 1) { currentTab++; updateTabs(); } }); // --- FINAL SETUP --- // loadSampleData(); // Load sample data on initial load updateTabs(); // Set initial tab state // Add autoTable plugin for jsPDF (function (jsPDFAPI) { // ... [autoTable plugin code - this is a large block, so for brevity it's assumed to be loaded] // In a real scenario, this would be a separate script tag. For this single file, we'll simulate its presence. // A simplified version for demonstration: jsPDF.API.autoTable = function(options) { console.log("jsPDF.autoTable called with:", options); // This is where the real autoTable logic would generate the table. // For now, let's just draw a basic representation. let doc = this; let cursorY = options.startY || 40; const head = options.head[0]; const body = options.body; const colWidth = (doc.internal.pageSize.getWidth() - 80) / head.length; doc.setFontSize(10); doc.setFont(undefined, 'bold'); head.forEach((h, i) => { doc.rect(40 + i * colWidth, cursorY, colWidth, 20, 'S'); doc.text(h, 45 + i * colWidth, cursorY + 14); }); cursorY += 20; doc.setFont(undefined, 'normal'); body.forEach(row => { row.forEach((cell, i) => { doc.rect(40 + i * colWidth, cursorY, colWidth, 20, 'S'); doc.text(String(cell), 45 + i * colWidth, cursorY + 14); }); cursorY += 20; }); return doc; }; })(jsPDF.API); });
Scroll to Top