Kairo-Jinkendo/frontend/src/components/PlanActionsSection.jsx
Lars 82a5b9adec
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 2m40s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 19s
Test Suite / playwright-smoke (push) Successful in 13s
AP2.2d: B2b Product-Backlog und B3 Sprint-Backlog in UI.
Zeitbox-API im Frontend, Plan/Ausführen Sprint-Ansichten, Backlog-Convert in aktiven Sprint. agile_iteration bei aktiver Zeitbox für continuous_product.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 16:27:34 +02:00

186 lines
6.3 KiB
JavaScript

import { Link } from 'react-router-dom'
import { useMemo, useState } from 'react'
import { EmptyState } from './EmptyState.jsx'
import { StatusBadge } from './StatusBadge.jsx'
import { PriorityBadge } from './PriorityBadge.jsx'
import { Modal } from './Modal.jsx'
import { ActionForm } from './ActionForm.jsx'
import { ActionTaskPanel } from './ActionTaskPanel.jsx'
import { gateTitleById } from './GateSelect.jsx'
import { buildProjectPath, collectProjectSubtreeIds } from '../utils/projectTree.js'
import { actionPath } from '../utils/routes.js'
import {
formatBlockedByTitles,
getActionExecutionMeta,
sortActionsForExecutionPlan,
} from '../utils/executionGraph.js'
import { ExecutionFlowBadge } from './ExecutionFlowBadge.jsx'
function formatProjectContext(projects, projectId) {
if (!projectId) return 'Direkt am Vorhaben'
const path = buildProjectPath(projects, projectId)
return path.map((p) => p.title).join(' → ')
}
export function PlanActionsSection({
actions,
projects = [],
roadmapItems = [],
scopeProjectId = '',
hideDone,
onHideDoneChange,
canManage,
onCreateAction,
actors,
actorsLoading,
actorsError,
actorsUsedFallback,
onReloadActors,
busy,
executionGraph = null,
sectionTitle = 'Arbeit',
sectionLead = 'Committete Arbeitspakete und zugehörige Aufgaben — Ausführung über die Objektseite.',
}) {
const [showCreate, setShowCreate] = useState(false)
const [expandedActions, setExpandedActions] = useState(() => new Set())
const visibleActions = useMemo(() => {
let list = hideDone
? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded')
: [...actions]
if (scopeProjectId) {
const subtreeIds = collectProjectSubtreeIds(projects, scopeProjectId)
list = list.filter((a) => a.project_id && subtreeIds.has(a.project_id))
}
return sortActionsForExecutionPlan(list)
}, [actions, hideDone, scopeProjectId, projects])
async function handleCreate(payload) {
await onCreateAction(payload)
setShowCreate(false)
}
function toggleActionTasks(actionId) {
setExpandedActions((prev) => {
const next = new Set(prev)
if (next.has(actionId)) next.delete(actionId)
else next.add(actionId)
return next
})
}
return (
<section className="card plan-actions-section">
<div className="section-header">
<div>
<h2>{sectionTitle}</h2>
<p className="section-lead muted">{sectionLead}</p>
</div>
<div className="section-actions">
<label className="checkbox-label inline-filter">
<input
type="checkbox"
checked={hideDone}
onChange={(e) => onHideDoneChange(e.target.checked)}
/>
Erledigte ausblenden
</label>
{canManage && (
<button
type="button"
className="btn btn-primary btn-block-mobile"
onClick={() => setShowCreate(true)}
>
Arbeitspaket
</button>
)}
</div>
</div>
{scopeProjectId && (
<p className="muted plan-actions-section__scope-hint">
Gefiltert nach Projekt im Scope Scope oben leeren für alle Arbeitspakete.
</p>
)}
{visibleActions.length === 0 && (
<EmptyState message="Noch keine Arbeitspakete — lege welche an oder wandle Backlog-Items um." />
)}
<ul className="item-list plan-actions-list">
{visibleActions.map((action) => {
const execMeta = getActionExecutionMeta(executionGraph, action.id)
const blockedHint =
execMeta?.blocked_by?.length > 0
? formatBlockedByTitles(actions, execMeta.blocked_by)
: null
return (
<li key={action.id} className="list-item card-list-item plan-actions-list__item">
<div className="plan-actions-list__header">
<div className="list-item-main">
<Link to={actionPath(action.id)} className="plan-actions-list__title">
<strong>{action.title}</strong>
</Link>
{action.action_kind === 'planning' && (
<span className="badge execution-flow-badge execution-flow-badge--planning">
Planung
</span>
)}
<p className="list-item-sub muted">
{formatProjectContext(projects, action.project_id)}
</p>
{action.roadmap_item_id && (
<p className="list-item-sub muted">
Gate: {gateTitleById(roadmapItems, action.roadmap_item_id)}
</p>
)}
{blockedHint && (
<p className="list-item-sub muted">Wartet auf: {blockedHint}</p>
)}
{action.description && (
<p className="list-item-desc">{action.description}</p>
)}
</div>
<div className="list-item-meta action-controls">
<ExecutionFlowBadge meta={execMeta} actionStatus={action.status} />
<StatusBadge kind="action" status={action.status} />
<PriorityBadge priority={action.priority} />
<Link to={actionPath(action.id)} className="btn btn-secondary btn-sm">
Öffnen
</Link>
</div>
</div>
<ActionTaskPanel
actionId={action.id}
roadmapItems={roadmapItems}
canManage={canManage}
expanded={expandedActions.has(action.id)}
onToggle={() => toggleActionTasks(action.id)}
/>
</li>
)
})}
</ul>
<Modal open={showCreate} title="Arbeitspaket anlegen" onClose={() => setShowCreate(false)} size="lg">
<ActionForm
projects={projects}
roadmapItems={roadmapItems}
actors={actors}
actorsLoading={actorsLoading}
actorsError={actorsError}
actorsUsedFallback={actorsUsedFallback}
onReloadActors={onReloadActors}
onSubmit={handleCreate}
onCancel={() => setShowCreate(false)}
busy={busy}
submitLabel="Anlegen"
/>
</Modal>
</section>
)
}