summaryrefslogtreecommitdiff
path: root/admin-panel/src/client/components/board.ts
blob: b611133a6db16fdb2b801f98e5ad5a086e0e3bb0 (plain)
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);
      }
    });
  });
}