Some checks failed
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Failing after 1m57s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
parent_task_id mit Validierung und Roll-up; aufklappbarer Task-Baum unter Arbeitspaketen im Plan-Modus (max. 3 UI-Ebenen). Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
4.9 KiB
JavaScript
195 lines
4.9 KiB
JavaScript
import { useEffect, useMemo, useState } from 'react'
|
|
import {
|
|
createActionTask,
|
|
deleteTask,
|
|
listActionTasks,
|
|
updateTask,
|
|
} from '../api/tasks.js'
|
|
import { buildTasksByParent } from '../utils/taskTree.js'
|
|
import { Modal } from './Modal.jsx'
|
|
import { TaskForm, TaskTreeNodes } from './TaskForm.jsx'
|
|
|
|
export function ActionTaskPanel({
|
|
actionId,
|
|
roadmapItems,
|
|
canManage,
|
|
expanded,
|
|
onToggle,
|
|
openTaskCount,
|
|
}) {
|
|
const [tasks, setTasks] = useState([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
const [busy, setBusy] = useState(false)
|
|
const [modal, setModal] = useState(null)
|
|
const [reloadKey, setReloadKey] = useState(0)
|
|
|
|
useEffect(() => {
|
|
if (!expanded || !actionId) return undefined
|
|
let cancelled = false
|
|
setLoading(true)
|
|
setError(null)
|
|
listActionTasks(actionId)
|
|
.then((data) => {
|
|
if (!cancelled) setTasks(data)
|
|
})
|
|
.catch((err) => {
|
|
if (!cancelled) setError(err.message)
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false)
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [expanded, actionId, reloadKey])
|
|
|
|
const tasksByParent = useMemo(() => buildTasksByParent(tasks), [tasks])
|
|
|
|
async function reload() {
|
|
setReloadKey((value) => value + 1)
|
|
}
|
|
|
|
async function handleCreate(payload) {
|
|
setBusy(true)
|
|
setError(null)
|
|
try {
|
|
await createActionTask(actionId, payload)
|
|
setModal(null)
|
|
await reload()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
async function handleUpdate(taskId, payload) {
|
|
setBusy(true)
|
|
setError(null)
|
|
try {
|
|
await updateTask(taskId, payload)
|
|
setModal(null)
|
|
await reload()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
async function handleStatus(taskId, status) {
|
|
setBusy(true)
|
|
setError(null)
|
|
try {
|
|
await updateTask(taskId, { status })
|
|
await reload()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
async function handleDelete(taskId) {
|
|
setBusy(true)
|
|
setError(null)
|
|
try {
|
|
await deleteTask(taskId)
|
|
await reload()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
const taskLabel = openTaskCount != null ? ` (${openTaskCount} offen)` : ''
|
|
|
|
return (
|
|
<div className="action-task-panel">
|
|
<button
|
|
type="button"
|
|
className="action-task-panel__toggle"
|
|
onClick={onToggle}
|
|
aria-expanded={expanded}
|
|
>
|
|
{expanded ? '▾' : '▸'} Aufgaben{taskLabel}
|
|
</button>
|
|
|
|
{expanded && (
|
|
<div className="action-task-panel__body">
|
|
{error && <p className="error">{error}</p>}
|
|
{loading && <p className="muted">Aufgaben werden geladen …</p>}
|
|
{!loading && tasks.length === 0 && (
|
|
<p className="muted">Noch keine Aufgaben für dieses Arbeitspaket.</p>
|
|
)}
|
|
{!loading && tasks.length > 0 && (
|
|
<TaskTreeNodes
|
|
tasksByParent={tasksByParent}
|
|
roadmapItems={roadmapItems}
|
|
canManage={canManage}
|
|
busy={busy}
|
|
onEdit={(task) => setModal({ mode: 'edit', task, key: task.id })}
|
|
onAddSubtask={(task) =>
|
|
setModal({
|
|
mode: 'create',
|
|
parentTaskId: task.id,
|
|
parentLabel: task.title,
|
|
key: `sub-${task.id}`,
|
|
})
|
|
}
|
|
onStatusChange={handleStatus}
|
|
onDelete={handleDelete}
|
|
/>
|
|
)}
|
|
{canManage && (
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary btn-sm action-task-panel__add"
|
|
disabled={busy}
|
|
onClick={() => setModal({ mode: 'create', key: 'root' })}
|
|
>
|
|
Aufgabe hinzufügen
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Modal
|
|
open={Boolean(modal)}
|
|
title={
|
|
modal?.mode === 'edit'
|
|
? 'Aufgabe bearbeiten'
|
|
: modal?.parentLabel
|
|
? 'Unteraufgabe anlegen'
|
|
: 'Aufgabe anlegen'
|
|
}
|
|
onClose={() => setModal(null)}
|
|
>
|
|
{modal?.mode === 'edit' ? (
|
|
<TaskForm
|
|
initial={modal.task}
|
|
roadmapItems={roadmapItems}
|
|
onSubmit={(payload) => handleUpdate(modal.task.id, payload)}
|
|
onCancel={() => setModal(null)}
|
|
busy={busy}
|
|
/>
|
|
) : (
|
|
modal && (
|
|
<TaskForm
|
|
parentTaskId={modal.parentTaskId}
|
|
parentLabel={modal.parentLabel}
|
|
roadmapItems={roadmapItems}
|
|
onSubmit={handleCreate}
|
|
onCancel={() => setModal(null)}
|
|
busy={busy}
|
|
submitLabel="Anlegen"
|
|
/>
|
|
)
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|