diff options
Diffstat (limited to 'admin-panel/src/client/components/board.ts')
| -rw-r--r-- | admin-panel/src/client/components/board.ts | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/admin-panel/src/client/components/board.ts b/admin-panel/src/client/components/board.ts new file mode 100644 index 0000000..b611133 --- /dev/null +++ b/admin-panel/src/client/components/board.ts @@ -0,0 +1,123 @@ +import { api, type Task } from '../lib/api.js'; +import { renderTaskCard } from './task-card.js'; +import { showTaskModal } from './task-modal.js'; + +const COLUMNS = [ + { id: 'todo', label: 'Req' }, + { id: 'plan', label: 'Plan' }, + { id: 'plan_review', label: 'Plan Review' }, + { id: 'impl', label: 'Impl' }, + { id: 'impl_review', label: 'Impl Review' }, + { id: 'test', label: 'Test' }, + { id: 'done', label: 'Done' }, +]; + +export function renderBoard(container: HTMLElement, projectName: string, tasks: Task[]): void { + const tasksByStatus = new Map<string, Task[]>(); + for (const col of COLUMNS) { + tasksByStatus.set(col.id, []); + } + for (const task of tasks) { + const list = tasksByStatus.get(task.status); + if (list) list.push(task); + else tasksByStatus.get('todo')!.push(task); // fallback + } + + container.innerHTML = `<div class="board-container">${COLUMNS.map(col => { + const colTasks = tasksByStatus.get(col.id)!; + return ` + <div class="board-column" data-status="${col.id}"> + <div class="column-header"> + <h3>${col.label}</h3> + <span class="column-count">${colTasks.length}</span> + </div> + <div class="column-body" data-status="${col.id}"> + ${colTasks.map(task => renderTaskCard(task)).join('')} + </div> + </div> + `; + }).join('')}</div>`; + + // Setup drag-drop + setupDragDrop(container, projectName); + + // Setup card click + container.querySelectorAll('.task-card').forEach(card => { + card.addEventListener('click', (e) => { + // Don't open modal if dragging + if ((card as HTMLElement).classList.contains('dragging')) return; + const taskId = parseInt((card as HTMLElement).dataset.taskId!); + const task = tasks.find(t => t.id === taskId); + if (task) showTaskModal(projectName, task); + }); + }); +} + +function setupDragDrop(container: HTMLElement, projectName: string): void { + const cards = container.querySelectorAll('.task-card'); + const columnBodies = container.querySelectorAll('.column-body'); + + let draggedCard: HTMLElement | null = null; + + cards.forEach(card => { + const el = card as HTMLElement; + el.draggable = true; + + el.addEventListener('dragstart', (e) => { + draggedCard = el; + el.classList.add('dragging'); + (e as DragEvent).dataTransfer!.effectAllowed = 'move'; + (e as DragEvent).dataTransfer!.setData('text/plain', el.dataset.taskId!); + }); + + el.addEventListener('dragend', () => { + el.classList.remove('dragging'); + draggedCard = null; + columnBodies.forEach(cb => cb.classList.remove('drag-over')); + }); + }); + + columnBodies.forEach(body => { + body.addEventListener('dragover', (e) => { + e.preventDefault(); + (e as DragEvent).dataTransfer!.dropEffect = 'move'; + body.classList.add('drag-over'); + }); + + body.addEventListener('dragleave', () => { + body.classList.remove('drag-over'); + }); + + body.addEventListener('drop', async (e) => { + e.preventDefault(); + body.classList.remove('drag-over'); + + if (!draggedCard) return; + + const taskId = parseInt(draggedCard.dataset.taskId!); + const newStatus = (body as HTMLElement).dataset.status!; + + // Calculate rank based on drop position + const existingCards = Array.from(body.querySelectorAll('.task-card:not(.dragging)')); + let rank: number; + + if (existingCards.length === 0) { + rank = 1; + } else { + // Drop at end by default + const lastCard = existingCards[existingCards.length - 1] as HTMLElement; + const lastRank = parseFloat(lastCard.dataset.rank || '0'); + rank = lastRank + 1; + } + + // Optimistically move card + body.appendChild(draggedCard); + + try { + await api.reorderTask(projectName, taskId, rank, newStatus); + } catch (err) { + console.error('Reorder failed:', err); + } + }); + }); +} |
