/** * api.js * * Powers the Dashboard page. Talks to the Express / Mongoose backend * at http://localhost:3001 to create, read, update and delete tasks. * * Core CRUD (original): * - displayTasks() GET /api/tasks * - createNewTask() POST /api/tasks/todo * - completeTask() PATCH /api/tasks/complete/:id * - taskNotCompleted() PATCH /api/tasks/notComplete/:id * - deleteTask() DELETE /api/tasks/delete/:id * - editTask() PUT /api/tasks/update/:id * * Added interactivity: * - Stats cards: renders live counts of Active, Completed, Overdue * and Due-Today tasks (see updateStats()). * - Search filter: an input box filters the rendered task lists by * title/description on every keystroke, without re-hitting the * API (see renderTasks()). * - Quick-pick date buttons: "Today", "Tomorrow" and "Next week" * populate the due-date field of the new-task form. * * The in-memory `allTasks` array holds the latest fetched list so * search filtering can be recomputed purely on the client side. */ const taskForm = document.getElementById('taskForm'); const toDoList = document.getElementById('toDoList'); const completedList = document.getElementById('completedList'); const toDoEmpty = document.getElementById('toDoEmpty'); const completedEmpty = document.getElementById('completedEmpty'); const searchInput = document.getElementById('searchInput'); const dueDateInput = document.getElementById('dueDate'); const url = "http://localhost:3001"; // Cache of the most recent /api/tasks response so the search filter // can re-render instantly without refetching. let allTasks = []; function resetForm() { taskForm.reset(); } function showAlert(message, variant = 'success') { const container = document.getElementById('alertContainer'); if (!container) return; const alert = document.createElement('div'); alert.className = `alert alert-${variant} alert-dismissible fade show shadow-sm`; alert.role = 'alert'; alert.innerHTML = ` ${message} `; container.appendChild(alert); setTimeout(() => bootstrap.Alert.getOrCreateInstance(alert).close(), 3000); } const sortButton = document.getElementById("sortSelect"); window.addEventListener("DOMContentLoaded", () => { sortButton.value = "default"; }); sortButton.addEventListener('change', () => { displayTasks(); }); // Search filter: re-renders the two lists on every keystroke using // the cached `allTasks` array. No API round-trip required. if (searchInput) { searchInput.addEventListener('input', () => { renderTasks(); }); } // Quick-pick date buttons: set the due-date input to today, tomorrow // or a week from now. Saves the user from having to open the date // picker for the most common options. document.querySelectorAll('.quick-date-btn').forEach(btn => { btn.addEventListener('click', () => { const offsetDays = { today: 0, tomorrow: 1, nextWeek: 7 }[btn.dataset.quick] ?? 0; const date = new Date(); date.setDate(date.getDate() + offsetDays); dueDateInput.value = date.toISOString().split('T')[0]; }); }); window.addEventListener("DOMContentLoaded", () => { displayTasks(); }); taskForm.addEventListener("submit", (event) => { event.preventDefault(); createNewTask(); }); [toDoList, completedList].forEach(list => { list.addEventListener('click', (event) => { if (event.target.classList.contains("done")) { const taskId = event.target.getAttribute("data-id"); completeTask(taskId); } else if (event.target.classList.contains("notDone")) { const taskId = event.target.getAttribute("data-id"); taskNotCompleted(taskId); } else if (event.target.classList.contains("delete")) { const taskId = event.target.getAttribute("data-id"); const confirmButton = document.getElementById('confirmDeleteButton'); confirmButton.addEventListener('click', async () => { await deleteTask(taskId); bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide(); }, { once: true }); } else if (event.target.classList.contains('edit')) { const task = { id: event.target.getAttribute('data-id'), title: event.target.getAttribute('data-title'), description: event.target.getAttribute('data-description'), dueDate: new Date(event.target.getAttribute('data-due-date')) }; const modal = { titleInput: document.getElementById('editTaskName'), descriptionInput: document.getElementById('editTaskDescription'), dueDateInput: document.getElementById('editDueDate'), saveButton: document.getElementById('saveButton') }; modal.titleInput.value = task.title; modal.descriptionInput.value = task.description; modal.dueDateInput.value = task.dueDate.toISOString().split('T')[0]; modal.saveButton.addEventListener('click', async () => { await editTask(task.id); bootstrap.Modal.getInstance(document.getElementById('editModal')).hide(); }, { once: true }); } }); }); async function displayTasks() { const sortSelect = document.getElementById('sortSelect'); const sortBy = sortSelect.value; let query = ''; if (sortBy !== 'default') { query = `?sortBy=${sortBy}`; } try { const response = await fetch(`${url}/api/tasks${query}`); if (!response.ok) { throw new Error(`Failed to get tasks: ${response.status}`); } allTasks = await response.json(); updateStats(allTasks); renderTasks(); resetForm(); } catch (error) { console.error("Error: ", error); } } function formatTask(task) { const li = document.createElement("li"); li.classList.add("card", "p-3", "shadow-sm", "mt-2"); const done = task.completed ? "text-decoration-line-through opacity-50" : ""; li.innerHTML = `

