/** * 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, blocked, done) * 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 ──────────────────────────── */ export interface DashboardTaskDto { id: string title: string detail: string | null source: string state: string priority: string assignedTo: string | null parentTaskId?: string | null dueDate?: string | null createdAt: string updatedAt: string isAgentTask?: boolean expectedFrom?: string | null lastActivityMessage?: string | null lastActivityAt?: string | null childTasks?: DashboardTaskDto[] | null childTaskCount?: number openChildTaskCount?: number hasVisibleDelegation?: boolean } export interface BoardGroup { offen: DashboardTaskDto[] inProgress: DashboardTaskDto[] review: DashboardTaskDto[] blocked: DashboardTaskDto[] done: DashboardTaskDto[] } export interface AgentWorkflowOverview { waitingForBao: DashboardTaskDto[] waitingForIris: DashboardTaskDto[] waitingForOthers: DashboardTaskDto[] staleTasks: DashboardTaskDto[] staleThreshold: string } /* ── 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[], blocked: [] as DashboardTaskDto[], done: [] as DashboardTaskDto[], } as BoardGroup, boardLoading: false, boardError: null as string | null, // Agent Workflow Overview (for Iris) agentOverview: null as AgentWorkflowOverview | null, agentOverviewLoading: false, agentOverviewError: null as string | null, }), getters: { taskList: (state) => state.tasks, // Iris helpers waitingForIrisTasks: (state) => state.agentOverview?.waitingForIris ?? [], waitingForBaoTasks: (state) => state.agentOverview?.waitingForBao ?? [], waitingForOthersTasks: (state) => state.agentOverview?.waitingForOthers ?? [], staleTasksList: (state) => state.agentOverview?.staleTasks ?? [], agentTaskCount: (state) => { if (!state.agentOverview) return 0 return state.agentOverview.waitingForBao.length + state.agentOverview.waitingForIris.length + state.agentOverview.waitingForOthers.length + state.agentOverview.staleTasks.length }, }, 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', blocked: 'Blocked', done: 'Done', } // 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 | null; priority?: string; assignedTo?: string | null; dueDate?: string | null }) { 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) throw err } }, async fetchTaskChildren(id: string) { try { const res = await apiFetch(`/api/dashboard/tasks/${id}/children`) if (!res.ok) throw new Error(`HTTP ${res.status}`) return await res.json() as DashboardTaskDto[] } catch (err) { console.warn('[TaskStore] fetchTaskChildren failed', err) throw err } }, async fetchTaskActivity(id: string) { try { const res = await apiFetch(`/api/dashboard/tasks/${id}/activity`) if (!res.ok) throw new Error(`HTTP ${res.status}`) return await res.json() as Array<{ id?: string; message?: string; type?: string; createdAt?: string; timestamp?: string }> } catch (err) { console.warn('[TaskStore] fetchTaskActivity failed', err) throw err } }, /* ── API: Fetch agent workflow overview ──────── */ async fetchAgentOverview(staleHours = 2) { this.agentOverviewLoading = true try { const res = await apiFetch(`/api/dashboard/tasks/agent-overview?staleHours=${staleHours}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data: AgentWorkflowOverview = await res.json() this.agentOverview = data this.agentOverviewError = null } catch (err) { console.warn('[TaskStore] fetchAgentOverview failed', err) this.agentOverviewError = 'Agent overview could not be loaded' } finally { this.agentOverviewLoading = false } }, /* ── API: Create agent task ───────────────────── */ async createAgentTask(data: { title: string detail?: string | null source?: string priority?: string assignedTo?: string expectedFrom?: string parentTaskId?: string | null startsInProgress?: boolean initialState?: string | null }) { try { const res = await apiFetch('/api/dashboard/tasks/agent', { method: 'POST', body: JSON.stringify({ title: data.title, detail: data.detail ?? null, source: data.source ?? 'iris', priority: data.priority ?? 'Medium', assignedTo: data.assignedTo ?? null, expectedFrom: data.expectedFrom ?? null, parentTaskId: data.parentTaskId ?? null, startsInProgress: data.startsInProgress ?? true, initialState: data.initialState ?? null, }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) await this.fetchBoard() await this.fetchAgentOverview() return await res.json() as DashboardTaskDto } catch (err) { console.warn('[TaskStore] createAgentTask failed', err) throw 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(force = false) { if (this.boardRefreshInterval && !force) return if (this.boardRefreshInterval && force) { clearInterval(this.boardRefreshInterval) this.boardRefreshInterval = null } this.fetchBoard() this.boardRefreshInterval = setInterval(() => { this.fetchBoard() }, 30000) }, stopBoardPolling() { if (this.boardRefreshInterval) { clearInterval(this.boardRefreshInterval) this.boardRefreshInterval = null } }, }, })