feat: complete task board workflow gates
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
import type { AgentNodeData } from '../types/agentNode'
|
||||
|
||||
export const TASK_AGENT_OPTIONS = [
|
||||
{ id: '', label: 'Nicht zugewiesen' },
|
||||
{ id: 'bao', label: '👤 Bao' },
|
||||
{ id: 'iris', label: '🤖 Iris' },
|
||||
{ id: 'product-owner', label: '📋 Product Owner' },
|
||||
{ id: 'programmer', label: '🛠 Programmer' },
|
||||
{ id: 'programmer-fast', label: '⚡ Programmer Fast' },
|
||||
{ id: 'reviewer', label: '🔎 Reviewer' },
|
||||
{ id: 'architekt', label: '🏛 Architekt' },
|
||||
{ id: 'researcher', label: '🔬 Researcher' },
|
||||
{ id: 'executor', label: '🚀 Executor' },
|
||||
] as const
|
||||
|
||||
export const TASK_AGENT_LABELS: Record<string, string> = Object.fromEntries(
|
||||
TASK_AGENT_OPTIONS
|
||||
.filter(option => option.id)
|
||||
.map(option => [option.id, option.label])
|
||||
) as Record<string, string>
|
||||
|
||||
export const EXTRA_AGENT_POOL: AgentNodeData[] = [
|
||||
{
|
||||
id: 'qa',
|
||||
|
||||
@@ -17,7 +17,9 @@ interface CatalogEntry {
|
||||
|
||||
const AGENT_CATALOG: Record<string, CatalogEntry> = {
|
||||
iris: { elapsed: '--', think: null, next: 'Standby' },
|
||||
'product-owner': { elapsed: '--', think: null, next: 'Standby' },
|
||||
programmer: { elapsed: '--', think: null, next: 'Standby' },
|
||||
'programmer-fast': { elapsed: '--', think: null, next: 'Standby' },
|
||||
developer: { elapsed: '--', think: null, next: 'Standby' },
|
||||
architekt: { elapsed: '--', think: null, next: 'Standby' },
|
||||
reviewer: { elapsed: '--', think: null, next: 'Standby' },
|
||||
@@ -33,7 +35,9 @@ function resolveStatus(isActive: boolean, currentTask: string | null): AgentNode
|
||||
|
||||
function resolveAvatar(id: string, name: string): string {
|
||||
if (id === 'iris') return 'IR'
|
||||
if (id === 'product-owner') return 'PO'
|
||||
if (id === 'programmer' || id === 'developer') return '</>'
|
||||
if (id === 'programmer-fast') return 'PF'
|
||||
return name.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ export interface DashboardTaskDto {
|
||||
expectedFrom?: string | null
|
||||
lastActivityMessage?: string | null
|
||||
lastActivityAt?: string | null
|
||||
childTasks?: DashboardTaskDto[] | null
|
||||
childTaskCount?: number
|
||||
openChildTaskCount?: number
|
||||
hasVisibleDelegation?: boolean
|
||||
}
|
||||
|
||||
export interface BoardGroup {
|
||||
@@ -319,6 +323,8 @@ export const useTaskStore = defineStore('tasks', {
|
||||
assignedTo?: string
|
||||
expectedFrom?: string
|
||||
parentTaskId?: string | null
|
||||
startsInProgress?: boolean
|
||||
initialState?: string | null
|
||||
}) {
|
||||
try {
|
||||
const res = await apiFetch('/api/dashboard/tasks/agent', {
|
||||
@@ -331,6 +337,8 @@ export const useTaskStore = defineStore('tasks', {
|
||||
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}`)
|
||||
|
||||
@@ -17,7 +17,8 @@ import { Plus, X, CalendarDays, Clock3, ExternalLink, Link2, ListChecks, Save, A
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useTaskStore } from '../stores/tasks'
|
||||
import { useLiveSyncStore } from '../stores/liveSync'
|
||||
import { useLiveSyncStore } from '../stores/live-sync'
|
||||
import { TASK_AGENT_LABELS, TASK_AGENT_OPTIONS } from '../constants/agentPool'
|
||||
|
||||
type BoardTask = ReturnType<typeof flattenBoard>[number]
|
||||
|
||||
@@ -222,16 +223,7 @@ const liveModeClass = computed(() => `live-pill-${liveSyncStore.connectionHealth
|
||||
|
||||
function expectedFromLabel(expected: string | null | undefined): string {
|
||||
if (!expected) return ''
|
||||
const map: Record<string, string> = {
|
||||
'bao': '👤 Bao',
|
||||
'iris': '🤖 Iris',
|
||||
'programmer': '🛠 Programmer',
|
||||
'reviewer': '🔎 Reviewer',
|
||||
'architekt': '🏛 Architekt',
|
||||
'researcher': '🔬 Researcher',
|
||||
'executor': '⚡ Executor',
|
||||
}
|
||||
return map[expected.toLowerCase()] ?? expected
|
||||
return TASK_AGENT_LABELS[expected.toLowerCase()] ?? expected
|
||||
}
|
||||
|
||||
function hoursSince(dateStr: string): number {
|
||||
@@ -827,13 +819,13 @@ onUnmounted(() => {
|
||||
<div class="field">
|
||||
<label for="task-assignee">Zugewiesen an</label>
|
||||
<select id="task-assignee" v-model="formAssignedTo" class="field-input field-select">
|
||||
<option value="bao">👤 Bao</option>
|
||||
<option value="iris">🤖 Iris</option>
|
||||
<option value="programmer">🛠 Programmer</option>
|
||||
<option value="reviewer">🔎 Reviewer</option>
|
||||
<option value="architekt">🏛 Architekt</option>
|
||||
<option value="researcher">🔬 Researcher</option>
|
||||
<option value="executor">⚡ Executor</option>
|
||||
<option
|
||||
v-for="option in TASK_AGENT_OPTIONS.filter(entry => entry.id)"
|
||||
:key="option.id"
|
||||
:value="option.id"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -951,14 +943,9 @@ onUnmounted(() => {
|
||||
<label class="sidebar-field">
|
||||
<span>Zuständig</span>
|
||||
<select v-model="detailForm.assignedTo" class="field-input field-select slim">
|
||||
<option value="">Nicht zugewiesen</option>
|
||||
<option value="bao">👤 Bao</option>
|
||||
<option value="iris">🤖 Iris</option>
|
||||
<option value="programmer">🛠 Programmer</option>
|
||||
<option value="reviewer">🔎 Reviewer</option>
|
||||
<option value="architekt">🏛 Architekt</option>
|
||||
<option value="researcher">🔬 Researcher</option>
|
||||
<option value="executor">⚡ Executor</option>
|
||||
<option v-for="option in TASK_AGENT_OPTIONS" :key="option.id || 'unassigned'" :value="option.id">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="sidebar-field">
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@lucide/vue'
|
||||
import { apiFetch } from '../services/api'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { TASK_AGENT_LABELS, TASK_AGENT_OPTIONS } from '../constants/agentPool'
|
||||
|
||||
/* ── Types ──────────────────────────────────── */
|
||||
interface TaskDto {
|
||||
@@ -166,7 +167,9 @@ function childStatusSummary(taskId: string): string {
|
||||
}
|
||||
|
||||
function progressHint(taskLike: Pick<TaskDto, 'id' | 'lastActivityMessage' | 'expectedFrom'>): string {
|
||||
return taskLike.lastActivityMessage?.trim() || childStatusSummary(taskLike.id) || (taskLike.expectedFrom ? `Wartet auf ${taskLike.expectedFrom}` : 'Noch kein relevanter Progress-Status')
|
||||
return taskLike.lastActivityMessage?.trim()
|
||||
|| childStatusSummary(taskLike.id)
|
||||
|| (taskLike.expectedFrom ? `Wartet auf ${TASK_AGENT_LABELS[taskLike.expectedFrom.toLowerCase()] ?? taskLike.expectedFrom}` : 'Noch kein relevanter Progress-Status')
|
||||
}
|
||||
|
||||
function delegationSummary(taskLike: TaskDto): string | null {
|
||||
@@ -451,7 +454,11 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Low">Low</option>
|
||||
</select>
|
||||
<input v-model="subtaskAssign" class="galaxy-input narrow" placeholder="Zuständig (bao, iris, researcher…)" />
|
||||
<select v-model="subtaskAssign" class="galaxy-input galaxy-select narrow">
|
||||
<option v-for="option in TASK_AGENT_OPTIONS" :key="option.id || 'unassigned'" :value="option.id">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button class="btn-primary btn-sm" @click="createSubtask" :disabled="creatingSubtask">
|
||||
{{ creatingSubtask ? 'Erstelle…' : 'Anlegen' }}
|
||||
</button>
|
||||
@@ -565,14 +572,9 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<label class="sidebar-field">
|
||||
<span>Zuständig</span>
|
||||
<select v-model="form.assignedTo" class="galaxy-input galaxy-select">
|
||||
<option value="">Nicht zugewiesen</option>
|
||||
<option value="bao">👤 Bao</option>
|
||||
<option value="iris">🤖 Iris</option>
|
||||
<option value="programmer">🛠 Programmer</option>
|
||||
<option value="reviewer">🔎 Reviewer</option>
|
||||
<option value="architekt">🏛 Architekt</option>
|
||||
<option value="researcher">🔬 Researcher</option>
|
||||
<option value="executor">⚡ Executor</option>
|
||||
<option v-for="option in TASK_AGENT_OPTIONS" :key="option.id || 'unassigned'" :value="option.id">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="sidebar-field">
|
||||
@@ -590,6 +592,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<div><dt>Erstellt</dt><dd>{{ formatDate(task.createdAt) }}</dd></div>
|
||||
<div><dt>Geändert</dt><dd>{{ formatDate(task.updatedAt, true) }}</dd></div>
|
||||
<div v-if="task.isAgentTask"><dt>Letzter Status</dt><dd>{{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</dd></div>
|
||||
<div v-if="task.expectedFrom"><dt>Erwartet von</dt><dd>{{ TASK_AGENT_LABELS[task.expectedFrom.toLowerCase()] ?? task.expectedFrom }}</dd></div>
|
||||
<div v-if="task.parentTaskId"><dt>Task-Typ</dt><dd>Sichtbare Child-Task</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user