agentic-os/dashboard/pages/kanban.js

291 lines
12 KiB
JavaScript

let kanbanData = null;
async function renderKanban() {
const content = document.getElementById('pageContent');
content.innerHTML = `
<div class="page-header">
<div class="page-header-left">
<div class="page-title">Kanban Board</div>
<div class="page-subtitle">Visual task management — track, prioritize, and organize work</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" onclick="showAddKanbanTask()">+ Add Task</button>
<button class="btn btn-ghost" onclick="renderKanban()">🔄 Refresh</button>
</div>
</div>
<div class="kanban-toolbar">
<input class="form-input" id="kanbanFilterInput" placeholder="Filter tasks..." oninput="filterKanbanTasks()" style="flex:1;max-width:280px">
<select class="form-select" id="kanbanFilterPriority" onchange="filterKanbanTasks()" style="width:120px">
<option value="all">All Priorities</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select class="form-select" id="kanbanFilterCategory" onchange="filterKanbanTasks()" style="width:140px">
<option value="all">All Categories</option>
<option value="development">Development</option>
<option value="devops">DevOps</option>
<option value="study">Study</option>
<option value="content">Content</option>
<option value="general">General</option>
</select>
</div>
<div class="kanban-board" id="kanbanBoard">
<div class="skeleton" style="height:400px"></div>
</div>
`;
await loadKanbanData();
}
async function loadKanbanData() {
try {
const data = await api.getKanbanBoard();
kanbanData = data;
renderKanbanBoard();
} catch (err) {
const board = document.getElementById('kanbanBoard');
if (board) board.innerHTML = `<div class="empty-state"><div class="empty-state-icon">📋</div><div class="empty-state-title">Failed to load kanban</div><div class="empty-state-desc">${escapeHtml(err.message)}</div></div>`;
}
}
function renderKanbanBoard() {
const board = document.getElementById('kanbanBoard');
if (!board || !kanbanData) return;
const columnsObj = kanbanData.columns || {};
const columns = Object.keys(columnsObj);
const allTasks = kanbanData.tasks || Object.values(columnsObj).flat();
const filterText = (document.getElementById('kanbanFilterInput')?.value || '').toLowerCase();
const filterPriority = document.getElementById('kanbanFilterPriority')?.value || 'all';
const filterCategory = document.getElementById('kanbanFilterCategory')?.value || 'all';
const columnLabels = { triage: 'Triage', todo: 'To Do', ready: 'Ready', in_progress: 'In Progress', blocked: 'Blocked', done: 'Done', backlog: 'Backlog', review: 'Review' };
const columnIcons = { triage: '🔍', todo: '📝', ready: '✅', in_progress: '🔄', blocked: '🚫', done: '🎉', backlog: '📋', review: '🔍' };
board.innerHTML = columns.map(col => {
let colTasks = (columnsObj[col] || []).filter(t => {
if (filterText) return (t.title || '').toLowerCase().includes(filterText) || (t.body || t.description || '').toLowerCase().includes(filterText);
return true;
}).filter(t => filterPriority === 'all' || (t.priority || 'medium') === filterPriority)
.filter(t => filterCategory === 'all' || (t.category || 'general') === filterCategory);
return `
<div class="kanban-column" data-column="${col}">
<div class="kanban-column-header">
<div class="kanban-column-title">
<span>${columnIcons[col] || '📌'}</span>
${columnLabels[col] || col}
<span class="kanban-count">${colTasks.length}</span>
</div>
</div>
<div class="kanban-column-body" ondragover="event.preventDefault()" ondrop="onKanbanDrop(event, '${col}')">
${colTasks.length === 0 ? `<div class="kanban-empty">No tasks</div>` :
colTasks.map(t => `
<div class="kanban-card" draggable="true" ondragstart="onKanbanDrag(event, '${t.id}')" onclick="showKanbanDetail('${t.id}')">
<div class="kanban-card-header">
<span class="kanban-priority priority-${t.priority || 'medium'}">${t.priority || 'medium'}</span>
</div>
<div class="kanban-card-title">${escapeHtml(t.title)}</div>
${t.body ? `<div class="kanban-card-desc">${escapeHtml(t.body.substring(0, 80))}${t.body.length > 80 ? '...' : ''}</div>` : ''}
<div class="kanban-card-meta">
${t.assignee ? `<span>👤 ${escapeHtml(t.assignee)}</span>` : ''}
</div>
${t.status === 'blocked' ? `<div class="kanban-blocked-badge">🚫 Blocked</div>` : ''}
</div>
`).join('')}
</div>
</div>
`;
}).join('');
}
function showAddKanbanTask() {
const modal = document.getElementById('modalContainer');
modal.innerHTML = `
<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
<div class="modal" style="max-width:520px">
<div class="modal-header">
<div class="modal-title">Add Kanban Task</div>
<button class="modal-close" onclick="closeModal()">✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Title *</label>
<input class="form-input" id="kanbanTitle" placeholder="e.g., Implement login page">
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea class="form-textarea" id="kanbanDescription" rows="2" placeholder="Brief description"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-select" id="kanbanStatus">
<option value="triage">Triage</option>
<option value="todo">To Do</option>
<option value="ready">Ready</option>
<option value="in_progress">In Progress</option>
<option value="blocked">Blocked</option>
<option value="done">Done</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Priority</label>
<select class="form-select" id="kanbanPriority">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Category</label>
<select class="form-select" id="kanbanCategory">
<option value="general">General</option>
<option value="development">Development</option>
<option value="devops">DevOps</option>
<option value="study">Study</option>
<option value="content">Content</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Assigned To</label>
<input class="form-input" id="kanbanAssigned" placeholder="e.g., opencode" style="text-transform:lowercase">
</div>
<div class="form-group">
<label class="form-label">Target Date</label>
<input class="form-input" id="kanbanDate" type="date">
</div>
</div>
<div class="form-group">
<label class="form-label">Tags (comma separated)</label>
<input class="form-input" id="kanbanTags" placeholder="e.g., frontend, api">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="createKanbanTask()">Add Task</button>
</div>
</div>
</div>
`;
}
async function createKanbanTask() {
const title = document.getElementById('kanbanTitle').value.trim();
if (!title) { showToast('Title is required', 'error'); return; }
const tagsStr = document.getElementById('kanbanTags').value.trim();
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(Boolean) : [];
try {
await api.createKanbanTask({
title,
body: document.getElementById('kanbanDescription').value.trim(),
status: document.getElementById('kanbanStatus').value,
priority: document.getElementById('kanbanPriority').value,
assignee: document.getElementById('kanbanAssigned').value.trim(),
});
showToast('Task added to kanban board!', 'success');
closeModal();
renderKanban();
} catch (err) {
showToast('Failed to create task: ' + err.message, 'error');
}
}
let kanbanDraggedId = null;
function onKanbanDrag(e, id) {
kanbanDraggedId = id;
e.dataTransfer.effectAllowed = 'move';
}
async function onKanbanDrop(e, status) {
e.preventDefault();
if (!kanbanDraggedId) return;
try {
await api.updateKanbanTask(kanbanDraggedId, { status });
kanbanDraggedId = null;
renderKanban();
} catch (err) {
showToast('Failed to move task: ' + err.message, 'error');
}
}
function showKanbanDetail(id) {
const task = kanbanData?.tasks?.find(t => t.id === id);
if (!task) return;
const modal = document.getElementById('modalContainer');
modal.innerHTML = `
<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
<div class="modal" style="max-width:560px">
<div class="modal-header">
<div class="modal-title">${escapeHtml(task.title)}</div>
<button class="modal-close" onclick="closeModal()">✕</button>
</div>
<div class="modal-body">
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px">
<span class="badge badge-${task.status === 'done' ? 'success' : task.status === 'in_progress' ? 'info' : 'warning'}">${task.status}</span>
<span class="kanban-priority priority-${task.priority || 'medium'}">${task.priority}</span>
${task.status === 'blocked' ? `<span class="badge badge-danger">🚫 Blocked</span>` : ''}
</div>
${task.body ? `<div style="margin-bottom:12px;color:var(--text-secondary);font-size:13px">${escapeHtml(task.body)}</div>` : ''}
<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:12px;font-size:12px">
${task.assignee ? `<span>👤 <strong>${escapeHtml(task.assignee)}</strong></span>` : ''}
<span>📅 <strong>${task.created || 'N/A'}</strong></span>
${task.completed_at ? `<span>✅ <strong>${task.completed_at}</strong></span>` : ''}
</div>
<div style="display:flex;gap:4px;flex-wrap:wrap">
${task.status !== 'done' ? `<button class="btn btn-sm btn-primary" onclick="completeKanbanTask('${task.id}')">✅ Mark Done</button>` : ''}
${task.status !== 'blocked' ? `<button class="btn btn-sm btn-ghost" onclick="blockKanbanTask('${task.id}')">🚫 Block</button>` : `<button class="btn btn-sm btn-ghost" onclick="unblockKanbanTask('${task.id}')">🔓 Unblock</button>`}
<button class="btn btn-sm btn-ghost" onclick="deleteKanbanTask('${task.id}')" style="color:var(--red)">🗑 Delete</button>
</div>
</div>
</div>
</div>
`;
}
async function completeKanbanTask(id) {
try {
await api.completeKanbanTask(id);
closeModal();
renderKanban();
showToast('Task completed! ✅', 'success');
} catch (err) {
showToast('Failed: ' + err.message, 'error');
}
}
async function blockKanbanTask(id) {
try {
await api.blockKanbanTask(id);
closeModal();
renderKanban();
showToast('Task blocked 🚫', 'warning');
} catch (err) {
showToast('Failed: ' + err.message, 'error');
}
}
async function unblockKanbanTask(id) {
try {
await api.unblockKanbanTask(id);
closeModal();
renderKanban();
showToast('Task unblocked 🔓', 'success');
} catch (err) {
showToast('Failed: ' + err.message, 'error');
}
}
async function deleteKanbanTask(id) {
if (!confirm('Delete this task?')) return;
try {
await api.deleteKanbanTask(id);
closeModal();
renderKanban();
showToast('Task deleted', 'info');
} catch (err) {
showToast('Failed: ' + err.message, 'error');
}
}
function filterKanbanTasks() {
renderKanbanBoard();
}