Files
nexus/frontend/src/stores/tasks.ts
T
devops 5e7d074593
CI - Build & Test / Backend (.NET) (push) Successful in 32s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 19s
CI - Build & Test / Security Check (push) Successful in 3s
feat: Linear-style Task Board mit Drag&Drop
2026-06-18 21:34:07 +02:00

273 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
}
},
},
})