Files
Orchestrator/shared/OrgMyLife/frontend/js/app.js
T
ankn a5f8fb49ab Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
2026-06-30 20:39:52 +02:00

1190 lines
58 KiB
JavaScript

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 `<div class="suggestion-box ${severityClass}">${s.content}</div>`;
}).join('');
} else {
suggestionsContainer.innerHTML = '<div class="suggestion-box suggestion-low">✅ All good! No urgent suggestions.</div>';
}
// Populate Priorities
const prioritiesList = document.getElementById("priorities-list");
prioritiesList.innerHTML = data.priorities.map(p =>
`<li>
<span class="${p.priority === 'Very High' || p.priority === 'High' ? 'priority-high' : ''}">${p.title}</span>
<small>(${p.priority})</small>
</li>`
).join('');
// Populate Calendar
const calendarList = document.getElementById("calendar-list");
calendarList.innerHTML = data.events.map(e =>
`<li>
<strong>${e.time}</strong><br>
${e.title}
</li>`
).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 =>
`<li><strong>${t.title}</strong> <small class="due-badge overdue">${t.days_overdue}d overdue</small></li>`
).join('');
} else {
overdueList.innerHTML = '<li style="color: #34a853;">✅ Nothing overdue!</li>';
}
const dueTodayList = document.getElementById("digest-due-today");
if (data.due_today.length > 0) {
dueTodayList.innerHTML = data.due_today.map(t =>
`<li>${t.title}</li>`
).join('');
} else {
dueTodayList.innerHTML = '<li style="color: #666;">No deadlines today.</li>';
}
const topList = document.getElementById("digest-top-tasks");
topList.innerHTML = data.top_tasks.map(t =>
`<li><span class="${t.priority === 'Very High' || t.priority === 'High' ? 'priority-high' : ''}">${t.title}</span> <small>(${t.priority})</small></li>`
).join('');
const eventsList = document.getElementById("digest-events");
if (data.events_today.length > 0) {
eventsList.innerHTML = data.events_today.map(e =>
`<li><strong>${e.time}</strong> ${e.title}</li>`
).join('');
} else {
eventsList.innerHTML = '<li style="color: #666;">No events today.</li>';
}
// 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 =>
`<li><strong>${c.person}</strong> <span style="color: #666;">— ${c.reason}</span></li>`
).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 = `
<div class="retro-stats">
<div class="retro-stat"><span class="retro-num">${data.completed}</span><br>Completed</div>
<div class="retro-stat"><span class="retro-num">${data.created}</span><br>Created</div>
<div class="retro-stat"><span class="retro-num">${data.total_open}</span><br>Open</div>
<div class="retro-stat"><span class="retro-num">${data.in_review}</span><br>In Review</div>
</div>
`;
const healthColor = data.health === 'growing' ? '#d93025' : data.health === 'shrinking' ? '#34a853' : '#ea8600';
document.getElementById("retro-health").innerHTML = `
<p style="font-size: 1.2rem; color: ${healthColor}; font-weight: 600;">
${data.health === 'growing' ? '📈 Backlog growing' : data.health === 'shrinking' ? '📉 Backlog shrinking' : '➡️ Backlog stable'}
</p>
<p>Net change: ${data.net_change > 0 ? '+' : ''}${data.net_change} tasks</p>
<p>${data.events_attended} calendar events this week</p>
`;
document.getElementById("retro-priorities").innerHTML = Object.entries(data.priority_breakdown)
.map(([label, count]) => `<div style="margin: 4px 0;"><strong>${label}:</strong> ${count}</div>`)
.join('');
const overdueList = document.getElementById("retro-overdue");
if (data.overdue.length > 0) {
overdueList.innerHTML = data.overdue.map(t =>
`<li><strong>${t.title}</strong> <small class="due-badge overdue">${t.days_overdue}d</small></li>`
).join('');
} else {
overdueList.innerHTML = '<li style="color: #34a853;">✅ Nothing overdue!</li>';
}
})
.catch(err => console.error("Error loading retro:", err));
}
// --- Calls Tab ---
function loadCalls() {
const callsList = document.getElementById("calls-list");
callsList.innerHTML = "<p>Loading calls...</p>";
fetch("/api/calls")
.then(res => res.json())
.then(data => {
if (!data.calls || data.calls.length === 0) {
callsList.innerHTML = "<p style=\"color: #666;\">No pending calls.</p>";
return;
}
callsList.innerHTML = data.calls.map(c => {
const phoneDisplay = c.phone_number
? `<a href="tel:${c.phone_number}">${c.phone_number}</a>`
: '<span style="color: #999;">No number</span>';
const priorityLabels = { 1: 'Very High', 2: 'High', 3: 'Medium', 4: 'Low' };
return `
<div class="call-item" data-id="${c.id}" style="padding: 12px 0; border-bottom: 1px solid #eee;">
<div class="call-display" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${c.person}</strong>
<div style="font-size: 0.85rem; color: #666;">${c.reason}</div>
<div style="font-size: 0.85rem; margin-top: 4px;">📞 ${phoneDisplay}</div>
</div>
<div style="display: flex; gap: 6px; align-items: center;">
<small class="priority-badge priority-${c.priority}">${priorityLabels[c.priority] || 'Medium'}</small>
<button class="btn-call-edit btn" data-id="${c.id}" title="Edit" style="padding: 4px 10px; cursor: pointer;">✎ Edit</button>
<button class="btn-call-done btn" data-id="${c.id}" data-task-id="${c.original_task_id || ''}" title="Mark done" style="padding: 4px 10px; cursor: pointer;">✓ Done</button>
<button class="btn-call-delete btn" data-id="${c.id}" title="Delete" style="padding: 4px 10px; cursor: pointer;">✕</button>
</div>
</div>
<div class="call-edit-form" style="display: none; margin-top: 8px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<input type="text" class="edit-person" value="${c.person.replace(/"/g, '&quot;')}" placeholder="Person" style="padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px;">
<input type="text" class="edit-reason" value="${c.reason.replace(/"/g, '&quot;')}" placeholder="Reason" style="padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px;">
<input type="text" class="edit-phone" value="${(c.phone_number || '').replace(/"/g, '&quot;')}" placeholder="Phone number" style="padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px;">
<select class="edit-priority" style="padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px;">
<option value="1" ${c.priority === 1 ? 'selected' : ''}>Very High</option>
<option value="2" ${c.priority === 2 ? 'selected' : ''}>High</option>
<option value="3" ${c.priority === 3 ? 'selected' : ''}>Medium</option>
<option value="4" ${c.priority === 4 ? 'selected' : ''}>Low</option>
</select>
</div>
<div style="display: flex; gap: 6px;">
<button class="btn-call-save btn" data-id="${c.id}" style="padding: 4px 12px; cursor: pointer; background: #1a73e8; color: #fff; border: none; border-radius: 4px;">Save</button>
<button class="btn-call-cancel btn" data-id="${c.id}" style="padding: 4px 12px; cursor: pointer; border: 1px solid #ddd; border-radius: 4px;">Cancel</button>
</div>
</div>
</div>
`;
}).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 = "<p style=\"color: #d93025;\">Error loading calls.</p>";
});
}
// --- 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 = "<li>No open tasks.</li>";
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 = `<a href="${t.source_url}" target="_blank" class="origin-badge origin-link" title="Open in source app">${originLabel} ↗</a>`;
} else {
originBadge = `<small class="origin-badge">${originLabel}</small>`;
}
}
const labelBadges = t.labels ? t.labels.split(',').map(l => l.trim()).filter(Boolean).map(l =>
`<span class="label-badge">${l}</span>`
).join('') : '';
return `
<li class="task-item ${t.agent_ready ? 'agent-ready' : ''} ${t.has_conflict ? 'has-conflict' : ''}" draggable="true" data-id="${t.id}" data-priority-value="${t.priority_value}">
<span class="drag-handle" title="Drag to reorder">⠿</span>
<span class="move-arrows">
<button class="btn-move-top" data-id="${t.id}" title="Move to top">⤒</button>
<button class="btn-move-bottom" data-id="${t.id}" title="Move to bottom">⤓</button>
</span>
<div class="task-content">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<div>
<strong class="${t.priority === 'Very High' || t.priority === 'High' ? 'priority-high' : ''}"><span style="color: #999; font-weight: normal; font-size: 0.75rem;">#${t.id}</span> ${t.title}</strong>
${t.has_conflict ? `<span class="conflict-badge" data-id="${t.id}" title="Sync conflict — click to resolve">⚠️</span>` : ''}
${t.sender ? `<div class="task-sender">From: ${t.sender}</div>` : ''}
${labelBadges ? `<div style="margin-top: 3px;">${labelBadges}</div>` : ''}
</div>
<div class="task-actions">
${originBadge}
${t.due_date ? `<small class="due-badge ${new Date(t.due_date) < new Date().toISOString().split('T')[0] ? 'overdue' : ''}">${t.due_date}</small>` : ''}
<small class="priority-badge priority-${t.priority_value}">${t.priority}</small>
<button class="btn-agent ${t.agent_ready ? 'active' : ''}" data-id="${t.id}" data-ready="${t.agent_ready}" title="${t.agent_ready ? 'Remove from AI queue' : 'Send to AI Orchestrator'}">🤖</button>
<button class="btn-snooze" data-id="${t.id}" title="Remind me later">⏰</button>
<button class="btn-call" data-id="${t.id}" title="Add to Call List">📞</button>
<button class="btn-note" data-id="${t.id}" title="Send to Notes">📝</button>
<button class="btn-edit" data-task='${JSON.stringify(t).replace(/'/g, "&#39;")}' title="Edit">✎</button>
<button class="btn-done" data-id="${t.id}" title="Mark done">✓</button>
<button class="btn-delete" data-id="${t.id}" title="Won't do (archive/dismiss)">✕</button>
<button class="btn-purge" data-id="${t.id}" title="Delete permanently (remove from server)">🗑</button>
</div>
</div>
${t.description && !t.sender ? `<div style="font-size: 0.85rem; color: #666; margin-top: 4px; white-space: pre-wrap;">${t.description}</div>` : ''}
${t.created_at ? `<div style="font-size: 0.75rem; color: #999; margin-top: 3px;">Created: ${t.created_at}</div>` : ''}
</div>
</li>
`}).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 = `
<div style="display: flex; flex-direction: column; gap: 4px; min-width: 150px;">
<button class="snooze-opt" data-date="${fmt(tomorrow)}" style="text-align: left; padding: 6px 8px; border: none; background: none; cursor: pointer; border-radius: 4px;">Tomorrow</button>
<button class="snooze-opt" data-date="${fmt(nextMonday)}" style="text-align: left; padding: 6px 8px; border: none; background: none; cursor: pointer; border-radius: 4px;">Next Monday</button>
<button class="snooze-opt" data-date="${fmt(nextMonth)}" style="text-align: left; padding: 6px 8px; border: none; background: none; cursor: pointer; border-radius: 4px;">Next Month</button>
<hr style="margin: 4px 0; border: none; border-top: 1px solid #eee;">
<label style="padding: 4px 8px; font-size: 0.8rem; color: #666;">Pick a date:</label>
<input type="date" class="snooze-date-pick" style="padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
`;
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 = "<li>Error loading tasks.</li>";
});
}
// --- 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 = '<option value="">All labels</option>';
(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 => `
<div class="kanban-card" data-id="${t.id}">
<div class="kanban-card-header">
<strong>${t.title}</strong>
<button class="btn-delete-project" data-id="${t.id}" title="Delete">✕</button>
</div>
${t.repo ? `<small class="kanban-repo">${t.repo}</small>` : ''}
${t.description ? `<p class="kanban-desc">${t.description}</p>` : ''}
<div class="kanban-actions">
${status !== 'backlog' ? `<button class="kanban-move" data-id="${t.id}" data-dir="left" title="Move left">←</button>` : ''}
${status !== 'done' ? `<button class="kanban-move" data-id="${t.id}" data-dir="right" title="Move right">→</button>` : ''}
</div>
</div>
`).join('') || '<p class="kanban-empty">No tasks</p>';
});
// 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 = `
<div class="conflict-modal">
<h3>⚠️ Sync Conflict</h3>
<p style="color: #666; margin-bottom: 1rem;">This task was modified both locally and remotely. Choose which version to keep.</p>
<div class="conflict-comparison">
<div class="conflict-version conflict-local">
<h4>📱 Local Version</h4>
<div class="conflict-field"><strong>Title:</strong> ${conflict.title}</div>
<div class="conflict-field"><strong>Status:</strong> ${conflict.status}</div>
<div class="conflict-field"><strong>Description:</strong> ${conflict.description || '<em>empty</em>'}</div>
</div>
<div class="conflict-version conflict-remote">
<h4>☁️ Remote Version</h4>
<div class="conflict-field"><strong>Title:</strong> ${remote.title || ''}</div>
<div class="conflict-field"><strong>Status:</strong> ${remote.status || ''}</div>
<div class="conflict-field"><strong>Description:</strong> ${remote.description || '<em>empty</em>'}</div>
</div>
</div>
<div class="conflict-actions">
<button class="btn conflict-btn-local">Keep Local</button>
<button class="btn conflict-btn-remote">Use Remote</button>
<button class="btn conflict-btn-cancel">Cancel</button>
</div>
</div>
`;
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 — <a href="#" id="conflict-notice-link">resolve now</a>`;
notice.querySelector("#conflict-notice-link")?.addEventListener("click", (e) => {
e.preventDefault();
switchTab("tasks");
});
} else if (notice) {
notice.remove();
}
})
.catch(() => {});
}
// Check for conflicts on load
checkConflicts();
});