kin: KIN-127-frontend_dev

This commit is contained in:
Gros Frumos 2026-03-18 21:14:50 +02:00
parent d552b8bd45
commit 484c9fc800
5 changed files with 487 additions and 15 deletions

View file

@ -681,6 +681,71 @@ const manualEscalationTasks = computed(() => {
)
})
// Tree helpers
const childrenMap = computed(() => {
const map = new Map<string, Task[]>()
for (const t of (project.value?.tasks || [])) {
if (t.parent_task_id) {
const arr = map.get(t.parent_task_id) || []
arr.push(t)
map.set(t.parent_task_id, arr)
}
}
return map
})
function taskDepth(task: Task): number {
let depth = 0
let current = task
const visited = new Set<string>()
while (current.parent_task_id && !visited.has(current.id)) {
visited.add(current.id)
const parent = (project.value?.tasks || []).find(t => t.id === current.parent_task_id)
if (!parent) break
current = parent
depth++
}
return depth
}
const expandedTasks = ref(new Set<string>())
function toggleExpand(taskId: string) {
const next = new Set(expandedTasks.value)
if (next.has(taskId)) next.delete(taskId)
else next.add(taskId)
expandedTasks.value = next
}
function hasChildren(taskId: string): boolean {
return (childrenMap.value.get(taskId)?.length || 0) > 0
}
const rootFilteredTasks = computed(() => {
const taskIds = new Set((project.value?.tasks || []).map(t => t.id))
return filteredTasks.value.filter(t => {
if (!t.parent_task_id) return true
return !taskIds.has(t.parent_task_id)
})
})
const flattenedTasks = computed(() => {
const result: Task[] = []
function addWithChildren(task: Task) {
result.push(task)
if (expandedTasks.value.has(task.id)) {
const children = childrenMap.value.get(task.id) || []
for (const child of children) {
addWithChildren(child)
}
}
}
for (const t of rootFilteredTasks.value) {
addWithChildren(t)
}
return result
})
const filteredDecisions = computed(() => {
if (!project.value) return []
let decs = project.value.decisions
@ -796,6 +861,7 @@ const KANBAN_COLUMNS = computed(() => [
{ status: 'pending', label: t('projectView.kanban_pending'), headerClass: 'text-gray-400', bgClass: 'bg-gray-900/20' },
{ status: 'in_progress', label: t('projectView.kanban_in_progress'), headerClass: 'text-blue-400', bgClass: 'bg-blue-950/20' },
{ status: 'review', label: t('projectView.kanban_review'), headerClass: 'text-purple-400', bgClass: 'bg-purple-950/20' },
{ status: 'revising', label: t('projectView.kanban_revising'), headerClass: 'text-orange-400', bgClass: 'bg-orange-950/20' },
{ status: 'blocked', label: t('projectView.kanban_blocked'), headerClass: 'text-red-400', bgClass: 'bg-red-950/20' },
{ status: 'done', label: t('projectView.kanban_done'), headerClass: 'text-green-400', bgClass: 'bg-green-950/20' },
])
@ -1127,13 +1193,20 @@ async function addDecision() {
</div>
</div>
<div v-if="filteredTasks.length === 0" class="text-gray-600 text-sm">{{ t('projectView.no_tasks') }}</div>
<div v-if="flattenedTasks.length === 0" class="text-gray-600 text-sm">{{ t('projectView.no_tasks') }}</div>
<div v-else class="space-y-1">
<router-link v-for="t in filteredTasks" :key="t.id"
<div v-for="t in flattenedTasks" :key="t.id" :style="{ paddingLeft: taskDepth(t) * 24 + 'px' }">
<router-link
:to="{ path: `/task/${t.id}`, query: selectedStatuses.length ? { back_status: selectedStatuses.join(',') } : undefined }"
class="flex flex-col gap-0.5 px-3 py-2 border border-gray-800 rounded text-sm hover:border-gray-600 no-underline block transition-colors">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 min-w-0">
<button v-if="hasChildren(t.id)" @click.prevent="toggleExpand(t.id)"
data-testid="task-toggle-children"
class="text-gray-500 hover:text-gray-300 w-4 shrink-0">
{{ expandedTasks.has(t.id) ? '▼' : '▶' }}
</button>
<span v-else class="w-4 shrink-0"></span>
<span class="text-gray-500 shrink-0 w-24">{{ t.id }}</span>
<Badge :text="t.status" :color="taskStatusColor(t.status)" />
<Badge v-if="t.category" :text="t.category" :color="CATEGORY_COLORS[t.category] || 'gray'" />
@ -1185,6 +1258,7 @@ async function addDecision() {
</div>
<div v-if="t.status === 'blocked' && t.blocked_reason" class="text-xs text-red-400 truncate">{{ t.blocked_reason }}</div>
</router-link>
</div>
</div>
</div>