/** * Task Store – V2 Dashboard + Task Board * * Fetches tasks from /api/dashboard/tasks and /api/dashboard/tasks/board * and maps them into TaskItem[] format for the TaskStrip component. * * Board state: grouped by column (offen, inProgress, review, done, blocked) * Auto-refresh: every 30 seconds. */ import { defineStore } from 'pinia' import { apiFetch } from '../services/api' import type { TaskItem } from '../components/dashboard/v2/types' /* ── API Response Shapes ──────────────────────────── */ interface DashboardTaskDto { id: string title: string detail: string | null source: string state: string priority: string assignedTo: string | null createdAt: string updatedAt: string } export interface BoardGroup { offen: DashboardTaskDto[] inProgress: DashboardTaskDto[] review: DashboardTaskDto[] done: DashboardTaskDto[] blocked: DashboardTaskDto[] } /* ── State Mapping ────────────────────────────────── */ function mapPriority(priority: string): TaskItem['priority'] { const p = priority.toLowerCase() if (p === 'high' || p === 'critical' || p === 'urgent') return 'high' if (p === 'low' || p === 'minor') return 'low' return 'medium' } function mapState(state: string): TaskItem['status'] { const s = state.toLowerCase() if (s === 'in progress' || s === 'active' || s === 'working') return 'active' if (s === 'blocked' || s === 'block') return 'blocked' return 'pending' } function mapProgress(state: string): number { const s = state.toLowerCase() if (s === 'in progress' || s === 'active' || s === 'working') return 50 if (s === 'done') return 100 if (s === 'blocked') return 30 return 0 } function mapTask(t: DashboardTaskDto): TaskItem { return { id: t.id, title: t.title, agent: t.assignedTo ?? '—', priority: mapPriority(t.priority), status: mapState(t.state), progress: mapProgress(t.state), detail: t.detail, source: t.source, } } export const useTaskStore = defineStore('tasks', { state: () => ({ tasks: [] as TaskItem[], loading: false, error: null as string | null, refreshInterval: null as ReturnType | null, boardRefreshInterval: null as ReturnType | null, // Board state board: { offen: [] as DashboardTaskDto[], inProgress: [] as DashboardTaskDto[], review: [] as DashboardTaskDto[], done: [] as DashboardTaskDto[], blocked: [] as DashboardTaskDto[], } as BoardGroup, boardLoading: false, boardError: null as string | null, }), getters: { taskList: (state) => state.tasks, }, actions: { /* ── API: Fetch tasks (for TaskStrip) ─────────── */ async fetchTasks() { this.loading = true try { const res = await apiFetch('/api/dashboard/tasks') if (!res.ok) throw new Error(`HTTP ${res.status}`) const data: DashboardTaskDto[] = await res.json() this.tasks = data.map(mapTask) this.error = null } catch (err) { console.warn('[TaskStore] fetchTasks failed', err) this.error = 'Tasks could not be loaded' } finally { this.loading = false } }, /* ── API: Fetch board (for TaskBoardView) ─────── */ async fetchBoard() { this.boardLoading = true try { const res = await apiFetch('/api/dashboard/tasks/board') if (!res.ok) throw new Error(`HTTP ${res.status}`) const data: BoardGroup = await res.json() this.board = data this.boardError = null } catch (err) { console.warn('[TaskStore] fetchBoard failed', err) this.boardError = 'Board could not be loaded' } finally { this.boardLoading = false } }, /* ── API: Move task (Drag & Drop) ─────────────── */ async moveTask(id: string, newState: string) { // Map board group key to canonical state string for the API payload const canonicalMap: Record = { offen: 'Backlog', inProgress: 'In progress', review: 'Review', done: 'Done', blocked: 'Blocked', } // Save previous state for rollback const prevBoard = JSON.parse(JSON.stringify(this.board)) as BoardGroup // Optimistic: find the task in current board and move it const findAndRemove = (arr: DashboardTaskDto[]): DashboardTaskDto | null => { const idx = arr.findIndex(t => t.id === id) if (idx === -1) return null return arr.splice(idx, 1)[0] } const task = findAndRemove(this.board.offen) ?? findAndRemove(this.board.inProgress) ?? findAndRemove(this.board.review) ?? findAndRemove(this.board.blocked) ?? findAndRemove(this.board.done) if (task) { const canonicalState = canonicalMap[newState] ?? newState task.state = canonicalState const targetKey = newState as keyof BoardGroup if (this.board[targetKey]) { this.board[targetKey].push(task) } } // Actually call API with the board group key (backend handles mapping) try { const res = await apiFetch(`/api/dashboard/tasks/${id}/move`, { method: 'PATCH', body: JSON.stringify({ state: newState }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) } catch (err) { console.warn('[TaskStore] moveTask failed, rolling back', err) this.board = prevBoard } }, /* ── API: Create task ─────────────────────────── */ async createTask(data: { title: string; detail?: string | null; priority?: string; assignedTo?: string }) { try { const res = await apiFetch('/api/dashboard/tasks', { method: 'POST', body: JSON.stringify({ title: data.title, detail: data.detail ?? null, priority: data.priority ?? 'Medium', assignedTo: data.assignedTo ?? 'bao', source: 'bao', }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) // Refresh board + task list await this.fetchBoard() await this.fetchTasks() } catch (err) { console.warn('[TaskStore] createTask failed', err) throw err } }, /* ── API: Add task (for TaskStrip) ────────────── */ async addTask(title: string, detail?: string, priority?: string, assignedTo?: string) { try { const res = await apiFetch('/api/dashboard/tasks', { method: 'POST', body: JSON.stringify({ title, detail: detail ?? null, priority: priority ?? null, assignedTo: assignedTo ?? null, source: 'bao', }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) // Refresh task list await this.fetchTasks() } catch (err) { console.warn('[TaskStore] addTask failed', err) } }, /* ── API: Update task ─────────────────────────── */ async updateTask(id: string, updates: { title?: string; detail?: string; priority?: string; assignedTo?: string }) { try { const res = await apiFetch(`/api/dashboard/tasks/${id}`, { method: 'PUT', body: JSON.stringify(updates), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) await this.fetchTasks() await this.fetchBoard() } catch (err) { console.warn('[TaskStore] updateTask failed', err) } }, /* ── Polling ──────────────────────────────────── */ startPolling() { if (this.refreshInterval) return this.fetchTasks() this.refreshInterval = setInterval(() => { this.fetchTasks() }, 30000) }, stopPolling() { if (this.refreshInterval) { clearInterval(this.refreshInterval) this.refreshInterval = null } }, startBoardPolling() { if (this.boardRefreshInterval) return this.fetchBoard() this.boardRefreshInterval = setInterval(() => { this.fetchBoard() }, 30000) }, stopBoardPolling() { if (this.boardRefreshInterval) { clearInterval(this.boardRefreshInterval) this.boardRefreshInterval = null } }, }, })