${task.title}

${task.description}

Due: ${new Date(task.dueDate).toLocaleDateString()}

${task.completed ? `` : ` ` }

Created on: ${new Date(task.dateCreated).toLocaleDateString()}

`; return li; } /** * Render the cached task list into the To-Do and Completed columns. * Applies the current search term (if any) as a case-insensitive * match on title or description, and toggles per-column empty-state * messages when nothing matches. */ function renderTasks() { const term = (searchInput?.value || '').trim().toLowerCase(); const filtered = term ? allTasks.filter(t => t.title.toLowerCase().includes(term) || t.description.toLowerCase().includes(term)) : allTasks; toDoList.innerHTML = ''; completedList.innerHTML = ''; let activeCount = 0; let completedCount = 0; filtered.forEach(task => { const node = formatTask(task); if (task.completed) { completedList.appendChild(node); completedCount += 1; } else { toDoList.appendChild(node); activeCount += 1; } }); toDoEmpty?.classList.toggle('d-none', activeCount > 0); completedEmpty?.classList.toggle('d-none', completedCount > 0); } /** * Recompute and display the four dashboard stat cards. * * Active - tasks that are not yet completed * Completed - tasks marked done * Overdue - active tasks whose due date is before today * Due Today - active tasks whose due date is today * * Dates are normalised to midnight so the comparison ignores time * of day. */ function updateStats(tasks) { const today = new Date(); today.setHours(0, 0, 0, 0); let active = 0, completed = 0, overdue = 0, dueToday = 0; tasks.forEach(task => { const due = new Date(task.dueDate); due.setHours(0, 0, 0, 0); if (task.completed) { completed += 1; } else { active += 1; if (due.getTime() < today.getTime()) { overdue += 1; } else if (due.getTime() === today.getTime()) { dueToday += 1; } } }); const set = (id, value) => { const el = document.getElementById(id); if (el) el.textContent = value; }; set('statActive', active); set('statCompleted', completed); set('statOverdue', overdue); set('statDueToday', dueToday); } async function createNewTask() { const taskDetails = { title: taskForm.taskName.value.trim(), description: taskForm.taskDescription.value.trim(), dueDate: taskForm.dueDate.value } if (!taskDetails.title || !taskDetails.description || !taskDetails.dueDate) { return alert("All fields required!"); } try { const response = await fetch(`${url}/api/tasks/todo`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(taskDetails) }); if (!response.ok) { throw new Error(`Failed to post task: ${response.status}`); } const data = await response.json(); console.log(data); displayTasks(); } catch (error) { console.error("Error:", error); } } async function completeTask(taskId) { try { const response = await fetch(`${url}/api/tasks/complete/${taskId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ completed: true }) }); if (!response.ok) { throw new Error(`Failed to complete task: ${response.status}`); } const data = await response.json(); console.log("Task completed:", data); displayTasks(); } catch (error) { console.error("Error:", error); } }; async function taskNotCompleted(taskId) { try { const response = await fetch(`${url}/api/tasks/notComplete/${taskId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ completed: false }) }); if (!response.ok) { throw new Error(`Failed to make task incomplete: ${response.status}`); } const data = await response.json(); console.log("Task not complete:", data); displayTasks(); } catch (error) { console.error("Error:", error); } }; async function deleteTask(taskId) { try { const response = await fetch(`${url}/api/tasks/delete/${taskId}`, { method: "DELETE", headers: { "Content-Type": "application/json" } }); if (!response.ok) { throw new Error(`Failed to delete task: ${response.status}`); } const data = await response.json(); console.log({ message: "Deleted Task:", Task: data }); displayTasks(); showAlert('Task deleted successfully.'); } catch (error) { console.error("Error:", error); } }; async function editTask(taskId) { const updatedDetails = { title: document.getElementById('editTaskName').value.trim(), description: document.getElementById('editTaskDescription').value.trim(), dueDate: document.getElementById('editDueDate').value }; if (!updatedDetails.title || !updatedDetails.description || !updatedDetails.dueDate) { return alert("All fields required!"); } try { const response = await fetch(`${url}/api/tasks/update/${taskId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updatedDetails) }); if (!response.ok) { throw new Error(`Failed to edit task: ${response.status}`); } const data = await response.json(); console.log("Edited Task:", data); displayTasks(); } catch (error) { console.error("Error:", error); } };