fix: Sprint-AP Bearbeiten, Sprint-Verschiebung und klarere Aktionsleiste.
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled

Behebt editingActionId-Bug in Plan/Work-Sprint; Schnellzuweisung AP zu anderem Sprint ohne Voll-Reload.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-24 18:04:26 +02:00
parent 04d1dbf1f9
commit ee1e1c0671
9 changed files with 178 additions and 40 deletions

View File

@ -3,6 +3,7 @@ import { ACTION_STATUS_LABELS, PRIORITY_LABELS } from '../constants/status.js'
import { ActorSelect } from './ActorSelect.jsx'
import { GateSelect } from './GateSelect.jsx'
import { flattenLeafProjectOptions, getLeafProjects } from '../utils/projectTree.js'
import { PLANNING_SPRINT_STATUSES } from '../utils/workCycleActions.js'
function isoToLocalInput(iso) {
if (!iso) return ''
@ -47,7 +48,9 @@ export function ActionForm({
const defaultActors = initial.assigned_actor_ids || []
const leafProjects = getLeafProjects(projects)
const projectOptions = flattenLeafProjectOptions(projects)
const cycleOptions = workCycles.filter((c) => c.status !== 'completed')
const cycleOptions = workCycles.filter(
(c) => PLANNING_SPRINT_STATUSES.has(c.status) || c.id === initial.work_cycle_id,
)
const defaultCycleId =
initial.work_cycle_id || (activeWorkCycle?.id && !initial.id ? activeWorkCycle.id : '')
@ -108,13 +111,19 @@ export function ActionForm({
/>
{cycleOptions.length > 0 && (
<label>
Sprint (optional)
Sprint
<select name="work_cycle_id" defaultValue={defaultCycleId}>
<option value=""> kein Sprint </option>
{cycleOptions.map((cycle) => (
<option key={cycle.id} value={cycle.id}>
{cycle.title}
{cycle.status === 'active' ? ' (aktiv)' : ''}
{cycle.status === 'active'
? ' (aktiv)'
: cycle.status === 'planned'
? ' (geplant)'
: cycle.status === 'reached'
? ' (abgeschlossen)'
: ''}
</option>
))}
</select>

View File

@ -5,6 +5,7 @@ import { PriorityBadge } from './PriorityBadge.jsx'
import { ActionForm } from './ActionForm.jsx'
import { gateTitleById } from './GateSelect.jsx'
import { actionTitleById } from '../utils/actionReferences.js'
import { SprintTransferSelect } from './SprintTransferSelect.jsx'
function formatDue(iso) {
if (!iso) return null
@ -45,6 +46,7 @@ export function ActionHubCard({
onQuickStatus,
onCreateBlocker,
onUnplanToBacklog,
onMoveToSprint,
canManage,
canManageBlocker,
formBusy,
@ -123,6 +125,14 @@ export function ActionHubCard({
{action.assigned_actor_ids?.length > 0 && (
<span>{action.assigned_actor_ids.length} zugewiesen</span>
)}
{action.work_cycle_id && (
<span>
Sprint:{' '}
{workCycles.find((c) => c.id === action.work_cycle_id)?.title ||
activeWorkCycle?.title ||
'…'}
</span>
)}
{due && <span>Fällig: {due}</span>}
</div>
</header>
@ -169,18 +179,29 @@ export function ActionHubCard({
{canManage && (
<footer className="action-hub-card-actions">
<select
className="inline-select"
value={action.status}
onChange={(e) => onQuickStatus(e.target.value)}
aria-label="Status ändern"
>
{ACTION_STATUSES.map((s) => (
<option key={s} value={s}>
{ACTION_STATUS_LABELS[s]}
</option>
))}
</select>
<div className="action-hub-card-actions__primary">
<select
className="inline-select"
value={action.status}
onChange={(e) => onQuickStatus(e.target.value)}
aria-label="Status ändern"
>
{ACTION_STATUSES.map((s) => (
<option key={s} value={s}>
{ACTION_STATUS_LABELS[s]}
</option>
))}
</select>
{typeof onMoveToSprint === 'function' && (
<SprintTransferSelect
action={action}
workCycles={workCycles}
busy={formBusy}
onMove={onMoveToSprint}
/>
)}
</div>
<div className="action-hub-card-actions__secondary">
{canManageBlocker && action.status !== 'done' && (
<button
type="button"
@ -216,6 +237,7 @@ export function ActionHubCard({
Details
</Link>
)}
</div>
</footer>
)}
</article>

View File

@ -22,6 +22,7 @@ export function InitiativeActionsHub({
onQuickStatus,
onCreateBlockerForAction,
onUnplanToBacklog,
onMoveToSprint,
canManage,
canManageBlocker,
formBusy,
@ -32,6 +33,7 @@ export function InitiativeActionsHub({
onReloadActors,
sectionTitle = 'Arbeitspakete',
sectionLead = 'Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews hängen am Arbeitspaket.',
allActions = [],
}) {
return (
<section className="card initiative-actions-hub">
@ -98,6 +100,7 @@ export function InitiativeActionsHub({
onQuickStatus={(status) => onQuickStatus(action, status)}
onCreateBlocker={() => onCreateBlockerForAction(action.id)}
onUnplanToBacklog={onUnplanToBacklog}
onMoveToSprint={onMoveToSprint}
canManage={canManage}
canManageBlocker={canManageBlocker}
formBusy={formBusy}
@ -111,7 +114,7 @@ export function InitiativeActionsHub({
roadmapItems={roadmapItems}
workCycles={workCycles}
activeWorkCycle={activeWorkCycle}
allActions={actions}
allActions={allActions.length ? allActions : actions}
/>
))}
</div>

View File

@ -0,0 +1,46 @@
import { listCarryoverTargetSprints } from '../utils/workCycleActions.js'
/**
* Schnellzuweisung: AP in einen anderen Sprint verschieben.
*/
export function SprintTransferSelect({
action,
workCycles = [],
busy = false,
onMove,
className = '',
}) {
if (!action?.work_cycle_id || action.status === 'done' || action.status === 'discarded') {
return null
}
const targets = listCarryoverTargetSprints(workCycles, action.work_cycle_id)
if (targets.length === 0) return null
return (
<label className={`sprint-transfer-select ${className}`.trim()}>
<span className="sr-only">In anderen Sprint verschieben</span>
<select
className="inline-select"
value=""
disabled={busy}
aria-label={`${action.title} in anderen Sprint verschieben`}
onChange={(e) => {
const targetId = e.target.value
if (targetId && typeof onMove === 'function') {
onMove(action, targetId)
}
e.target.value = ''
}}
>
<option value=""> Sprint </option>
{targets.map((cycle) => (
<option key={cycle.id} value={cycle.id}>
{cycle.title}
{cycle.status === 'active' ? ' (aktiv)' : cycle.status === 'planned' ? ' (geplant)' : ''}
</option>
))}
</select>
</label>
)
}

View File

@ -118,7 +118,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showActionForm, setShowActionForm] = useState(false)
const [editingAction, setEditingAction] = useState(null)
const [editingActionId, setEditingActionId] = useState(null)
const [formBusy, setFormBusy] = useState(false)
const [hideDone, setHideDone] = useState(true)
const [selectedWorkCycleId, setSelectedWorkCycleId] = useState('')
@ -364,7 +364,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
if (capabilities.has('kairo.action.manage')) {
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
}
setEditingAction(null)
setEditingActionId(null)
await load()
} catch (err) {
setError(err.message)
@ -373,6 +373,20 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
}
}
async function handleMoveActionToSprint(action, targetCycleId) {
if (!action?.id || !targetCycleId || action.work_cycle_id === targetCycleId) return
setFormBusy(true)
setError(null)
try {
const updated = await updateAction(action.id, { work_cycle_id: targetCycleId })
setActions((prev) => prev.map((a) => (a.id === action.id ? updated : a)))
} catch (err) {
setError(err.message)
} finally {
setFormBusy(false)
}
}
async function handleQuickStatus(action, status) {
if (!capabilities.has('kairo.action.manage')) return
try {
@ -966,8 +980,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
error,
showActionForm,
setShowActionForm,
editingAction,
setEditingAction,
editingActionId,
setEditingActionId,
formBusy,
hideDone,
setHideDone,
@ -983,6 +997,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
handleMethodProfileChange,
handleCreateAction,
handleUpdateAction,
handleMoveActionToSprint,
handleQuickStatus,
handleCreateBlocker,
handleCreateBlockerForAction,

View File

@ -17,8 +17,8 @@ export function InitiativeExecutionPage() {
setHideDone,
showActionForm,
setShowActionForm,
editingAction,
setEditingAction,
editingActionId,
setEditingActionId,
capabilities,
formBusy,
unlinkedBlockers,
@ -63,11 +63,11 @@ export function InitiativeExecutionPage() {
showForm={showActionForm}
onToggleForm={() => {
setShowActionForm((v) => !v)
setEditingAction(null)
setEditingActionId(null)
}}
editingActionId={editingAction}
onEditAction={setEditingAction}
onCancelEdit={() => setEditingAction(null)}
editingActionId={editingActionId}
onEditAction={setEditingActionId}
onCancelEdit={() => setEditingActionId(null)}
onCreateAction={ops.handleCreateAction}
onUpdateAction={ops.handleUpdateAction}
onQuickStatus={ops.handleQuickStatus}

View File

@ -28,8 +28,8 @@ function PlanSprintInner() {
formBusy,
showActionForm,
setShowActionForm,
editingAction,
setEditingAction,
editingActionId,
setEditingActionId,
actionContextById,
actors,
actorsLoading,
@ -45,6 +45,7 @@ function PlanSprintInner() {
handleUpdateAction,
handleQuickStatus,
handleCreateBlockerForAction,
handleMoveActionToSprint,
handleUnplanAction,
} = ops
@ -102,14 +103,15 @@ function PlanSprintInner() {
onHideDoneChange={setHideDone}
showForm={showActionForm}
onToggleForm={() => setShowActionForm((v) => !v)}
editingActionId={editingAction?.id}
onEditAction={setEditingAction}
onCancelEdit={() => setEditingAction(null)}
editingActionId={editingActionId}
onEditAction={setEditingActionId}
onCancelEdit={() => setEditingActionId(null)}
onCreateAction={createSprintAction}
onUpdateAction={handleUpdateAction}
onQuickStatus={handleQuickStatus}
onCreateBlockerForAction={handleCreateBlockerForAction}
onUnplanToBacklog={handleUnplanAction}
onMoveToSprint={handleMoveActionToSprint}
canManage={capabilities.has('kairo.action.manage')}
canManageBlocker={capabilities.has('kairo.blocker.manage')}
formBusy={formBusy}
@ -120,6 +122,7 @@ function PlanSprintInner() {
onReloadActors={reloadActors}
workCycles={workCycles}
activeWorkCycle={selectedWorkCycle}
allActions={actions}
sectionTitle={`Sprint-Backlog — ${selectedWorkCycle.title}`}
sectionLead={sprintLead}
/>

View File

@ -26,8 +26,8 @@ function WorkSprintInner() {
formBusy,
showActionForm,
setShowActionForm,
editingAction,
setEditingAction,
editingActionId,
setEditingActionId,
actionContextById,
actors,
actorsLoading,
@ -38,6 +38,8 @@ function WorkSprintInner() {
handleUpdateAction,
handleQuickStatus,
handleCreateBlockerForAction,
handleMoveActionToSprint,
handleUnplanAction,
initiative,
steeringSnapshot,
steeringSnapshotLoading,
@ -101,13 +103,15 @@ function WorkSprintInner() {
onHideDoneChange={setHideDone}
showForm={showActionForm}
onToggleForm={() => setShowActionForm((v) => !v)}
editingActionId={editingAction?.id}
onEditAction={setEditingAction}
onCancelEdit={() => setEditingAction(null)}
editingActionId={editingActionId}
onEditAction={setEditingActionId}
onCancelEdit={() => setEditingActionId(null)}
onCreateAction={handleCreateAction}
onUpdateAction={handleUpdateAction}
onQuickStatus={handleQuickStatus}
onCreateBlockerForAction={handleCreateBlockerForAction}
onMoveToSprint={handleMoveActionToSprint}
onUnplanToBacklog={handleUnplanAction}
canManage={capabilities.has('kairo.action.manage')}
canManageBlocker={capabilities.has('kairo.blocker.manage')}
formBusy={formBusy}
@ -159,13 +163,15 @@ function WorkSprintInner() {
onHideDoneChange={setHideDone}
showForm={showActionForm}
onToggleForm={() => setShowActionForm((v) => !v)}
editingActionId={editingAction?.id}
onEditAction={setEditingAction}
onCancelEdit={() => setEditingAction(null)}
editingActionId={editingActionId}
onEditAction={setEditingActionId}
onCancelEdit={() => setEditingActionId(null)}
onCreateAction={createAction}
onUpdateAction={handleUpdateAction}
onQuickStatus={handleQuickStatus}
onCreateBlockerForAction={handleCreateBlockerForAction}
onMoveToSprint={handleMoveActionToSprint}
onUnplanToBacklog={handleUnplanAction}
canManage={capabilities.has('kairo.action.manage')}
canManageBlocker={capabilities.has('kairo.blocker.manage')}
formBusy={formBusy}
@ -176,9 +182,14 @@ function WorkSprintInner() {
onReloadActors={reloadActors}
workCycles={ops.workCycles}
activeWorkCycle={activeWorkCycle}
allActions={ops.actions}
/>
</>
)}
{!activeWorkCycle && !isProductArchetype && (
<EmptyState message="Sprint-Ausführung ist für Product-Vorhaben — wähle ein passendes Vorhaben im Scope." />
)}
</>
)
}

View File

@ -999,13 +999,42 @@
.action-hub-card-actions {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.75rem;
padding-top: 0.65rem;
border-top: 1px solid var(--jk-border, #e5e7eb);
}
.action-hub-card-actions__primary,
.action-hub-card-actions__secondary {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.action-hub-card-actions__primary {
padding-bottom: 0.35rem;
border-bottom: 1px dashed var(--jk-border, #e5e7eb);
}
.sprint-transfer-select .inline-select {
min-width: 9rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.btn-sm {
padding: 0.25rem 0.55rem;
font-size: 0.8rem;