';
return;
}
stsoTasks.forEach(task => {
const item = document.createElement('div');
item.className = 'stso-task-list-item';
const details = document.createElement('div');
details.className = 'stso-task-details';
const depNames = task.dependencies.map(depId => stsoTasks.find(t => t.id === depId)?.name || 'Unknown Task').join(', ');
details.innerHTML = `
${task.name} (Duration: ${task.duration} ${task.durationUnit}, Priority: ${task.priority})
Dependencies: ${depNames || 'None'}
`;
const buttons = document.createElement('div');
const editButton = document.createElement('button');
editButton.className = 'stso-small-button';
editButton.textContent = 'Edit Deps';
editButton.onclick = () => stsoOpenDependencyModal(task.id);
const deleteButton = document.createElement('button');
deleteButton.className = 'stso-small-button stso-danger-button';
deleteButton.textContent = 'Delete';
deleteButton.onclick = () => stsoDeleteTask(task.id);
buttons.appendChild(editButton);
buttons.appendChild(deleteButton);
item.appendChild(details);
item.appendChild(buttons);
listContainer.appendChild(item);
});
}
function stsoDeleteTask(taskId) {
if (!confirm('Are you sure you want to delete this task? This will also remove it from other tasks\' dependencies.')) return;
stsoTasks = stsoTasks.filter(task => task.id !== taskId);
// Remove this task from any other tasks' dependencies
stsoTasks.forEach(task => {
task.dependencies = task.dependencies.filter(depId => depId !== taskId);
});
stsoRenderTaskList();
stsoOptimizedResult = null; // Invalidate previous optimization
if (document.getElementById('stso-tab-optimize').classList.contains('active')) {
document.getElementById('stso-optimized-sequence-body').innerHTML = '';
document.getElementById('stso-total-duration-display').textContent = '';
document.getElementById('stso-no-tasks-optimized-message').style.display = 'block';
document.getElementById('stso-optimization-message').style.display = 'none';
}
}
function stsoOpenDependencyModal(taskId) {
stsoCurrentEditingTaskId = taskId;
const task = stsoTasks.find(t => t.id === taskId);
if (!task) return;
document.getElementById('stso-modal-task-name').textContent = `Set Dependencies for: ${task.name}`;
const checklist = document.getElementById('stso-dependency-checklist');
checklist.innerHTML = '';
stsoTasks.filter(t => t.id !== taskId).forEach(otherTask => {
const li = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `dep-checkbox-${otherTask.id}`;
checkbox.value = otherTask.id;
checkbox.checked = task.dependencies.includes(otherTask.id);
const label = document.createElement('label');
label.htmlFor = `dep-checkbox-${otherTask.id}`;
label.textContent = otherTask.name;
li.appendChild(checkbox);
li.appendChild(label);
checklist.appendChild(li);
});
document.getElementById('stso-dependency-modal').style.display = 'block';
}
function stsoCloseDependencyModal() {
document.getElementById('stso-dependency-modal').style.display = 'none';
stsoCurrentEditingTaskId = null;
}
function stsoSaveDependencies() {
const task = stsoTasks.find(t => t.id === stsoCurrentEditingTaskId);
if (!task) return;
const newDependencies = [];
document.querySelectorAll('#stso-dependency-checklist input[type="checkbox"]:checked').forEach(cb => {
newDependencies.push(parseInt(cb.value));
});
task.dependencies = newDependencies;
stsoRenderTaskList();
stsoCloseDependencyModal();
stsoOptimizedResult = null; // Invalidate previous optimization if dependencies change
}
function stsoOptimizeSequence() {
if (stsoTasks.length === 0) {
stsoShowMessage('info', 'No tasks to optimize. Please add tasks first.', 'stso-optimization-message');
document.getElementById('stso-optimized-sequence-body').innerHTML = '';
document.getElementById('stso-no-tasks-optimized-message').style.display = 'block';
document.getElementById('stso-total-duration-display').textContent = '';
return;
}
document.getElementById('stso-no-tasks-optimized-message').style.display = 'none';
// Graph representation
const adj = new Map(); // task_id -> [dependent_task_ids]
const inDegree = new Map(); // task_id -> count
const taskMap = new Map(stsoTasks.map(task => [task.id, task]));
stsoTasks.forEach(task => {
adj.set(task.id, []);
inDegree.set(task.id, 0);
});
stsoTasks.forEach(task => {
task.dependencies.forEach(depId => {
if (adj.has(depId) && taskMap.has(task.id)) { // Ensure both exist
adj.get(depId).push(task.id);
inDegree.set(task.id, (inDegree.get(task.id) || 0) + 1);
}
});
});
// Ready queue: Min-heap based on (1. Priority DESC, 2. Duration ASC)
// Higher priority value means higher priority. Lower duration means higher priority.
const readyQueue = stsoTasks.filter(task => inDegree.get(task.id) === 0)
.sort((a, b) => {
if (PRIORITY_VALUES[b.priority] !== PRIORITY_VALUES[a.priority]) {
return PRIORITY_VALUES[b.priority] - PRIORITY_VALUES[a.priority];
}
return a.duration - b.duration;
});
const optimizedSequence = [];
let currentTime = 0;
const taskEndTimes = new Map(); // To track when prerequisites finish
while (readyQueue.length > 0) {
const currentTaskDetails = readyQueue.shift(); // Get highest priority
const currentTask = taskMap.get(currentTaskDetails.id);
let taskStartTime = 0;
currentTask.dependencies.forEach(depId => {
taskStartTime = Math.max(taskStartTime, taskEndTimes.get(depId) || 0);
});
// If tasks are strictly sequential in THIS final list and not just by deps:
// taskStartTime = Math.max(taskStartTime, currentTime);
optimizedSequence.push({
...currentTask,
startTime: taskStartTime,
endTime: taskStartTime + currentTask.duration
});
taskEndTimes.set(currentTask.id, taskStartTime + currentTask.duration);
// For strictly sequential total time based on THIS list (not parallel execution):
// currentTime = taskStartTime + currentTask.duration;
(adj.get(currentTask.id) || []).forEach(dependentTaskId => {
inDegree.set(dependentTaskId, inDegree.get(dependentTaskId) - 1);
if (inDegree.get(dependentTaskId) === 0) {
const dependentTask = taskMap.get(dependentTaskId);
// Add to readyQueue and re-sort (simple way for now, a proper P-Queue would be better)
readyQueue.push(dependentTask);
readyQueue.sort((a, b) => {
if (PRIORITY_VALUES[b.priority] !== PRIORITY_VALUES[a.priority]) {
return PRIORITY_VALUES[b.priority] - PRIORITY_VALUES[a.priority];
}
return a.duration - b.duration;
});
}
});
}
// Re-calculate start/end times for the final linear sequence for simpler display
let linearCurrentTime = 0;
const finalLinearSequence = optimizedSequence.map(task => {
const startTime = linearCurrentTime;
const endTime = startTime + task.duration;
linearCurrentTime = endTime;
return {...task, startTime, endTime};
});
const tableBody = document.getElementById('stso-optimized-sequence-body');
tableBody.innerHTML = '';
if (finalLinearSequence.length !== stsoTasks.length) {
stsoShowMessage('error', 'Cycle detected in dependencies! Cannot create a valid sequence. Please review task dependencies.', 'stso-optimization-message');
stsoOptimizedResult = { error: "Cycle detected", tasks: [] }; // Store error state
document.getElementById('stso-total-duration-display').textContent = '';
return;
}
stsoShowMessage('success', 'Task sequence optimized successfully!', 'stso-optimization-message');
stsoOptimizedResult = { tasks: finalLinearSequence, durationUnit: stsoTasks.length > 0 ? stsoTasks[0].durationUnit : "units" }; // Store for PDF
let totalDuration = 0;
finalLinearSequence.forEach((task, index) => {
const row = tableBody.insertRow();
row.insertCell().textContent = index + 1;
row.insertCell().textContent = task.name;
row.insertCell().textContent = `${task.duration} ${task.durationUnit}`;
const priorityCell = row.insertCell();
priorityCell.textContent = task.priority;
priorityCell.className = `stso-priority-${task.priority}`;
const depNames = task.dependencies.map(depId => taskMap.get(depId)?.name || 'Unknown').join(', ') || 'None';
row.insertCell().textContent = depNames;
row.insertCell().textContent = task.startTime.toFixed(1);
row.insertCell().textContent = task.endTime.toFixed(1);
totalDuration = Math.max(totalDuration, task.endTime);
});
document.getElementById('stso-total-duration-display').textContent = `Total Estimated Project Duration: ${totalDuration.toFixed(1)} ${stsoOptimizedResult.durationUnit}`;
}
function stsoShowMessage(type, message, elementId) {
const msgElement = document.getElementById(elementId);
msgElement.textContent = message;
msgElement.className = `stso-message stso-message-${type}`;
msgElement.style.display = 'block';
setTimeout(() => { msgElement.style.display = 'none'; }, 5000);
}
function stsoDownloadPDF() {
if (!stsoOptimizedResult || stsoOptimizedResult.tasks.length === 0) {
alert("No optimized sequence to download. Please generate the sequence first.");
return;
}
if (stsoOptimizedResult.error) {
alert(`Cannot generate PDF due to an error: ${stsoOptimizedResult.error}`);
return;
}
const doc = new jsPDF('p', 'pt', 'a4'); // Using points for finer control, A4 size
const FONT_FAMILY = "helvetica";
const ACCENT_COLOR_HEX = getComputedStyle(document.documentElement).getPropertyValue('--accent-color').trim();
const PRIMARY_TEXT_COLOR_HEX = getComputedStyle(document.documentElement).getPropertyValue('--primary-text').trim();
const BUTTON_TEXT_COLOR_HEX = getComputedStyle(document.documentElement).getPropertyValue('--button-text-color').trim();
let yPos = 40;
const pageWidth = doc.internal.pageSize.getWidth();
const margin = 40;
// Header
doc.setFillColor(ACCENT_COLOR_HEX);
doc.rect(0, 0, pageWidth, yPos + 5, 'F');
doc.setFont(FONT_FAMILY, "bold");
doc.setFontSize(18);
doc.setTextColor(BUTTON_TEXT_COLOR_HEX);
doc.text("Optimized Task Sequence Report", pageWidth / 2, yPos -5, { align: "center" });
yPos += 25;
// Report Date
doc.setFont(FONT_FAMILY, "normal");
doc.setFontSize(10);
doc.setTextColor(PRIMARY_TEXT_COLOR_HEX);
const reportDate = new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
doc.text(`Report Generated: ${reportDate}`, margin, yPos);
yPos += 20;
const tableColumns = ["#", "Task Name", "Duration", "Priority", "Dependencies", "Start", "End"];
const tableRows = stsoOptimizedResult.tasks.map((task, index) => [
index + 1,
task.name,
`${task.duration} ${task.durationUnit}`,
task.priority,
task.dependencies.map(depId => stsoTasks.find(t => t.id === depId)?.name || '?').join(', ') || 'None',
task.startTime.toFixed(1),
task.endTime.toFixed(1)
]);
doc.autoTable({
head: [tableColumns],
body: tableRows,
startY: yPos,
theme: 'grid',
margin: { left: margin, right: margin },
headStyles: {
fillColor: ACCENT_COLOR_HEX,
textColor: BUTTON_TEXT_COLOR_HEX,
fontStyle: 'bold',
fontSize: 9,
halign: 'center'
},
styles: {
font: FONT_FAMILY,
fontSize: 8,
cellPadding: 3,
overflow: 'linebreak'
},
columnStyles: {
0: { cellWidth: 25, halign: 'center' }, // Order #
1: { cellWidth: 'auto' }, // Task Name
2: { cellWidth: 50, halign: 'right' }, // Duration
3: { cellWidth: 40, halign: 'center' }, // Priority
4: { cellWidth: 'auto' }, // Dependencies
5: { cellWidth: 35, halign: 'right' }, // Start
6: { cellWidth: 35, halign: 'right' } // End
},
didParseCell: function (data) { // Color priority
if (data.column.index === 3 && data.cell.section === 'body') {
const priority = data.cell.raw;
if (priority === "High") data.cell.styles.textColor = getComputedStyle(document.documentElement).getPropertyValue('--danger-color').trim();
if (priority === "Medium") data.cell.styles.textColor = getComputedStyle(document.documentElement).getPropertyValue('--info-color').trim();
if (priority === "Low") data.cell.styles.textColor = getComputedStyle(document.documentElement).getPropertyValue('--success-color').trim();
data.cell.styles.fontStyle = 'bold';
}
}
});
yPos = doc.lastAutoTable.finalY + 20;
doc.setFont(FONT_FAMILY, "bold");
doc.setFontSize(11);
doc.setTextColor(PRIMARY_TEXT_COLOR_HEX);
const totalDurationValue = stsoOptimizedResult.tasks.length > 0 ? stsoOptimizedResult.tasks[stsoOptimizedResult.tasks.length - 1].endTime : 0;
doc.text(`Total Estimated Project Duration: ${totalDurationValue.toFixed(1)} ${stsoOptimizedResult.durationUnit}`, margin, yPos, {align: 'right', maxWidth: pageWidth - (2*margin)});
yPos += 20;
// Footer
const pageCount = doc.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
const pageHeight = doc.internal.pageSize.getHeight();
doc.setLineWidth(0.5);
doc.setDrawColor(ACCENT_COLOR_HEX);
doc.line(margin, pageHeight - 30, pageWidth - margin, pageHeight - 30);
doc.setFont(FONT_FAMILY, "normal");
doc.setFontSize(8);
doc.setTextColor(PRIMARY_TEXT_COLOR_HEX);
doc.text(`Smart Task Sequence Optimizer - Page ${i} of ${pageCount}`, pageWidth / 2, pageHeight - 20, { align: "center" });
}
doc.save("Optimized_Task_Sequence.pdf");
}