`;
taskListUL.appendChild(li);
});
document.querySelectorAll('.edit-task-btn').forEach(btn => btn.addEventListener('click', (e) => populateFormForEdit(e.target.dataset.id)));
document.querySelectorAll('.delete-task-btn').forEach(btn => btn.addEventListener('click', (e) => {
if (confirm('Are you sure you want to delete this task?')) deleteTask(e.target.dataset.id);
}));
}
function formatTimeDiff(ms) {
if (ms < 0) return "ago";
let seconds = Math.floor(ms / 1000);
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
if (days > 0) return `${days}d ${hours % 24}h left`;
if (hours > 0) return `${hours}h ${minutes % 60}m left`;
if (minutes > 0) return `${minutes}m ${seconds % 60}s left`;
return `${seconds}s left`;
}
// --- Reminder Logic ---
function checkReminders() {
const now = new Date().getTime();
let nextReminderCheckTime = Infinity; // For scheduling the global check more efficiently
tasks.forEach(task => {
if (task.isSnoozed && task.snoozeUntil && now < task.snoozeUntil) {
nextReminderCheckTime = Math.min(nextReminderCheckTime, task.snoozeUntil);
return; // Still snoozed
}
if (task.isSnoozed) task.isSnoozed = false; // Snooze period over
const dueDateTime = new Date(task.dueDateTime).getTime();
const reminderTriggerTime = dueDateTime - (task.reminderLeadTime * 60000);
if (now >= reminderTriggerTime && dueDateTime > (task.lastDismissedAt || 0) && !task.triggeredThisSession) {
// Check if it should have triggered and hasn't been dismissed for this occurrence yet
// `triggeredThisSession` prevents re-triggering immediately on page load if already handled by setTimeout
if (now < dueDateTime + (5 * 60000)) { // Allow a 5-min window past due time for reminder to be relevant
triggerReminder(task);
task.triggeredThisSession = true; // Mark as triggered to avoid loops by setInterval
}
} else if (reminderTriggerTime > now) {
nextReminderCheckTime = Math.min(nextReminderCheckTime, reminderTriggerTime);
}
});
renderTaskList(); // Update display (e.g. time until due)
scheduleNextGlobalReminderCheck(nextReminderCheckTime); // Schedule next check based on closest reminder
}
function scheduleNextGlobalReminderCheck(specificTime = null) {
if (activeReminderTimeout) clearTimeout(activeReminderTimeout); // Clear previous specific reminder timeout
if (reminderCheckInterval) clearInterval(reminderCheckInterval); // Clear the general interval
// General periodic check (e.g., every minute) for UI updates and catching missed reminders
reminderCheckInterval = setInterval(checkReminders, 30000); // Check every 30 seconds
// More precise check for the very next upcoming reminder
if (specificTime && specificTime !== Infinity && specificTime > new Date().getTime()) {
const delay = specificTime - new Date().getTime();
activeReminderTimeout = setTimeout(() => {
checkReminders(); // This will find and trigger the specific reminder
}, delay);
}
}
function triggerReminder(task) {
currentRemindingTask = task;
reminderTextP.innerHTML = `${task.description} is due at ${new Date(task.dueDateTime).toLocaleTimeString()}.`;
reminderAlertDiv.style.display = 'block';
if (soundEnabled) {
reminderSound.currentTime = 0; // Rewind
reminderSound.play().catch(e => console.warn("Audio play failed:", e)); // Autoplay might be blocked
}
if (notificationPermission === 'granted') {
try {
const notificationBody = `Due: ${new Date(task.dueDateTime).toLocaleTimeString()}\nPriority: ${task.priority}`;
const notification = new Notification(task.description, {
body: notificationBody,
icon: 'https://ssl.gstatic.com/calendar/images/dynamiclogo_2020q4/calendar_14_2x.png' // Generic icon
});
notification.onclick = () => { // Focus tab on click
window.focus();
reminderAlertDiv.style.display = 'block'; // Ensure in-page is visible
};
} catch(e) {
console.error("Error showing browser notification:", e);
}
}
}
closeReminderAlertBtn.onclick = () => reminderAlertDiv.style.display = 'none';
dismissReminderBtn.addEventListener('click', () => {
if (!currentRemindingTask) return;
currentRemindingTask.lastDismissedAt = new Date().getTime();
currentRemindingTask.triggeredThisSession = false; // Reset for next potential trigger
currentRemindingTask.isSnoozed = false;
if (currentRemindingTask.recurrence === 'daily') {
const currentDueDate = new Date(currentRemindingTask.originalDueDateTime); // Base next on original due time
let nextDueDate = new Date(currentDueDate);
// Ensure nextDueDate is in the future relative to current dueDateTime
const now = new Date();
let taskLastEffectiveDueDate = new Date(currentRemindingTask.dueDateTime);
if (taskLastEffectiveDueDate <= now) { // If current due date is past or now
nextDueDate.setDate(now.getDate() + 1); // Set to tomorrow relative to today
nextDueDate.setHours(currentDueDate.getHours(), currentDueDate.getMinutes(), 0, 0); // Keep original time
} else { // If current due date is still in future (e.g. dismissed early)
// This case means it was dismissed before it was due, just advance from its current due date
nextDueDate = new Date(taskLastEffectiveDueDate);
nextDueDate.setDate(nextDueDate.getDate() + 1);
}
currentRemindingTask.dueDateTime = nextDueDate.toISOString().slice(0, 16);
// Note: originalDueDateTime remains the same for daily recurrence to keep the time of day.
} else {
// For non-recurring, effectively "done" with reminders for this task unless edited.
// Could add a 'completed' flag if needed.
}
const taskIndex = tasks.findIndex(t => t.id === currentRemindingTask.id);
if (taskIndex > -1) tasks[taskIndex] = { ...currentRemindingTask }; // Update task in main array
saveTasks();
renderTaskList();
reminderAlertDiv.style.display = 'none';
currentRemindingTask = null;
scheduleNextGlobalReminderCheck();
});
function snoozeReminder(minutes) {
if (!currentRemindingTask) return;
const now = new Date().getTime();
currentRemindingTask.snoozeUntil = now + (minutes * 60000);
currentRemindingTask.isSnoozed = true;
currentRemindingTask.triggeredThisSession = false; // Allow it to be picked up by checkReminders after snooze
const taskIndex = tasks.findIndex(t => t.id === currentRemindingTask.id);
if (taskIndex > -1) tasks[taskIndex] = { ...currentRemindingTask };
saveTasks();
renderTaskList();
reminderAlertDiv.style.display = 'none';
currentRemindingTask = null;
scheduleNextGlobalReminderCheck(); // Will pick up the snoozeUntil time
}
snooze5minBtn.addEventListener('click', () => snoozeReminder(5));
snooze10minBtn.addEventListener('click', () => snoozeReminder(10));
// --- PDF Export ---
downloadTasksPdfBtn.addEventListener('click', () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
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('Task Reminder List', 14, 22);
doc.setFontSize(11);
doc.setTextColor(100);
doc.text(`Generated on: ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`, 14, 30);
const body = [];
const sortedTasksForPdf = [...tasks].sort((a,b) => new Date(a.dueDateTime) - new Date(b.dueDateTime));
sortedTasksForPdf.forEach(task => {
const dueDate = new Date(task.dueDateTime);
const reminderLead = task.reminderLeadTime === 0 ? 'At event time' : `${task.reminderLeadTime} min before`;
body.push([
task.description,
dueDate.toLocaleString(),
task.priority.charAt(0).toUpperCase() + task.priority.slice(1),
reminderLead,
task.recurrence !== 'none' ? task.recurrence.charAt(0).toUpperCase() + task.recurrence.slice(1) : 'One-time'
]);
});
if (body.length === 0) {
doc.text("No tasks to report.", 14, 45);
} else {
doc.autoTable({
startY: 35,
head: [['Description', 'Due Date & Time', 'Priority', 'Reminder', 'Recurrence']],
body: body,
theme: 'grid',
headStyles: { fillColor: [0, 123, 255], textColor: 255 }, // Primary color header
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('--success-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_Reminder_List.pdf');
});
// --- Initial Load ---
renderTaskList();
checkReminders(); // Initial check and schedule next
});