let goals = []; let tasks = []; let finishedTasks = []; let selectedGoalId = null; let selectedTaskId = null; let sortableInstance = null; async function loadGoals() { try { goals = await get("/api/goals"); const selector = document.getElementById("goal-selector"); const activatedGoals = goals.filter(g => g.activated); selector.innerHTML = activatedGoals.map(goal => `` ).join(""); if (activatedGoals.length > 0) { selectedGoalId = activatedGoals[0].id; selector.value = selectedGoalId; await loadTasks(); } } catch (error) { console.error("Failed to load goals:", error); } } async function loadTasks() { if (!selectedGoalId) return; try { const allTasks = await get(`/api/tasks?goal_id=${selectedGoalId}`); tasks = allTasks.filter(t => t.status !== "done"); finishedTasks = allTasks.filter(t => t.status === "done"); renderTasks(); renderFinishedTasks(); initSortable(); initScrollFocus(); const doingTask = tasks.find(t => t.status === "doing"); if (doingTask) { scrollToTask(doingTask.id); } } catch (error) { console.error("Failed to load tasks:", error); } } function renderTasks() { const container = document.getElementById("tasks-list"); if (tasks.length === 0) { container.innerHTML = `

No tasks yet

Create your first task for this goal!

`; return; } container.innerHTML = tasks.map(task => `
${escapeHtml(task.title)}
${task.status}
`).join(""); } function renderFinishedTasks() { const container = document.getElementById("finished-list"); if (finishedTasks.length === 0) { container.innerHTML = '

No completed tasks yet

'; return; } container.innerHTML = finishedTasks.map(task => `
${escapeHtml(task.title)}
Completed: ${formatTime(task.finished_time)}
`).join(""); } function initSortable() { const container = document.getElementById("tasks-list"); if (sortableInstance) { sortableInstance.destroy(); } sortableInstance = Sortable.create(container, { animation: 150, ghostClass: "sortable-ghost", chosenClass: "sortable-chosen", filter: ".task-item.done", onEnd: async function(evt) { const taskId = evt.item.dataset.taskId; const newIndex = evt.newIndex; const prevTask = tasks[newIndex - 1]; const nextTask = tasks[newIndex + 1]; let prevOrder = prevTask ? prevTask.order : 0; let nextOrder = nextTask ? nextTask.order : prevOrder + 2; const newOrder = (prevOrder + nextOrder) / 2; try { await patch(`/api/tasks/${taskId}/order`, { order: newOrder }); await loadTasks(); } catch (error) { console.error("Failed to update order:", error); await loadTasks(); } } }); } function scrollToTask(taskId) { const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); if (taskElement) { const scrollView = document.getElementById("scroll-view"); const taskTop = taskElement.offsetTop; const scrollViewHeight = scrollView.clientHeight; const taskHeight = taskElement.offsetHeight; scrollView.scrollTop = taskTop - (scrollViewHeight / 2) + (taskHeight / 2); } } function initScrollFocus() { const scrollView = document.getElementById("scroll-view"); scrollView.removeEventListener("scroll", handleScrollFocus); scrollView.addEventListener("scroll", handleScrollFocus); handleScrollFocus(); } function handleScrollFocus() { const scrollView = document.getElementById("scroll-view"); const taskItems = document.querySelectorAll(".task-item"); const scrollViewRect = scrollView.getBoundingClientRect(); const focusCenter = scrollViewRect.top + scrollViewRect.height / 2; let closestItem = null; let closestDistance = Infinity; taskItems.forEach(item => { const itemRect = item.getBoundingClientRect(); const itemCenter = itemRect.top + itemRect.height / 2; const distance = Math.abs(itemCenter - focusCenter); item.classList.remove("in-focus"); if (distance < closestDistance) { closestDistance = distance; closestItem = item; } }); if (closestItem) { closestItem.classList.add("in-focus"); } } function selectTask(taskId) { selectedTaskId = taskId; const task = [...tasks, ...finishedTasks].find(t => t.id === taskId); if (!task) return; document.getElementById("edit-task-title").value = task.title; document.getElementById("edit-task-desc").value = task.desc || ""; document.getElementById("edit-task-status").value = task.status; document.getElementById("side-panel-error").textContent = ""; document.getElementById("side-panel").classList.add("active"); } function closeSidePanel() { document.getElementById("side-panel").classList.remove("active"); selectedTaskId = null; } async function saveTask() { if (!selectedTaskId) return; const error = document.getElementById("side-panel-error"); error.textContent = ""; const title = document.getElementById("edit-task-title").value.trim(); const desc = document.getElementById("edit-task-desc").value; const status = document.getElementById("edit-task-status").value; if (!title) { error.textContent = "Title is required"; return; } try { await put(`/api/tasks/${selectedTaskId}`, { title, desc }); const currentTask = [...tasks, ...finishedTasks].find(t => t.id === selectedTaskId); if (status !== currentTask?.status) { await patch(`/api/tasks/${selectedTaskId}/status`, { status }); } await loadTasks(); } catch (err) { error.textContent = err.message; } } async function deleteTask() { if (!selectedTaskId) return; if (!confirm("Are you sure you want to delete this task?")) return; try { await del(`/api/tasks/${selectedTaskId}`); closeSidePanel(); await loadTasks(); } catch (error) { console.error("Failed to delete task:", error); } } function openTaskModal() { document.getElementById("task-id").value = ""; document.getElementById("task-title").value = ""; document.getElementById("task-desc").value = ""; document.getElementById("task-error").textContent = ""; document.getElementById("task-modal-title").textContent = "Create Task"; document.getElementById("task-modal").classList.add("active"); } function closeTaskModal() { document.getElementById("task-modal").classList.remove("active"); } async function handleTaskSubmit(event) { event.preventDefault(); const error = document.getElementById("task-error"); error.textContent = ""; const title = document.getElementById("task-title").value.trim(); const desc = document.getElementById("task-desc").value; if (!title) { error.textContent = "Title is required"; return; } const goalId = parseInt(document.getElementById("goal-selector").value); if (!goalId) { error.textContent = "Please select a goal first"; return; } try { await post("/api/tasks", { goal_id: goalId, title, desc }); closeTaskModal(); await loadTasks(); } catch (err) { error.textContent = err.message; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatTime(isoString) { if (!isoString) return ""; const date = new Date(isoString); return date.toLocaleString(); } document.addEventListener("DOMContentLoaded", () => { document.getElementById("goal-selector").addEventListener("change", (e) => { selectedGoalId = parseInt(e.target.value); loadTasks(); }); document.getElementById("create-task-btn").addEventListener("click", openTaskModal); document.getElementById("task-modal-close").addEventListener("click", closeTaskModal); document.getElementById("task-modal-cancel").addEventListener("click", closeTaskModal); document.getElementById("task-form").addEventListener("submit", handleTaskSubmit); document.getElementById("side-panel-close").addEventListener("click", closeSidePanel); document.getElementById("save-task-btn").addEventListener("click", saveTask); document.getElementById("delete-task-btn").addEventListener("click", deleteTask); loadGoals(); });