diff --git a/src/aggregator/web/index.html b/src/aggregator/web/index.html index ca1688e..8339853 100644 --- a/src/aggregator/web/index.html +++ b/src/aggregator/web/index.html @@ -71,7 +71,7 @@ .board { padding: 2rem; display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 1.5rem; min-height: calc(100vh - 180px); } @@ -157,6 +157,65 @@ display: none; } .task-card.expanded .task-description { display: block; } + .task-card { position: relative; } + .task-actions { + position: absolute; + top: -8px; + right: -8px; + display: none; + gap: 4px; + z-index: 10; + } + .task-card:hover .task-actions { display: flex; } + .task-action-btn { + width: 24px; + height: 24px; + border-radius: 50%; + border: none; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: bold; + box-shadow: 0 2px 4px rgba(0,0,0,0.3); + } + .task-action-btn.archive { background: #3b82f6; font-size: 12px; } + .task-action-btn.delete { background: #ef4444; } + .task-action-btn:hover { transform: scale(1.1); } + /* Inline editing styles */ + .edit-input { + background: #1e293b; + border: 1px solid #3b82f6; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + color: #e2e8f0; + font-size: inherit; + width: 100%; + outline: none; + } + .inline-select { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.25rem; + padding: 0.125rem 0.25rem; + color: #e2e8f0; + font-size: 0.625rem; + cursor: pointer; + outline: none; + } + .task-title.editable:hover { + background: rgba(59, 130, 246, 0.1); + border-radius: 0.25rem; + cursor: text; + } + .task-controls { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.25rem; + } /* Modal styles */ .modal-overlay { @@ -281,6 +340,13 @@
+
+
+

Won't Do

+ 0 +
+
+
@@ -317,6 +383,7 @@ +
@@ -413,19 +480,138 @@ `${projects.length} projects | ${tasks.length} tasks | Updated ${new Date().toLocaleTimeString()}`; } + async function archiveTask(projectPath, taskId) { + try { + const response = await fetch('/api/tasks/archive', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectPath, taskId }) + }); + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to archive task'); + } + showToast('Task archived', 'success'); + } catch (err) { + console.error('Archive error:', err); + showToast(err.message || 'Failed to archive task', 'error'); + } + } + + async function deleteTask(projectPath, taskId, title) { + if (!confirm(`Are you sure you want to delete "${title}"? This cannot be undone.`)) return; + try { + const response = await fetch('/api/tasks/delete', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectPath, taskId }) + }); + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete task'); + } + showToast('Task deleted', 'success'); + } catch (err) { + console.error('Delete error:', err); + showToast(err.message || 'Failed to delete task', 'error'); + } + } + + async function updateTask(projectPath, taskId, updates) { + try { + const response = await fetch('/api/tasks/update', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectPath, taskId, ...updates }) + }); + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to update task'); + } + showToast('Task updated', 'success'); + } catch (err) { + console.error('Update error:', err); + showToast(err.message || 'Failed to update task', 'error'); + } + } + + function startInlineEdit(card, task, field) { + const element = card.querySelector(`[data-field="${field}"]`); + const currentValue = field === 'title' ? task.title : (task.description || ''); + const isTextarea = field === 'description'; + + if (isTextarea) { + element.innerHTML = ` + +
+ Ctrl+Enter to save + + +
+ `; + } else { + element.innerHTML = ` +
+ + + +
+ `; + } + + const input = element.querySelector(isTextarea ? 'textarea' : 'input'); + input.focus(); + if (!isTextarea) input.select(); + + const saveEdit = async () => { + const newValue = input.value; + if (newValue !== currentValue) { + await updateTask(task.projectPath, task.id, { [field]: newValue }); + } + // Re-render will happen via WebSocket update + }; + + const cancelEdit = () => { + renderTasks(); // Re-render to restore original + }; + + element.querySelector('[data-save]').addEventListener('click', (e) => { + e.stopPropagation(); + saveEdit(); + }); + element.querySelector('[data-cancel]').addEventListener('click', (e) => { + e.stopPropagation(); + cancelEdit(); + }); + + input.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.stopPropagation(); + cancelEdit(); + } else if (e.key === 'Enter' && (!isTextarea || e.ctrlKey)) { + e.stopPropagation(); + e.preventDefault(); + saveEdit(); + } + }); + + input.addEventListener('click', (e) => e.stopPropagation()); + } + function renderTasks() { const filter = document.getElementById('filter').value.toLowerCase(); const statusMap = { 'to do': 'todo', 'in progress': 'progress', - 'done': 'done' + 'done': 'done', + "won't do": 'wontdo' }; - ['todo', 'progress', 'done'].forEach(id => { + ['todo', 'progress', 'done', 'wontdo'].forEach(id => { document.getElementById(`tasks-${id}`).innerHTML = ''; }); - let counts = { todo: 0, progress: 0, done: 0 }; + let counts = { todo: 0, progress: 0, done: 0, wontdo: 0 }; tasks.forEach(task => { if (selectedProject && task.projectName !== selectedProject) return; @@ -457,13 +643,35 @@ priorityClass = `priority-${task.priority.toLowerCase()}`; } + const priorityBgColors = { high: 'rgba(239,68,68,0.2)', medium: 'rgba(245,158,11,0.2)', low: 'rgba(34,197,94,0.2)' }; + const priorityTextColors = { high: '#ef4444', medium: '#f59e0b', low: '#22c55e' }; + const currentPriority = task.priority ? task.priority.toLowerCase() : ''; + card.innerHTML = ` +
+ + +
-
-
${escapeHtml(task.title)}
-
${task.id} | ${task.projectName}
+
+
${escapeHtml(task.title)}
+
+ ${task.id} | ${task.projectName} + +
- ${task.priority ? `${task.priority}` : ''} +
${task.labels && task.labels.length > 0 ? `
@@ -473,17 +681,54 @@ ${task.assignee && task.assignee.length > 0 ? `
${task.assignee.join(', ')}
` : ''} - ${task.description ? ` -
${escapeHtml(task.description.slice(0, 500))}${task.description.length > 500 ? '...' : ''}
- ` : ''} +
${task.description ? escapeHtml(task.description.slice(0, 500)) + (task.description.length > 500 ? '...' : '') : 'Click to add description...'}
`; + // Add event listeners for action buttons + card.querySelector('[data-action="archive"]').addEventListener('click', (e) => { + e.stopPropagation(); + archiveTask(task.projectPath, task.id); + }); + card.querySelector('[data-action="delete"]').addEventListener('click', (e) => { + e.stopPropagation(); + deleteTask(task.projectPath, task.id, task.title); + }); + + // Status dropdown + card.querySelector('[data-field="status"]').addEventListener('change', (e) => { + e.stopPropagation(); + updateTask(task.projectPath, task.id, { status: e.target.value }); + }); + card.querySelector('[data-field="status"]').addEventListener('click', (e) => e.stopPropagation()); + + // Priority dropdown + card.querySelector('[data-field="priority"]').addEventListener('change', (e) => { + e.stopPropagation(); + updateTask(task.projectPath, task.id, { priority: e.target.value || undefined }); + }); + card.querySelector('[data-field="priority"]').addEventListener('click', (e) => e.stopPropagation()); + + // Inline title editing + card.querySelector('[data-field="title"]').addEventListener('click', (e) => { + e.stopPropagation(); + startInlineEdit(card, task, 'title'); + }); + + // Description editing (only when expanded) + card.querySelector('[data-field="description"]').addEventListener('click', (e) => { + e.stopPropagation(); + if (card.classList.contains('expanded')) { + startInlineEdit(card, task, 'description'); + } + }); + document.getElementById(`tasks-${statusKey}`).appendChild(card); }); document.getElementById('count-todo').textContent = counts.todo; document.getElementById('count-progress').textContent = counts.progress; document.getElementById('count-done').textContent = counts.done; + document.getElementById('count-wontdo').textContent = counts.wontdo; } // Drag and Drop handlers