summaryrefslogtreecommitdiff
path: root/admin-panel/src/client/components/board.ts
diff options
context:
space:
mode:
Diffstat (limited to 'admin-panel/src/client/components/board.ts')
-rw-r--r--admin-panel/src/client/components/board.ts123
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);
+ }
+ });
+ });
+}