1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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);
}
});
});
}
|