273 lines
8.6 KiB
TypeScript
273 lines
8.6 KiB
TypeScript
/**
|
||
* 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<typeof setInterval> | null,
|
||
boardRefreshInterval: null as ReturnType<typeof setInterval> | 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<string, string> = {
|
||
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
|
||
}
|
||
},
|
||
},
|
||
})
|