AP2.2d: Sprint-Planung, Abschluss und Product-Fallback.
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Successful in 2m56s
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 18s
Test Suite / playwright-smoke (push) Has been cancelled
Some checks failed
Deploy Development / deploy (push) Successful in 48s
Test Suite / pytest-backend (push) Successful in 2m56s
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 18s
Test Suite / playwright-smoke (push) Has been cancelled
Backlog committet in gewaehlten Sprint, Sprint complete-Endpoint, continuous_product-Ansicht ohne aktiven Sprint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
09826d146e
commit
f6aba94864
|
|
@ -85,6 +85,26 @@ def create_work_cycle(
|
|||
raise HTTPException(status_code=400, detail=detail) from exc
|
||||
|
||||
|
||||
@initiative_router.post("/{work_cycle_id}/complete")
|
||||
def complete_work_cycle(
|
||||
initiative_id: str,
|
||||
work_cycle_id: str,
|
||||
ctx: TenantContext = Depends(require_capability("kairo.milestone.manage")),
|
||||
):
|
||||
try:
|
||||
return work_cycle_service.complete_work_cycle(
|
||||
tenant_id=ctx.tenant_id,
|
||||
initiative_id=initiative_id,
|
||||
work_cycle_id=work_cycle_id,
|
||||
user_id=ctx.user_id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
if detail in ("Initiative nicht gefunden", "Sprint nicht gefunden oder bereits abgeschlossen"):
|
||||
raise HTTPException(status_code=404, detail=detail) from exc
|
||||
raise HTTPException(status_code=400, detail=detail) from exc
|
||||
|
||||
|
||||
@initiative_router.post("/{work_cycle_id}/activate")
|
||||
def activate_work_cycle(
|
||||
initiative_id: str,
|
||||
|
|
|
|||
|
|
@ -176,6 +176,52 @@ def activate_work_cycle(
|
|||
return result
|
||||
|
||||
|
||||
def complete_work_cycle(
|
||||
*,
|
||||
tenant_id: str,
|
||||
initiative_id: str,
|
||||
work_cycle_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
validate_work_cycle_in_initiative(
|
||||
tenant_id=tenant_id,
|
||||
initiative_id=initiative_id,
|
||||
work_cycle_id=work_cycle_id,
|
||||
)
|
||||
if not get_initiative(tenant_id=tenant_id, initiative_id=initiative_id):
|
||||
raise ValueError("Initiative nicht gefunden")
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE roadmap_items
|
||||
SET status = 'reached', updated_at = NOW()
|
||||
WHERE id = %s AND tenant_id = %s
|
||||
AND item_type = %s
|
||||
AND status IN ('planned', 'active', 'at_risk')
|
||||
RETURNING id, title, goal_description, status, target_date, sort_order, item_type
|
||||
""",
|
||||
(work_cycle_id, tenant_id, WORK_CYCLE_TYPE),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise ValueError("Sprint nicht gefunden oder bereits abgeschlossen")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
result = _serialize_cycle({**dict(row), "initiative_id": initiative_id})
|
||||
log_audit(
|
||||
"work_cycle.completed",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
details={"work_cycle_id": work_cycle_id, "initiative_id": initiative_id},
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def count_actions_in_work_cycle(
|
||||
*, tenant_id: str, initiative_id: str, work_cycle_id: str
|
||||
) -> int:
|
||||
|
|
|
|||
|
|
@ -42,3 +42,73 @@ def test_convert_backlog_assigns_active_work_cycle(client):
|
|||
assert converted.status_code == 201
|
||||
action = converted.json()["action"]
|
||||
assert action["work_cycle_id"] == cycle_id
|
||||
|
||||
|
||||
def test_convert_backlog_to_planned_sprint(client):
|
||||
user = provision_user_in_tenant(tenant_role="admin")
|
||||
token = _login(client, user)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
token,
|
||||
title="Product Sprint Plan",
|
||||
archetype_key="initiative.product",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
cycle = client.post(
|
||||
f"/api/initiatives/{initiative_id}/work-cycles",
|
||||
json={"title": "Sprint R2", "status": "planned"},
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert cycle.status_code == 201
|
||||
cycle_id = cycle.json()["id"]
|
||||
|
||||
backlog = client.post(
|
||||
f"/api/initiatives/{initiative_id}/backlog",
|
||||
json={"title": "Story Y", "status": "accepted"},
|
||||
headers=_auth(token),
|
||||
)
|
||||
backlog_id = backlog.json()["id"]
|
||||
|
||||
converted = client.post(
|
||||
f"/api/backlog/{backlog_id}/convert-to-action",
|
||||
json={"work_cycle_id": cycle_id, "assign_active_sprint": False},
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert converted.status_code == 201
|
||||
assert converted.json()["action"]["work_cycle_id"] == cycle_id
|
||||
|
||||
|
||||
def test_complete_work_cycle(client):
|
||||
user = provision_user_in_tenant(tenant_role="admin")
|
||||
token = _login(client, user)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
token,
|
||||
title="Sprint Complete",
|
||||
archetype_key="initiative.product",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
cycle = client.post(
|
||||
f"/api/initiatives/{initiative_id}/work-cycles",
|
||||
json={"title": "Sprint R1", "status": "active"},
|
||||
headers=_auth(token),
|
||||
)
|
||||
cycle_id = cycle.json()["id"]
|
||||
|
||||
completed = client.post(
|
||||
f"/api/initiatives/{initiative_id}/work-cycles/{cycle_id}/complete",
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert completed.status_code == 200
|
||||
assert completed.json()["status"] == "reached"
|
||||
|
||||
active = client.get(
|
||||
f"/api/initiatives/{initiative_id}/work-cycles/active",
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert active.status_code == 200
|
||||
assert active.json() is None
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ export function createWorkCycle(initiativeId, body) {
|
|||
})
|
||||
}
|
||||
|
||||
export function completeWorkCycle(initiativeId, workCycleId) {
|
||||
return apiFetch(`/api/initiatives/${initiativeId}/work-cycles/${workCycleId}/complete`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export function activateWorkCycle(initiativeId, workCycleId) {
|
||||
return apiFetch(`/api/initiatives/${initiativeId}/work-cycles/${workCycleId}/activate`, {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { StatusBadge } from './StatusBadge.jsx'
|
||||
import { PriorityBadge } from './PriorityBadge.jsx'
|
||||
import { EmptyState } from './EmptyState.jsx'
|
||||
|
|
@ -9,25 +9,58 @@ import { gateTitleById } from './GateSelect.jsx'
|
|||
import { computeDropPatches, computeMovePatches, sortByOrder } from '../utils/reorder.js'
|
||||
import { useMinWidth } from '../hooks/useMinWidth.js'
|
||||
|
||||
const PLANNING_STATUSES = new Set(['planned', 'active', 'at_risk'])
|
||||
|
||||
export function BacklogSection({
|
||||
items,
|
||||
roadmapItems = [],
|
||||
workCycles = [],
|
||||
activeWorkCycle = null,
|
||||
sprintPlanningEnabled = false,
|
||||
canManage,
|
||||
onCreate,
|
||||
onUpdate,
|
||||
onReorder,
|
||||
onConvert,
|
||||
onBulkConvert,
|
||||
onDelete,
|
||||
busy,
|
||||
sectionTitle = 'Product Backlog',
|
||||
sectionLead = 'Eingang vor dem Commit — triagieren, dann in Arbeitspaket (Sprint) umwandeln.',
|
||||
sectionLead = 'Eingang vor dem Commit — triagieren, dann in den Sprint planen.',
|
||||
}) {
|
||||
const [modalMode, setModalMode] = useState(null)
|
||||
const [dragItemId, setDragItemId] = useState('')
|
||||
const [dropTargetId, setDropTargetId] = useState('')
|
||||
const [selectedIds, setSelectedIds] = useState(() => new Set())
|
||||
const [sprintTargetId, setSprintTargetId] = useState('')
|
||||
const isDesktop = useMinWidth(1024)
|
||||
const canReorder = canManage && typeof onReorder === 'function'
|
||||
|
||||
const planableSprints = useMemo(
|
||||
() => workCycles.filter((cycle) => PLANNING_STATUSES.has(cycle.status)),
|
||||
[workCycles],
|
||||
)
|
||||
|
||||
const showSprintPlanning = sprintPlanningEnabled && planableSprints.length > 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSprintPlanning) {
|
||||
setSprintTargetId('')
|
||||
return
|
||||
}
|
||||
if (sprintTargetId && planableSprints.some((cycle) => cycle.id === sprintTargetId)) {
|
||||
return
|
||||
}
|
||||
const preferred = activeWorkCycle?.id || planableSprints[0]?.id || ''
|
||||
setSprintTargetId(preferred)
|
||||
}, [showSprintPlanning, sprintTargetId, planableSprints, activeWorkCycle?.id])
|
||||
|
||||
const selectedSprint = planableSprints.find((cycle) => cycle.id === sprintTargetId) || null
|
||||
|
||||
const convertLabel = selectedSprint
|
||||
? `In Sprint „${selectedSprint.title}" planen`
|
||||
: 'In Arbeitspaket umwandeln'
|
||||
|
||||
const sortedItems = useMemo(
|
||||
() => sortByOrder(items.filter((item) => item.status !== 'converted')),
|
||||
[items],
|
||||
|
|
@ -104,6 +137,46 @@ export function BacklogSection({
|
|||
const modalTitle =
|
||||
modalMode?.kind === 'create' ? 'Backlog-Item anlegen' : 'Backlog-Item bearbeiten'
|
||||
|
||||
const acceptedItems = sortedItems.filter((item) => item.status === 'accepted')
|
||||
const selectedAccepted = acceptedItems.filter((item) => selectedIds.has(item.id))
|
||||
|
||||
function toggleSelected(itemId) {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(itemId)) next.delete(itemId)
|
||||
else next.add(itemId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
function convertOptions() {
|
||||
return sprintTargetId ? { work_cycle_id: sprintTargetId } : {}
|
||||
}
|
||||
|
||||
async function handleSingleConvert(itemId) {
|
||||
await onConvert(itemId, convertOptions())
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(itemId)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
async function handleBulkPlan() {
|
||||
if (!selectedAccepted.length) return
|
||||
if (typeof onBulkConvert === 'function') {
|
||||
await onBulkConvert(
|
||||
selectedAccepted.map((item) => item.id),
|
||||
convertOptions(),
|
||||
)
|
||||
} else {
|
||||
for (const item of selectedAccepted) {
|
||||
await onConvert(item.id, convertOptions())
|
||||
}
|
||||
}
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card backlog-section">
|
||||
<div className="section-header">
|
||||
|
|
@ -122,6 +195,45 @@ export function BacklogSection({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{showSprintPlanning && canManage && acceptedItems.length > 0 && (
|
||||
<div className="backlog-sprint-planning card-list-item">
|
||||
<label className="backlog-sprint-planning__field">
|
||||
<span className="muted">Sprint-Ziel</span>
|
||||
<select
|
||||
value={sprintTargetId}
|
||||
onChange={(e) => setSprintTargetId(e.target.value)}
|
||||
disabled={busy}
|
||||
>
|
||||
{planableSprints.map((cycle) => (
|
||||
<option key={cycle.id} value={cycle.id}>
|
||||
{cycle.title}
|
||||
{cycle.status === 'active' ? ' (aktiv)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{selectedAccepted.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={busy || !sprintTargetId}
|
||||
onClick={handleBulkPlan}
|
||||
>
|
||||
{selectedAccepted.length} Item{selectedAccepted.length === 1 ? '' : 's'} {convertLabel}
|
||||
</button>
|
||||
)}
|
||||
<p className="muted backlog-sprint-planning__hint">
|
||||
Items markieren und in den gewählten Sprint committen — Planung unter Plan → Sprint.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sprintPlanningEnabled && canManage && acceptedItems.length > 0 && planableSprints.length === 0 && (
|
||||
<p className="muted backlog-sprint-planning__hint">
|
||||
Lege zuerst einen Sprint unter Plan → Sprint an, um Backlog-Items zu planen.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{items.length === 0 && <EmptyState message="Backlog ist leer." />}
|
||||
|
||||
<ul className="item-list backlog-reorder-list">
|
||||
|
|
@ -150,6 +262,17 @@ export function BacklogSection({
|
|||
⋮⋮
|
||||
</span>
|
||||
)}
|
||||
{showSprintPlanning && canManage && item.status === 'accepted' && (
|
||||
<label className="backlog-select-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(item.id)}
|
||||
onChange={() => toggleSelected(item.id)}
|
||||
disabled={busy}
|
||||
aria-label={`${item.title} für Sprint-Planung markieren`}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="list-item-main list-item-main--clickable"
|
||||
|
|
@ -185,10 +308,10 @@ export function BacklogSection({
|
|||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => onConvert(item.id)}
|
||||
disabled={busy}
|
||||
onClick={() => handleSingleConvert(item.id)}
|
||||
disabled={busy || (showSprintPlanning && !sprintTargetId)}
|
||||
>
|
||||
In Arbeitspaket umwandeln
|
||||
{convertLabel}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export function WorkCyclesPanel({
|
|||
canManage,
|
||||
onCreate,
|
||||
onActivate,
|
||||
onComplete,
|
||||
busy = false,
|
||||
}) {
|
||||
const [title, setTitle] = useState('')
|
||||
|
|
@ -100,7 +101,17 @@ export function WorkCyclesPanel({
|
|||
<p className="muted item-meta">{cycle.goal_description}</p>
|
||||
)}
|
||||
</div>
|
||||
{canManage && cycle.status !== 'active' && (
|
||||
{canManage && cycle.status === 'active' && typeof onComplete === 'function' && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
disabled={busy}
|
||||
onClick={() => onComplete(cycle.id)}
|
||||
>
|
||||
Abschließen
|
||||
</button>
|
||||
)}
|
||||
{canManage && cycle.status !== 'active' && cycle.status !== 'reached' && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ import {
|
|||
getActiveWorkCycle,
|
||||
createWorkCycle,
|
||||
activateWorkCycle,
|
||||
completeWorkCycle,
|
||||
} from '../api/workCycles.js'
|
||||
import { useCapabilities } from '../hooks/useCapabilities.js'
|
||||
import { useActors } from '../hooks/useActors.js'
|
||||
|
|
@ -420,10 +421,31 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
}
|
||||
}
|
||||
|
||||
async function handleConvertBacklog(itemId) {
|
||||
async function handleConvertBacklog(itemId, options = {}) {
|
||||
setFormBusy(true)
|
||||
try {
|
||||
await convertBacklogToAction(itemId, { assign_active_sprint: true })
|
||||
await convertBacklogToAction(itemId, {
|
||||
assign_active_sprint: options.work_cycle_id ? false : options.assign_active_sprint ?? true,
|
||||
work_cycle_id: options.work_cycle_id,
|
||||
})
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setFormBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBulkConvertBacklog(itemIds, options = {}) {
|
||||
if (!itemIds.length) return
|
||||
setFormBusy(true)
|
||||
try {
|
||||
for (const itemId of itemIds) {
|
||||
await convertBacklogToAction(itemId, {
|
||||
assign_active_sprint: options.work_cycle_id ? false : options.assign_active_sprint ?? true,
|
||||
work_cycle_id: options.work_cycle_id,
|
||||
})
|
||||
}
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -456,6 +478,18 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
}
|
||||
}
|
||||
|
||||
async function handleCompleteWorkCycle(workCycleId) {
|
||||
setFormBusy(true)
|
||||
try {
|
||||
await completeWorkCycle(id, workCycleId)
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setFormBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteBacklog(itemId) {
|
||||
try {
|
||||
await deleteBacklogItem(itemId)
|
||||
|
|
@ -821,8 +855,10 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
handleBacklogGate,
|
||||
handleBacklogStatus,
|
||||
handleConvertBacklog,
|
||||
handleBulkConvertBacklog,
|
||||
handleCreateWorkCycle,
|
||||
handleActivateWorkCycle,
|
||||
handleCompleteWorkCycle,
|
||||
handleDeleteBacklog,
|
||||
handleCreateRoadmapItem,
|
||||
handleRoadmapItemStatus,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import { LoadingState } from '../../components/LoadingState.jsx'
|
|||
|
||||
export function InitiativeInboxPage() {
|
||||
const {
|
||||
initiative,
|
||||
backlogItems,
|
||||
roadmapItems,
|
||||
workCycles,
|
||||
activeWorkCycle,
|
||||
capabilities,
|
||||
formBusy,
|
||||
error,
|
||||
|
|
@ -14,6 +17,7 @@ export function InitiativeInboxPage() {
|
|||
handleUpdateBacklog,
|
||||
handleReorderBacklog,
|
||||
handleConvertBacklog,
|
||||
handleBulkConvertBacklog,
|
||||
handleDeleteBacklog,
|
||||
} = useInitiativeOperations()
|
||||
|
||||
|
|
@ -25,17 +29,23 @@ export function InitiativeInboxPage() {
|
|||
return <LoadingState message="Lade Eingang …" />
|
||||
}
|
||||
|
||||
const isProductArchetype = initiative?.archetype_key === 'initiative.product'
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<BacklogSection
|
||||
items={backlogItems}
|
||||
roadmapItems={roadmapItems}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
sprintPlanningEnabled={isProductArchetype}
|
||||
canManage={capabilities.has('kairo.backlog.manage')}
|
||||
onCreate={handleCreateBacklog}
|
||||
onUpdate={handleUpdateBacklog}
|
||||
onReorder={handleReorderBacklog}
|
||||
onConvert={handleConvertBacklog}
|
||||
onBulkConvert={handleBulkConvertBacklog}
|
||||
onDelete={handleDeleteBacklog}
|
||||
busy={formBusy}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function PlanSprintInner() {
|
|||
reloadActors,
|
||||
handleCreateWorkCycle,
|
||||
handleActivateWorkCycle,
|
||||
handleCompleteWorkCycle,
|
||||
handleCreateAction,
|
||||
handleUpdateAction,
|
||||
handleQuickStatus,
|
||||
|
|
@ -62,6 +63,7 @@ function PlanSprintInner() {
|
|||
canManage={capabilities.has('kairo.milestone.manage')}
|
||||
onCreate={handleCreateWorkCycle}
|
||||
onActivate={handleActivateWorkCycle}
|
||||
onComplete={handleCompleteWorkCycle}
|
||||
busy={formBusy}
|
||||
/>
|
||||
{activeWorkCycle && (
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ function WorkSprintInner() {
|
|||
initiative,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
visibleActions,
|
||||
} = ops
|
||||
|
||||
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
||||
|
|
@ -66,16 +67,61 @@ function WorkSprintInner() {
|
|||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{!activeWorkCycle && isProductArchetype && (
|
||||
<section className="card">
|
||||
<EmptyState
|
||||
message="Kein aktiver Sprint — lege unter Plan → Sprint einen Sprint an und aktiviere ihn."
|
||||
<>
|
||||
<section className="card sprint-context-banner">
|
||||
<h2>Product-Ist (ohne aktiven Sprint)</h2>
|
||||
<p className="section-lead muted">
|
||||
Kein aktiver Sprint — Kairo steuert nach{' '}
|
||||
<strong>continuous_product</strong> (wirkungsvollster Schritt). Sprint-Planung
|
||||
unter{' '}
|
||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||
Plan → Sprint
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<NextActionWidget
|
||||
scope="initiative"
|
||||
initiativeId={initiativeId}
|
||||
items={steeringSnapshot?.next_actions}
|
||||
loading={steeringSnapshotLoading}
|
||||
embedded
|
||||
title="Nächster Schritt (Product)"
|
||||
subtitle="Empfehlung ohne Sprint-Zeitbox — committete Arbeitspakete im Vorhaben."
|
||||
/>
|
||||
<p>
|
||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||
Zur Sprint-Planung
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<InitiativeActionsHub
|
||||
initiativeId={initiativeId}
|
||||
actions={visibleActions}
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
actionContextById={actionContextById}
|
||||
hideDone={hideDone}
|
||||
onHideDoneChange={setHideDone}
|
||||
showForm={showActionForm}
|
||||
onToggleForm={() => setShowActionForm((v) => !v)}
|
||||
editingActionId={editingAction?.id}
|
||||
onEditAction={setEditingAction}
|
||||
onCancelEdit={() => setEditingAction(null)}
|
||||
onCreateAction={handleCreateAction}
|
||||
onUpdateAction={handleUpdateAction}
|
||||
onQuickStatus={handleQuickStatus}
|
||||
onCreateBlockerForAction={handleCreateBlockerForAction}
|
||||
canManage={capabilities.has('kairo.action.manage')}
|
||||
canManageBlocker={capabilities.has('kairo.blocker.manage')}
|
||||
formBusy={formBusy}
|
||||
actors={actors}
|
||||
actorsLoading={actorsLoading}
|
||||
actorsError={actorsError}
|
||||
actorsUsedFallback={actorsUsedFallback}
|
||||
onReloadActors={reloadActors}
|
||||
workCycles={ops.workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
sectionTitle="Committete Arbeitspakete"
|
||||
sectionLead="Ohne aktiven Sprint — alle offenen Arbeitspakete im Product-Vorhaben."
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeWorkCycle && (
|
||||
|
|
|
|||
|
|
@ -669,3 +669,38 @@
|
|||
border-color: var(--jk-primary);
|
||||
background: var(--jk-surface);
|
||||
}
|
||||
|
||||
.backlog-sprint-planning {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--jk-surface-muted, rgba(0, 0, 0, 0.03));
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.backlog-sprint-planning__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.backlog-sprint-planning__hint {
|
||||
flex: 1 1 100%;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.backlog-select-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.backlog-select-checkbox input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user