document.addEventListener("DOMContentLoaded", () => { console.log("OrgMyLife frontend initialized."); // Fetch Dashboard Data fetch("/api/dashboard") .then(res => res.json()) .then(data => { console.log("Dashboard Data:", data); // Populate Digest document.getElementById("digest-content").textContent = data.digest; // Populate Suggestions const suggestionsContainer = document.getElementById("suggestions-container"); if (data.suggestions && data.suggestions.length > 0) { suggestionsContainer.innerHTML = data.suggestions.map(s => { const severityClass = s.severity === 'high' ? 'suggestion-high' : s.severity === 'medium' ? 'suggestion-medium' : 'suggestion-low'; return `
${s.content}
`; }).join(''); } else { suggestionsContainer.innerHTML = '
โœ… All good! No urgent suggestions.
'; } // Populate Priorities const prioritiesList = document.getElementById("priorities-list"); prioritiesList.innerHTML = data.priorities.map(p => `
  • ${p.title} (${p.priority})
  • ` ).join(''); // Populate Calendar const calendarList = document.getElementById("calendar-list"); calendarList.innerHTML = data.events.map(e => `
  • ${e.time}
    ${e.title}
  • ` ).join(''); }) .catch(err => { console.error("Error fetching dashboard data:", err); document.getElementById("digest-content").textContent = "Error loading data."; }); // --- Sync Buttons --- const syncBacklogBtn = document.getElementById("sync-backlog-btn"); if (syncBacklogBtn) { syncBacklogBtn.addEventListener("click", () => { syncBacklogBtn.textContent = "Syncing..."; syncBacklogBtn.disabled = true; fetch("/api/sync/backlog", { method: "POST" }) .then(res => res.json()) .then(data => { alert(data.message); window.location.reload(); }) .catch(err => { console.error("Error syncing backlog:", err); alert("Failed to sync backlog."); syncBacklogBtn.textContent = "Sync Backlog"; syncBacklogBtn.disabled = false; }); }); } function makeSyncHandler(btnId, endpoint, label) { const btn = document.getElementById(btnId); if (!btn) return; btn.addEventListener("click", () => { btn.textContent = "Syncing..."; btn.disabled = true; fetch(endpoint, { method: "POST" }) .then(res => res.json()) .then(data => { alert(data.message); window.location.reload(); }) .catch(err => { console.error(`Error syncing ${label}:`, err); alert(`Failed to sync ${label}.`); btn.textContent = label; btn.disabled = false; }); }); } makeSyncHandler("sync-todo-btn", "/api/sync/nextcloud-todo", "Sync ToDo"); makeSyncHandler("sync-email-btn", "/api/sync/email", "Sync Email"); makeSyncHandler("sync-gmail-btn", "/api/sync/gmail", "Sync Gmail"); // --- Sync All Button --- const syncAllBtn = document.getElementById("sync-all-btn"); if (syncAllBtn) { syncAllBtn.addEventListener("click", async () => { syncAllBtn.textContent = "Syncing..."; syncAllBtn.disabled = true; const endpoints = [ "/api/sync/nextcloud-todo", "/api/sync/email", "/api/sync/gmail", "/api/sync/backlog" ]; try { for (const ep of endpoints) { await fetch(ep, { method: "POST" }); } window.location.reload(); } catch (err) { console.error("Error in Sync All:", err); alert("Sync All failed. Check console."); syncAllBtn.textContent = "โŸณ Sync All"; syncAllBtn.disabled = false; } }); } // --- Navigation Tabs --- const navDashboard = document.getElementById("nav-dashboard"); const navTasks = document.getElementById("nav-tasks"); const navDigest = document.getElementById("nav-digest"); const navRetro = document.getElementById("nav-retro"); const dashboardView = document.getElementById("dashboard"); const allTasksView = document.getElementById("all-tasks-view"); const digestView = document.getElementById("digest-view"); const retroView = document.getElementById("retro-view"); function switchTab(tab) { // Hide all views dashboardView.style.display = "none"; allTasksView.style.display = "none"; digestView.style.display = "none"; retroView.style.display = "none"; if (projectsView) projectsView.style.display = "none"; if (callsView) callsView.style.display = "none"; // Deactivate all nav buttons document.querySelectorAll(".nav-btn").forEach(b => b.classList.remove("active")); if (tab === "tasks") { navTasks.classList.add("active"); allTasksView.style.display = "flex"; loadAllTasks(); } else if (tab === "digest") { navDigest.classList.add("active"); digestView.style.display = "flex"; loadDigest(); } else if (tab === "retro") { navRetro.classList.add("active"); retroView.style.display = "flex"; loadRetro(); } else if (tab === "projects") { navProjects.classList.add("active"); projectsView.style.display = "flex"; loadProjects(); } else if (tab === "calls") { navCalls.classList.add("active"); callsView.style.display = "flex"; loadCalls(); } else { navDashboard.classList.add("active"); dashboardView.style.display = "flex"; } } navDashboard?.addEventListener("click", () => switchTab("dashboard")); navTasks?.addEventListener("click", () => switchTab("tasks")); navDigest?.addEventListener("click", () => switchTab("digest")); navRetro?.addEventListener("click", () => switchTab("retro")); const navProjects = document.getElementById("nav-projects"); const projectsView = document.getElementById("projects-view"); navProjects?.addEventListener("click", () => switchTab("projects")); const navCalls = document.getElementById("nav-calls"); const callsView = document.getElementById("calls-view"); navCalls?.addEventListener("click", () => switchTab("calls")); // --- Daily Digest --- function loadDigest() { fetch("/api/digest") .then(res => res.json()) .then(data => { document.getElementById("digest-greeting").textContent = `${data.greeting}! You have ${data.total_open} open tasks.`; document.getElementById("digest-date").textContent = data.date; const overdueList = document.getElementById("digest-overdue"); if (data.overdue.length > 0) { overdueList.innerHTML = data.overdue.map(t => `
  • ${t.title} ${t.days_overdue}d overdue
  • ` ).join(''); } else { overdueList.innerHTML = '
  • โœ… Nothing overdue!
  • '; } const dueTodayList = document.getElementById("digest-due-today"); if (data.due_today.length > 0) { dueTodayList.innerHTML = data.due_today.map(t => `
  • ${t.title}
  • ` ).join(''); } else { dueTodayList.innerHTML = '
  • No deadlines today.
  • '; } const topList = document.getElementById("digest-top-tasks"); topList.innerHTML = data.top_tasks.map(t => `
  • ${t.title} (${t.priority})
  • ` ).join(''); const eventsList = document.getElementById("digest-events"); if (data.events_today.length > 0) { eventsList.innerHTML = data.events_today.map(e => `
  • ${e.time} ${e.title}
  • ` ).join(''); } else { eventsList.innerHTML = '
  • No events today.
  • '; } // Render pending calls section const callsSection = document.getElementById("digest-calls-section"); const callsList = document.getElementById("digest-calls"); if (data.pending_calls && data.pending_calls.length > 0) { callsSection.style.display = ""; callsList.innerHTML = data.pending_calls.map(c => `
  • ${c.person} โ€” ${c.reason}
  • ` ).join(''); } else { callsSection.style.display = "none"; callsList.innerHTML = ''; } }) .catch(err => console.error("Error loading digest:", err)); } // --- Weekly Retro --- function loadRetro() { fetch("/api/retro") .then(res => res.json()) .then(data => { document.getElementById("retro-period").textContent = data.period; document.getElementById("retro-summary").innerHTML = `
    ${data.completed}
    Completed
    ${data.created}
    Created
    ${data.total_open}
    Open
    ${data.in_review}
    In Review
    `; const healthColor = data.health === 'growing' ? '#d93025' : data.health === 'shrinking' ? '#34a853' : '#ea8600'; document.getElementById("retro-health").innerHTML = `

    ${data.health === 'growing' ? '๐Ÿ“ˆ Backlog growing' : data.health === 'shrinking' ? '๐Ÿ“‰ Backlog shrinking' : 'โžก๏ธ Backlog stable'}

    Net change: ${data.net_change > 0 ? '+' : ''}${data.net_change} tasks

    ${data.events_attended} calendar events this week

    `; document.getElementById("retro-priorities").innerHTML = Object.entries(data.priority_breakdown) .map(([label, count]) => `
    ${label}: ${count}
    `) .join(''); const overdueList = document.getElementById("retro-overdue"); if (data.overdue.length > 0) { overdueList.innerHTML = data.overdue.map(t => `
  • ${t.title} ${t.days_overdue}d
  • ` ).join(''); } else { overdueList.innerHTML = '
  • โœ… Nothing overdue!
  • '; } }) .catch(err => console.error("Error loading retro:", err)); } // --- Calls Tab --- function loadCalls() { const callsList = document.getElementById("calls-list"); callsList.innerHTML = "

    Loading calls...

    "; fetch("/api/calls") .then(res => res.json()) .then(data => { if (!data.calls || data.calls.length === 0) { callsList.innerHTML = "

    No pending calls.

    "; return; } callsList.innerHTML = data.calls.map(c => { const phoneDisplay = c.phone_number ? `${c.phone_number}` : 'No number'; const priorityLabels = { 1: 'Very High', 2: 'High', 3: 'Medium', 4: 'Low' }; return `
    ${c.person}
    ${c.reason}
    ๐Ÿ“ž ${phoneDisplay}
    ${priorityLabels[c.priority] || 'Medium'}
    `; }).join(''); // Edit handlers callsList.querySelectorAll(".btn-call-edit").forEach(btn => { btn.addEventListener("click", () => { const item = btn.closest(".call-item"); item.querySelector(".call-display").style.display = "none"; item.querySelector(".call-edit-form").style.display = "block"; }); }); // Cancel handlers callsList.querySelectorAll(".btn-call-cancel").forEach(btn => { btn.addEventListener("click", () => { const item = btn.closest(".call-item"); item.querySelector(".call-display").style.display = "flex"; item.querySelector(".call-edit-form").style.display = "none"; }); }); // Save handlers callsList.querySelectorAll(".btn-call-save").forEach(btn => { btn.addEventListener("click", () => { const item = btn.closest(".call-item"); const callId = btn.dataset.id; const person = item.querySelector(".edit-person").value.trim(); const reason = item.querySelector(".edit-reason").value.trim(); const phone_number = item.querySelector(".edit-phone").value.trim(); const priority = parseInt(item.querySelector(".edit-priority").value); if (!person) { alert("Person is required."); return; } if (!reason) { alert("Reason is required."); return; } fetch(`/api/calls/${callId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ person, reason, phone_number, priority }) }) .then(res => { if (!res.ok) throw new Error("Update failed"); return res.json(); }) .then(() => loadCalls()) .catch(err => { console.error("Error updating call:", err); alert("Failed to update call."); }); }); }); // Done handlers callsList.querySelectorAll(".btn-call-done").forEach(btn => { btn.addEventListener("click", () => { const callId = btn.dataset.id; const taskId = btn.dataset.taskId; let resolveTask = false; if (taskId) { resolveTask = confirm("Also mark the original task as completed?"); } fetch(`/api/calls/${callId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "done", resolve_task: resolveTask }) }) .then(res => res.json()) .then(() => loadCalls()) .catch(err => console.error("Error completing call:", err)); }); }); // Delete handlers callsList.querySelectorAll(".btn-call-delete").forEach(btn => { btn.addEventListener("click", () => { if (!confirm("Delete this call item?")) return; fetch(`/api/calls/${btn.dataset.id}`, { method: "DELETE" }) .then(res => res.json()) .then(() => loadCalls()) .catch(err => console.error("Error deleting call:", err)); }); }); }) .catch(err => { console.error("Error loading calls:", err); callsList.innerHTML = "

    Error loading calls.

    "; }); } // --- Sort Buttons --- document.querySelectorAll(".sort-btn").forEach(btn => { btn.addEventListener("click", () => { document.querySelectorAll(".sort-btn").forEach(b => b.classList.remove("active")); btn.classList.add("active"); currentSort = btn.dataset.sort; loadAllTasks(); }); }); // --- Filter Dropdowns --- const filterOrigin = document.getElementById("filter-origin"); const filterAgent = document.getElementById("filter-agent"); const filterLabel = document.getElementById("filter-label"); filterOrigin?.addEventListener("change", () => loadAllTasks()); filterAgent?.addEventListener("change", () => loadAllTasks()); filterLabel?.addEventListener("change", () => loadAllTasks()); // --- Edit Form State --- const addTaskForm = document.getElementById("add-task-form"); const formHeading = document.getElementById("form-heading"); const formSubmitBtn = document.getElementById("form-submit-btn"); const formCancelBtn = document.getElementById("form-cancel-btn"); const taskEditId = document.getElementById("task-edit-id"); const taskTitleInput = document.getElementById("task-title"); const taskDescInput = document.getElementById("task-desc"); const taskPrioritySelect = document.getElementById("task-priority"); const taskDueInput = document.getElementById("task-due"); const taskLabelsInput = document.getElementById("task-labels"); function enterEditMode(task) { taskEditId.value = task.id; taskTitleInput.value = task.title; taskDescInput.value = task.description || ""; taskPrioritySelect.value = task.priority_value; taskDueInput.value = task.due_date || ""; if (taskLabelsInput) taskLabelsInput.value = task.labels || ""; formHeading.textContent = "Edit Task"; formSubmitBtn.textContent = "Save"; formCancelBtn.style.display = "inline-block"; taskTitleInput.focus(); } function exitEditMode() { taskEditId.value = ""; addTaskForm.reset(); formHeading.textContent = "Add New Task"; formSubmitBtn.textContent = "Add"; formCancelBtn.style.display = "none"; } formCancelBtn?.addEventListener("click", exitEditMode); // --- All Tasks Logic --- let currentSort = ""; function loadAllTasks() { const params = new URLSearchParams(); if (currentSort) params.set("sort", currentSort); const originVal = document.getElementById("filter-origin")?.value; const agentVal = document.getElementById("filter-agent")?.value; const labelVal = document.getElementById("filter-label")?.value; if (originVal) params.set("filter_origin", originVal); if (agentVal) params.set("filter_agent", agentVal); if (labelVal) params.set("filter_label", labelVal); const url = `/api/tasks${params.toString() ? '?' + params.toString() : ''}`; fetch(url) .then(res => res.json()) .then(data => { const allTasksList = document.getElementById("all-tasks-list"); if (data.tasks.length === 0) { allTasksList.innerHTML = "
  • No open tasks.
  • "; return; } allTasksList.innerHTML = data.tasks.map(t => { // Build origin badge with link let originBadge = ''; if (t.origin) { let originLabel = t.origin; if (t.origin.startsWith('nextcloud_todo')) { originLabel = 'โ˜๏ธ ' + (t.origin.split(':')[1] || 'Nextcloud'); } else if (t.origin === 'dhive_email') { originLabel = 'โœ‰๏ธ dHive Mail'; } else if (t.origin === 'gmail') { originLabel = 'โœ‰๏ธ Gmail'; } else if (t.origin === 'manual') { originLabel = 'โœ๏ธ Manual'; } else { originLabel = '๐Ÿ“‹ ' + t.origin; } if (t.source_url) { originBadge = `${originLabel} โ†—`; } else { originBadge = `${originLabel}`; } } const labelBadges = t.labels ? t.labels.split(',').map(l => l.trim()).filter(Boolean).map(l => `${l}` ).join('') : ''; return `
  • โ ฟ
    #${t.id} ${t.title} ${t.has_conflict ? `โš ๏ธ` : ''} ${t.sender ? `
    From: ${t.sender}
    ` : ''} ${labelBadges ? `
    ${labelBadges}
    ` : ''}
    ${originBadge} ${t.due_date ? `${t.due_date}` : ''} ${t.priority}
    ${t.description && !t.sender ? `
    ${t.description}
    ` : ''} ${t.created_at ? `
    Created: ${t.created_at}
    ` : ''}
  • `}).join(''); // Attach edit handlers allTasksList.querySelectorAll(".btn-edit").forEach(btn => { btn.addEventListener("click", () => { const task = JSON.parse(btn.dataset.task); enterEditMode(task); }); }); // Attach done handlers allTasksList.querySelectorAll(".btn-done").forEach(btn => { btn.addEventListener("click", () => { const taskId = btn.dataset.id; fetch(`/api/tasks/${taskId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: "completed" }) }) .then(res => res.json()) .then(() => loadAllTasks()) .catch(err => console.error("Error completing task:", err)); }); }); // Attach delete handlers (won't do / dismiss) allTasksList.querySelectorAll(".btn-delete").forEach(btn => { btn.addEventListener("click", () => { const taskId = btn.dataset.id; if (!confirm("Won't do this task? (archives email, dismisses from board)")) return; fetch(`/api/tasks/${taskId}`, { method: "DELETE" }) .then(res => res.json()) .then(() => loadAllTasks()) .catch(err => console.error("Error dismissing task:", err)); }); }); // Attach purge handlers (delete from server permanently) allTasksList.querySelectorAll(".btn-purge").forEach(btn => { btn.addEventListener("click", () => { const taskId = btn.dataset.id; if (!confirm("Delete permanently? This removes the email/task from the server.")) return; fetch(`/api/tasks/${taskId}/purge`, { method: "POST" }) .then(res => res.json()) .then(() => loadAllTasks()) .catch(err => console.error("Error purging task:", err)); }); }); // Attach agent-ready toggle handlers allTasksList.querySelectorAll(".btn-agent").forEach(btn => { btn.addEventListener("click", () => { const taskId = btn.dataset.id; const currentlyReady = btn.dataset.ready === "true" || btn.dataset.ready === "True"; fetch(`/api/tasks/${taskId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agent_ready: !currentlyReady }) }) .then(res => res.json()) .then(() => loadAllTasks()) .catch(err => console.error("Error toggling agent_ready:", err)); }); }); // Attach call list handlers allTasksList.querySelectorAll(".btn-call").forEach(btn => { btn.addEventListener("click", () => { const taskId = btn.dataset.id; const originalText = btn.textContent; btn.disabled = true; fetch("/api/calls", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ task_id: parseInt(taskId) }) }) .then(res => { if (!res.ok) throw new Error("Failed to add to call list"); return res.json(); }) .then(() => { btn.textContent = "โœ“"; setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 1000); }) .catch(err => { console.error("Error adding to call list:", err); btn.disabled = false; }); }); }); // Attach send-to-notes handlers allTasksList.querySelectorAll(".btn-note").forEach(btn => { btn.addEventListener("click", () => { const taskId = btn.dataset.id; const originalText = btn.textContent; btn.disabled = true; btn.textContent = "โณ"; fetch(`/api/tasks/${taskId}/to-note`, { method: "POST", headers: { "Content-Type": "application/json" } }) .then(res => res.json()) .then(data => { if (data.status === "success") { btn.textContent = "โœ“"; setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 1500); } else { alert("Failed to send to notes: " + (data.message || "Unknown error")); btn.textContent = originalText; btn.disabled = false; } }) .catch(err => { console.error("Error sending to notes:", err); alert("Error sending to notes"); btn.textContent = originalText; btn.disabled = false; }); }); }); // Attach move-to-top handlers allTasksList.querySelectorAll(".btn-move-top").forEach(btn => { btn.addEventListener("click", () => { fetch(`/api/tasks/${btn.dataset.id}/move?direction=top`, { method: "PUT" }) .then(res => res.json()) .then(() => loadAllTasks()) .catch(err => console.error("Error moving task:", err)); }); }); // Attach move-to-bottom handlers allTasksList.querySelectorAll(".btn-move-bottom").forEach(btn => { btn.addEventListener("click", () => { fetch(`/api/tasks/${btn.dataset.id}/move?direction=bottom`, { method: "PUT" }) .then(res => res.json()) .then(() => loadAllTasks()) .catch(err => console.error("Error moving task:", err)); }); }); // Attach conflict badge handlers allTasksList.querySelectorAll(".conflict-badge").forEach(badge => { badge.addEventListener("click", (e) => { e.stopPropagation(); const taskId = badge.dataset.id; showConflictModal(taskId); }); }); // Attach snooze (remind me later) handlers allTasksList.querySelectorAll(".btn-snooze").forEach(btn => { btn.addEventListener("click", (e) => { e.stopPropagation(); // Remove any existing snooze popup document.querySelectorAll(".snooze-popup").forEach(p => p.remove()); const taskId = btn.dataset.id; const popup = document.createElement("div"); popup.className = "snooze-popup"; popup.style.cssText = "position: absolute; background: white; border: 1px solid #ddd; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); padding: 8px; z-index: 1000; font-size: 0.85rem;"; // Calculate dates const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const nextMonday = new Date(); nextMonday.setDate(nextMonday.getDate() + ((8 - nextMonday.getDay()) % 7 || 7)); const nextMonth = new Date(); nextMonth.setMonth(nextMonth.getMonth() + 1, 1); const fmt = d => d.toISOString().split("T")[0]; popup.innerHTML = `

    `; btn.style.position = "relative"; btn.parentElement.style.position = "relative"; btn.parentElement.appendChild(popup); // Option click handlers popup.querySelectorAll(".snooze-opt").forEach(opt => { opt.addEventListener("mouseenter", () => { opt.style.background = "#e8f0fe"; }); opt.addEventListener("mouseleave", () => { opt.style.background = "none"; }); opt.addEventListener("click", () => { snoozeTask(taskId, opt.dataset.date); popup.remove(); }); }); // Date picker popup.querySelector(".snooze-date-pick").addEventListener("change", (ev) => { if (ev.target.value) { snoozeTask(taskId, ev.target.value); popup.remove(); } }); // Close on outside click setTimeout(() => { document.addEventListener("click", function closePopup(ev) { if (!popup.contains(ev.target) && ev.target !== btn) { popup.remove(); document.removeEventListener("click", closePopup); } }); }, 10); }); }); function snoozeTask(taskId, dateStr) { fetch(`/api/tasks/${taskId}/snooze`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ until: dateStr }) }) .then(res => { if (!res.ok) throw new Error("Snooze failed"); return res.json(); }) .then(() => loadAllTasks()) .catch(err => console.error("Error snoozing task:", err)); } // Attach drag-and-drop handlers initDragAndDrop(allTasksList); }) .catch(err => { console.error("Error loading all tasks:", err); document.getElementById("all-tasks-list").innerHTML = "
  • Error loading tasks.
  • "; }); } // --- Drag and Drop --- let dragSrcEl = null; function initDragAndDrop(list) { const items = list.querySelectorAll(".task-item"); items.forEach(item => { item.addEventListener("dragstart", handleDragStart); item.addEventListener("dragover", handleDragOver); item.addEventListener("dragenter", handleDragEnter); item.addEventListener("dragleave", handleDragLeave); item.addEventListener("drop", handleDrop); item.addEventListener("dragend", handleDragEnd); }); } function handleDragStart(e) { dragSrcEl = this; this.classList.add("dragging"); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", this.dataset.id); } function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; } function handleDragEnter(e) { e.preventDefault(); this.classList.add("drag-over"); } function handleDragLeave() { this.classList.remove("drag-over"); } function handleDrop(e) { e.preventDefault(); e.stopPropagation(); this.classList.remove("drag-over"); if (dragSrcEl !== this) { const list = this.parentNode; const allItems = [...list.querySelectorAll(".task-item")]; const fromIndex = allItems.indexOf(dragSrcEl); const toIndex = allItems.indexOf(this); if (fromIndex < toIndex) { list.insertBefore(dragSrcEl, this.nextSibling); } else { list.insertBefore(dragSrcEl, this); } // Send new order to backend const reordered = [...list.querySelectorAll(".task-item")].map((item, idx) => ({ id: parseInt(item.dataset.id), position: idx })); fetch("/api/tasks/reorder", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ order: reordered }) }) .catch(err => console.error("Error saving reorder:", err)); } } function handleDragEnd() { this.classList.remove("dragging"); document.querySelectorAll(".task-item").forEach(item => { item.classList.remove("drag-over"); }); } // --- Form Submit (Create or Update) --- addTaskForm?.addEventListener("submit", (e) => { e.preventDefault(); const editId = taskEditId.value; const title = taskTitleInput.value; const desc = taskDescInput.value; const priority = parseInt(taskPrioritySelect.value, 10); const dueDate = taskDueInput.value || null; const labels = taskLabelsInput ? taskLabelsInput.value : ""; if (editId) { // Update existing task fetch(`/api/tasks/${editId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, description: desc, priority, due_date: dueDate || "", labels }) }) .then(res => res.json()) .then(data => { if (data.status === "success") { exitEditMode(); loadAllTasks(); loadLabelFilter(); } else { alert("Failed to update task."); } }) .catch(err => { console.error("Error updating task:", err); alert("Error updating task."); }); } else { // Create new task fetch("/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, description: desc, priority, due_date: dueDate, labels: labels || null }) }) .then(res => res.json()) .then(data => { if (data.status === "success") { addTaskForm.reset(); loadAllTasks(); loadLabelFilter(); } else { alert("Failed to add task."); } }) .catch(err => { console.error("Error adding task:", err); alert("Error adding task."); }); } }); // --- Label Filter Population --- function loadLabelFilter() { fetch("/api/tasks/labels") .then(res => res.json()) .then(data => { const select = document.getElementById("filter-label"); if (!select) return; const currentVal = select.value; select.innerHTML = ''; (data.labels || []).forEach(label => { const opt = document.createElement("option"); opt.value = label; opt.textContent = label; select.appendChild(opt); }); // Restore previous selection if still valid if (currentVal && data.labels && data.labels.includes(currentVal)) { select.value = currentVal; } }) .catch(err => console.error("Error loading labels:", err)); } // Load labels on init loadLabelFilter(); // --- Projects Kanban Board --- function loadProjects() { fetch("/api/projects") .then(res => res.json()) .then(data => { const statuses = ["backlog", "ready", "in_progress", "blocked", "review", "done"]; statuses.forEach(status => { const container = document.getElementById(`kanban-${status}`); const tasks = data.tasks.filter(t => t.status === status); container.innerHTML = tasks.map(t => `
    ${t.title}
    ${t.repo ? `${t.repo}` : ''} ${t.description ? `

    ${t.description}

    ` : ''}
    ${status !== 'backlog' ? `` : ''} ${status !== 'done' ? `` : ''}
    `).join('') || '

    No tasks

    '; }); // Move handlers document.querySelectorAll(".kanban-move").forEach(btn => { btn.addEventListener("click", () => { const taskId = btn.dataset.id; const dir = btn.dataset.dir; const card = btn.closest(".kanban-card"); const column = card.closest(".kanban-column"); const currentStatus = column.dataset.status; const order = ["backlog", "ready", "in_progress", "blocked", "review", "done"]; const idx = order.indexOf(currentStatus); const newStatus = dir === "right" ? order[idx + 1] : order[idx - 1]; if (!newStatus) return; fetch(`/api/projects/${taskId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: newStatus }) }) .then(res => { if (!res.ok) { console.error("Failed to move project task:", res.status, res.statusText); return res.json().then(d => { throw new Error(d.detail || "Move failed"); }); } return res.json(); }) .then(() => loadProjects()) .catch(err => { console.error("Error moving project task:", err); alert("Failed to move task: " + err.message); }); }); }); // Delete handlers document.querySelectorAll(".btn-delete-project").forEach(btn => { btn.addEventListener("click", () => { if (!confirm("Delete this project task?")) return; fetch(`/api/projects/${btn.dataset.id}`, { method: "DELETE" }) .then(res => res.json()) .then(() => loadProjects()) .catch(err => console.error("Error deleting project task:", err)); }); }); }) .catch(err => console.error("Error loading projects:", err)); } // Add project task form const addProjectForm = document.getElementById("add-project-task-form"); addProjectForm?.addEventListener("submit", (e) => { e.preventDefault(); const title = document.getElementById("pt-title").value; const repo = document.getElementById("pt-repo").value; fetch("/api/projects/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, repo }) }) .then(res => res.json()) .then(data => { if (data.status === "success") { addProjectForm.reset(); loadProjects(); } }) .catch(err => console.error("Error adding project task:", err)); }); // --- Add Call Form --- const addCallForm = document.getElementById("add-call-form"); addCallForm?.addEventListener("submit", (e) => { e.preventDefault(); const person = document.getElementById("call-person").value; const reason = document.getElementById("call-reason").value; const phone_number = document.getElementById("call-phone").value || null; const priority = parseInt(document.getElementById("call-priority").value, 10); fetch("/api/calls", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ person, reason, phone_number, priority }) }) .then(res => { if (!res.ok) throw new Error("Failed to add call"); return res.json(); }) .then(() => { addCallForm.reset(); loadCalls(); }) .catch(err => { console.error("Error adding call:", err); alert("Failed to add call."); }); }); // --- Conflict Resolution Modal --- function showConflictModal(taskId) { // Remove any existing modal document.querySelectorAll(".conflict-modal-overlay").forEach(m => m.remove()); fetch("/api/tasks/conflicts") .then(res => res.json()) .then(data => { const conflict = data.conflicts.find(c => c.id == taskId); if (!conflict) { alert("No conflict data found for this task."); return; } const remote = conflict.conflict_data || {}; const overlay = document.createElement("div"); overlay.className = "conflict-modal-overlay"; overlay.innerHTML = `

    โš ๏ธ Sync Conflict

    This task was modified both locally and remotely. Choose which version to keep.

    ๐Ÿ“ฑ Local Version

    Title: ${conflict.title}
    Status: ${conflict.status}
    Description: ${conflict.description || 'empty'}

    โ˜๏ธ Remote Version

    Title: ${remote.title || ''}
    Status: ${remote.status || ''}
    Description: ${remote.description || 'empty'}
    `; document.body.appendChild(overlay); // Close on overlay click overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); }); // Cancel overlay.querySelector(".conflict-btn-cancel").addEventListener("click", () => overlay.remove()); // Keep Local overlay.querySelector(".conflict-btn-local").addEventListener("click", () => { resolveConflict(taskId, "local", overlay); }); // Use Remote overlay.querySelector(".conflict-btn-remote").addEventListener("click", () => { resolveConflict(taskId, "remote", overlay); }); }) .catch(err => console.error("Error loading conflict data:", err)); } function resolveConflict(taskId, resolution, overlay) { fetch(`/api/tasks/${taskId}/resolve-conflict`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ resolution }) }) .then(res => { if (!res.ok) throw new Error("Failed to resolve conflict"); return res.json(); }) .then(() => { overlay.remove(); loadAllTasks(); checkConflicts(); }) .catch(err => { console.error("Error resolving conflict:", err); alert("Failed to resolve conflict."); }); } // --- Dashboard Conflict Notification --- function checkConflicts() { fetch("/api/tasks/conflicts") .then(res => res.json()) .then(data => { let notice = document.getElementById("conflict-notice"); if (data.total > 0) { if (!notice) { notice = document.createElement("div"); notice.id = "conflict-notice"; notice.className = "conflict-notice"; const digestSection = document.getElementById("digest-section"); if (digestSection) { digestSection.parentNode.insertBefore(notice, digestSection); } } notice.innerHTML = `โš ๏ธ ${data.total} task${data.total > 1 ? 's have' : ' has'} sync conflicts โ€” resolve now`; notice.querySelector("#conflict-notice-link")?.addEventListener("click", (e) => { e.preventDefault(); switchTab("tasks"); }); } else if (notice) { notice.remove(); } }) .catch(() => {}); } // Check for conflicts on load checkConflicts(); });