Smart Repetitive Task Scheduler

Smart Repetitive Task Scheduler

Define New Repetitive Task

Recurrence Rule

day(s)

End Recurrence

Defined Task Series

'; return; } const ul = document.createElement('ul'); ul.className = 'item-list'; allInstances.forEach(instance => { const li = document.createElement('li'); li.className = `task-instance-item priority-${instance.priority}`; if (instance.isOverdue) li.classList.add('instance-overdue'); else if (instance.isToday) li.classList.add('instance-today'); li.innerHTML = `

${instance.description}

Due: ${new Date(instance.dueDate).toLocaleString()} ${instance.duration ? `Duration: ${instance.duration}` : ''} Priority: ${instance.priority}

`; ul.appendChild(li); }); taskInstancesViewDiv.appendChild(ul); // Add event listeners for Mark Done / Skip document.querySelectorAll('.mark-done-btn, .skip-instance-btn').forEach(btn => { btn.addEventListener('click', function() { handleInstanceAction(this.dataset.seriesId, this.dataset.instanceDate, this.classList.contains('mark-done-btn')); }); }); } function handleInstanceAction(seriesId, instanceDateStr, isMarkDone) { const seriesIndex = taskSeriesArray.findIndex(s => s.id === seriesId); if (seriesIndex === -1) return; const series = taskSeriesArray[seriesIndex]; // Ensure we are acting on the *current* nextDueDate of the series if (series.nextDueDate !== instanceDateStr) { // This can happen if user clicks on an older projected instance. // For simplicity, "Mark Done" / "Skip" always operate on the series' current `nextDueDate`. // A more complex system might allow marking historical instances done. // For now, if it's not the current nextDueDate, we could just refresh or give a message. // Let's assume for now it should be the current one to avoid complications with out-of-order completions. if (new Date(instanceDateStr) > new Date(series.nextDueDate)) { alert("You can only mark the current or next upcoming instance as done/skipped for this series from this view. This instance is further in the future."); return; } else if (new Date(instanceDateStr) < new Date(series.nextDueDate) && instanceDateStr !== series.lastCompletedInstanceDate) { // If user tries to mark an older instance as done (that is not the last completed one) // This implies they want to "catch up". // For now, let's just increment completed count and set this as last completed. if(isMarkDone) series.completedOccurrences++; series.lastCompletedInstanceDate = instanceDateStr; } } else { // Acting on the current `series.nextDueDate` if(isMarkDone) series.completedOccurrences++; series.lastCompletedInstanceDate = series.nextDueDate; } const newNextDueDate = calculateNextDueDate(series.lastCompletedInstanceDate, series, false); if (newNextDueDate) { series.nextDueDate = newNextDueDate; } else { // Task series has ended (either by condition or no more valid dates) series.nextDueDate = null; // Mark as no more due dates // Optionally: Could move to an "archived" state or flag as "completed series" alert(`Task series "${series.description}" has now ended based on its rules.`); } taskSeriesArray[seriesIndex] = series; saveTaskSeries(); renderTaskSeriesList(); // Update the defined series list (e.g. its next due date) generateAndDisplayInstances(); // Refresh the current view } // --- PDF Download --- downloadInstancesPdfBtn.addEventListener('click', () => { const instancesUl = taskInstancesViewDiv.querySelector('.item-list'); if (!instancesUl || !instancesUl.children.length) { alert('No instances to download for the current view.'); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); let viewStartStr, viewEndStr; const period = viewPeriodSelect.value; if (period === 'customRange') { viewStartStr = new Date(customStartDateInput.value + "T00:00:00").toLocaleDateString(); viewEndStr = new Date(customEndDateInput.value + "T23:59:59").toLocaleDateString(); } else { const tempStart = new Date(today); tempStart.setHours(0,0,0,0); const tempEnd = new Date(tempStart); if (period === 'next7days') tempEnd.setDate(tempStart.getDate() + 6); // 0-6 is 7 days else if (period === 'next30days') tempEnd.setDate(tempStart.getDate() + 29); viewStartStr = tempStart.toLocaleDateString(); viewEndStr = tempEnd.toLocaleDateString(); } doc.setFontSize(18); doc.setTextColor(parseInt(getComputedStyle(document.documentElement).getPropertyValue('--primary-color').substring(1,3),16), parseInt(getComputedStyle(document.documentElement).getPropertyValue('--primary-color').substring(3,5),16), parseInt(getComputedStyle(document.documentElement).getPropertyValue('--primary-color').substring(5,7),16)); doc.text('Scheduled Task Instances', 14, 22); doc.setFontSize(12); doc.setTextColor(100); doc.text(`Period: ${viewStartStr} - ${viewEndStr}`, 14, 30); doc.setFontSize(10); doc.text(`Generated on: ${new Date().toLocaleString()}`, 14, 36); const body = []; Array.from(instancesUl.children).forEach(li => { const description = li.querySelector('.task-description').textContent; // Extracting from textContent, so need to be careful with labels const metaText = li.querySelector('.task-meta').textContent; const dueDateMatch = metaText.match(/Due: (.*?)(?:Duration:|Priority:|$)/); const durationMatch = metaText.match(/Duration: (.*?)(?:Priority:|$)/); const priorityMatch = metaText.match(/Priority: (.*)/); const dueDate = dueDateMatch ? dueDateMatch[1].trim() : 'N/A'; const duration = durationMatch ? durationMatch[1].trim() : 'N/A'; const priority = priorityMatch ? priorityMatch[1].trim() : 'N/A'; body.push([dueDate, description, priority, duration]); }); if (body.length === 0) { doc.text("No instances in the selected period.", 14, 45); } else { doc.autoTable({ startY: 42, head: [['Due Date & Time', 'Description', 'Priority', 'Est. Duration']], body: body, theme: 'grid', headStyles: { fillColor: [0, 123, 255], textColor: 255 }, didParseCell: function (data) { const priorityColorsPDF = { high: getComputedStyle(document.documentElement).getPropertyValue('--danger-color').trim(), medium: getComputedStyle(document.documentElement).getPropertyValue('--warning-color').trim(), low: getComputedStyle(document.documentElement).getPropertyValue('--info-color').trim(), }; if (data.column.index === 2 && data.cell.section === 'body') { // Priority column const priorityValue = data.cell.raw.toString().toLowerCase(); if (priorityColorsPDF[priorityValue]) { data.cell.styles.textColor = priorityColorsPDF[priorityValue]; data.cell.styles.fontStyle = 'bold'; } } } }); } doc.save(`Task_Instances_${viewStartStr}_to_${viewEndStr}.pdf`); }); // --- Initializations --- setDateInputMin(); updateRecurrenceOptionsVisibility(); updateEndRecurrenceOptionsVisibility(); renderTaskSeriesList(); initializeCustomRangeDates(); // Set default custom range dates generateAndDisplayInstances(); // Initial load of instances for default view (next 7 days) })(); // End IIFE
Scroll to Top