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
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>
186 lines
6.3 KiB
JavaScript
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>
|
|
)
|
|
}
|