AP2.2d: B2b Product-Backlog und B3 Sprint-Backlog in UI.
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
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>
This commit is contained in:
parent
0b57e2ec75
commit
82a5b9adec
|
|
@ -34,6 +34,8 @@ class BacklogUpdateRequest(BaseModel):
|
|||
|
||||
class BacklogConvertRequest(BaseModel):
|
||||
assigned_actor_ids: list[str] = Field(default_factory=list)
|
||||
work_cycle_id: Optional[str] = None
|
||||
assign_active_sprint: bool = True
|
||||
|
||||
|
||||
@router.get("/{backlog_item_id}")
|
||||
|
|
@ -103,6 +105,8 @@ def convert_backlog_to_action(
|
|||
backlog_item_id=backlog_item_id,
|
||||
user_id=ctx.user_id,
|
||||
assigned_actor_ids=assigned,
|
||||
work_cycle_id=body.work_cycle_id,
|
||||
assign_active_sprint=body.assign_active_sprint,
|
||||
)
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
|
|
|
|||
|
|
@ -295,6 +295,8 @@ def convert_backlog_to_action(
|
|||
backlog_item_id: str,
|
||||
user_id: Optional[str] = None,
|
||||
assigned_actor_ids: Optional[list[str]] = None,
|
||||
work_cycle_id: Optional[str] = None,
|
||||
assign_active_sprint: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
existing = get_backlog_item(tenant_id=tenant_id, backlog_item_id=backlog_item_id)
|
||||
if not existing:
|
||||
|
|
@ -304,6 +306,16 @@ def convert_backlog_to_action(
|
|||
if existing["status"] not in ("accepted", "triaged", "new"):
|
||||
raise ValueError("Backlog-Item kann in diesem Status nicht konvertiert werden")
|
||||
|
||||
resolved_cycle_id = work_cycle_id
|
||||
if not resolved_cycle_id and assign_active_sprint:
|
||||
from services.work_cycle import get_active_work_cycle
|
||||
|
||||
active = get_active_work_cycle(
|
||||
tenant_id=tenant_id, initiative_id=existing["initiative_id"]
|
||||
)
|
||||
if active:
|
||||
resolved_cycle_id = active["id"]
|
||||
|
||||
action = create_action(
|
||||
tenant_id=tenant_id,
|
||||
initiative_id=existing["initiative_id"],
|
||||
|
|
@ -311,6 +323,7 @@ def convert_backlog_to_action(
|
|||
description=existing["description"] or "",
|
||||
priority=existing["priority"],
|
||||
roadmap_item_id=existing.get("roadmap_item_id"),
|
||||
work_cycle_id=resolved_cycle_id,
|
||||
assigned_actor_ids=assigned_actor_ids or [],
|
||||
user_id=user_id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,25 @@ def _resolve_method_key(ctx: TenantContext, initiative_id: str | None) -> str:
|
|||
return "generic_operating"
|
||||
|
||||
|
||||
def _resolve_next_action_strategy_key(
|
||||
ctx: TenantContext, initiative_id: str | None
|
||||
) -> str:
|
||||
method_key = _resolve_method_key(ctx, initiative_id)
|
||||
method = get_method(method_key)
|
||||
strategy_key = method.next_action_strategy_key if method else default_strategy.key
|
||||
if not initiative_id:
|
||||
return strategy_key
|
||||
|
||||
from services.work_cycle import get_active_work_cycle
|
||||
|
||||
active = get_active_work_cycle(tenant_id=ctx.tenant_id, initiative_id=initiative_id)
|
||||
if active and strategy_key == "continuous_product":
|
||||
agile = get_next_action_strategy("agile_iteration")
|
||||
if agile:
|
||||
return agile.key
|
||||
return strategy_key
|
||||
|
||||
|
||||
def evaluate(
|
||||
ctx: TenantContext,
|
||||
kind: SignalKind = "attention",
|
||||
|
|
@ -35,10 +54,6 @@ def evaluate(
|
|||
if kind == "attention":
|
||||
return default_rules.get_attention_items(ctx)
|
||||
|
||||
method_key = _resolve_method_key(ctx, initiative_id)
|
||||
method = get_method(method_key)
|
||||
strategy_key = (
|
||||
method.next_action_strategy_key if method else default_strategy.key
|
||||
)
|
||||
strategy_key = _resolve_next_action_strategy_key(ctx, initiative_id)
|
||||
strategy = get_next_action_strategy(strategy_key) or default_strategy
|
||||
return strategy.evaluate(ctx, initiative_id=initiative_id, limit=limit)
|
||||
|
|
|
|||
44
backend/tests/test_ap22d_sprint_backlog.py
Normal file
44
backend/tests/test_ap22d_sprint_backlog.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""AP2.2d — Backlog convert assigns active sprint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from tests.factories import provision_user_in_tenant
|
||||
from tests.test_initiatives_actions import _auth, _create_initiative, _login
|
||||
|
||||
|
||||
def test_convert_backlog_assigns_active_work_cycle(client):
|
||||
user = provision_user_in_tenant(tenant_role="admin")
|
||||
token = _login(client, user)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
token,
|
||||
title="Product Backlog",
|
||||
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),
|
||||
)
|
||||
assert cycle.status_code == 201
|
||||
cycle_id = cycle.json()["id"]
|
||||
|
||||
backlog = client.post(
|
||||
f"/api/initiatives/{initiative_id}/backlog",
|
||||
json={"title": "Feature X", "status": "accepted"},
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert backlog.status_code == 201
|
||||
backlog_id = backlog.json()["id"]
|
||||
|
||||
converted = client.post(
|
||||
f"/api/backlog/{backlog_id}/convert-to-action",
|
||||
json={"assign_active_sprint": True},
|
||||
headers=_auth(token),
|
||||
)
|
||||
assert converted.status_code == 201
|
||||
action = converted.json()["action"]
|
||||
assert action["work_cycle_id"] == cycle_id
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
APP_VERSION = "0.20.1-ap2.2bc"
|
||||
APP_VERSION = "0.20.2-ap2.2d"
|
||||
DB_SCHEMA_VERSION = "025"
|
||||
APP_NAME = "jinkendo-kairo"
|
||||
|
|
|
|||
|
|
@ -148,8 +148,8 @@ Verhindert, dass Zielbild-Dokumente als Ist-Stand gelesen werden.
|
|||
|----------|------------|------------------|-----------|--------|-------|
|
||||
| A1 Reifegrad | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2c + AP2.0e |
|
||||
| A2 Linear | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2b |
|
||||
| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d |
|
||||
| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d |
|
||||
| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
||||
| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
||||
| B2a Programm | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2e |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ✓
|
|||
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐ (Hints + Recurring)
|
||||
Phase 3 AP2.2b A2 Linear End-to-End ◐
|
||||
AP2.2c A1 Reifegrad + AP2.0e ◐ Code
|
||||
AP2.2d B2b Product + B3 Sprint
|
||||
AP2.2d B2b Product + B3 Sprint ✓
|
||||
AP2.2e B2a Programm (optional vor AP2.1)
|
||||
Phase 4 AP1.7b Operational API — Pflege-Parität
|
||||
Phase 5 AP2.1 Validation Report v0.3 (alle Stufe-A-Szenarien)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "kairo-jinkendo-frontend",
|
||||
"version": "0.20.1-ap2.2bc",
|
||||
"version": "0.20.2-ap2.2d",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function AppRoutes() {
|
|||
<Route path="/work" element={<WorkLayout />}>
|
||||
<Route index element={<WorkIndexRedirect />} />
|
||||
<Route path="today" element={<MODE_ROUTE_COMPONENTS.workToday />} />
|
||||
<Route path="sprint" element={<MODE_ROUTE_COMPONENTS.workSprint />} />
|
||||
<Route path="mine" element={<MODE_ROUTE_COMPONENTS.workMine />} />
|
||||
</Route>
|
||||
|
||||
|
|
@ -73,6 +74,7 @@ function AppRoutes() {
|
|||
<Route path="structure" element={<MODE_ROUTE_COMPONENTS.planStructure />} />
|
||||
<Route path="gates" element={<MODE_ROUTE_COMPONENTS.planGates />} />
|
||||
<Route path="inbox" element={<MODE_ROUTE_COMPONENTS.planInbox />} />
|
||||
<Route path="sprint" element={<MODE_ROUTE_COMPONENTS.planSprint />} />
|
||||
<Route path="work" element={<MODE_ROUTE_COMPONENTS.planWork />} />
|
||||
</Route>
|
||||
|
||||
|
|
|
|||
22
frontend/src/api/workCycles.js
Normal file
22
frontend/src/api/workCycles.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { apiFetch } from './client.js'
|
||||
|
||||
export function listInitiativeWorkCycles(initiativeId) {
|
||||
return apiFetch(`/api/initiatives/${initiativeId}/work-cycles`)
|
||||
}
|
||||
|
||||
export function getActiveWorkCycle(initiativeId) {
|
||||
return apiFetch(`/api/initiatives/${initiativeId}/work-cycles/active`)
|
||||
}
|
||||
|
||||
export function createWorkCycle(initiativeId, body) {
|
||||
return apiFetch(`/api/initiatives/${initiativeId}/work-cycles`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
}
|
||||
|
||||
export function activateWorkCycle(initiativeId, workCycleId) {
|
||||
return apiFetch(`/api/initiatives/${initiativeId}/work-cycles/${workCycleId}/activate`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ export function ActionForm({
|
|||
initial = {},
|
||||
projects = [],
|
||||
roadmapItems = [],
|
||||
workCycles = [],
|
||||
activeWorkCycle = null,
|
||||
actors = [],
|
||||
actorsLoading = false,
|
||||
actorsError = null,
|
||||
|
|
@ -37,6 +39,7 @@ export function ActionForm({
|
|||
due_at: form.due_at.value ? new Date(form.due_at.value).toISOString() : null,
|
||||
project_id: form.project_id?.value || undefined,
|
||||
roadmap_item_id: form.roadmap_item_id?.value || undefined,
|
||||
work_cycle_id: form.work_cycle_id?.value || undefined,
|
||||
assigned_actor_ids: selected,
|
||||
})
|
||||
}
|
||||
|
|
@ -44,6 +47,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 defaultCycleId =
|
||||
initial.work_cycle_id || (activeWorkCycle?.id && !initial.id ? activeWorkCycle.id : '')
|
||||
|
||||
return (
|
||||
<form className="form workspace-form" onSubmit={handleSubmit}>
|
||||
|
|
@ -100,6 +106,20 @@ export function ActionForm({
|
|||
roadmapItems={roadmapItems}
|
||||
defaultValue={initial.roadmap_item_id || ''}
|
||||
/>
|
||||
{cycleOptions.length > 0 && (
|
||||
<label>
|
||||
Sprint / Zeitbox (optional)
|
||||
<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)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<ActorSelect
|
||||
actors={actors}
|
||||
defaultSelected={defaultActors}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export function ActionHubCard({
|
|||
initiativeId,
|
||||
projects = [],
|
||||
roadmapItems = [],
|
||||
workCycles = [],
|
||||
activeWorkCycle = null,
|
||||
detailMode = false,
|
||||
}) {
|
||||
const due = formatDue(action.due_at)
|
||||
|
|
@ -66,6 +68,8 @@ export function ActionHubCard({
|
|||
initial={action}
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
actors={actors}
|
||||
actorsLoading={actorsLoading}
|
||||
actorsError={actorsError}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ const HINTS = {
|
|||
link: { to: '/plan/gates', label: 'Gate-Graph öffnen' },
|
||||
},
|
||||
'initiative.product': {
|
||||
text: 'Product: Eingang triagieren, committete Actions — optional Sprint-Zeitbox.',
|
||||
link: { to: '/plan/inbox', label: 'Zum Eingang' },
|
||||
text: 'Product: Eingang triagieren → committen in Sprint-Backlog (aktive Zeitbox).',
|
||||
link: { to: '/plan/sprint', label: 'Sprint planen' },
|
||||
secondaryLink: { to: '/work/sprint', label: 'Sprint ausführen' },
|
||||
},
|
||||
'initiative.program': {
|
||||
text: 'Programm: Gate-Horizont und Abschluss im Blick behalten.',
|
||||
|
|
@ -30,6 +31,14 @@ export function ArchetypeSteeringHints({ archetypeKey }) {
|
|||
<Link to={scopedPath(hint.link.to)} className="link-inline">
|
||||
{hint.link.label}
|
||||
</Link>
|
||||
{hint.secondaryLink && (
|
||||
<>
|
||||
{' · '}
|
||||
<Link to={scopedPath(hint.secondaryLink.to)} className="link-inline">
|
||||
{hint.secondaryLink.label}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export function BacklogSection({
|
|||
onConvert,
|
||||
onDelete,
|
||||
busy,
|
||||
sectionTitle = 'Product Backlog',
|
||||
sectionLead = 'Eingang vor dem Commit — triagieren, dann in Arbeitspaket (Sprint) umwandeln.',
|
||||
}) {
|
||||
const [modalMode, setModalMode] = useState(null)
|
||||
const [dragItemId, setDragItemId] = useState('')
|
||||
|
|
@ -106,10 +108,8 @@ export function BacklogSection({
|
|||
<section className="card backlog-section">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Backlog</h2>
|
||||
<p className="section-lead muted">
|
||||
Reihenfolge per Drag & Drop (Desktop) oder ↑/↓ (Mobile).
|
||||
</p>
|
||||
<h2>{sectionTitle}</h2>
|
||||
<p className="section-lead muted">{sectionLead}</p>
|
||||
</div>
|
||||
{canManage && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ export function InitiativeActionsHub({
|
|||
actions,
|
||||
projects = [],
|
||||
roadmapItems = [],
|
||||
workCycles = [],
|
||||
activeWorkCycle = null,
|
||||
actionContextById,
|
||||
hideDone,
|
||||
onHideDoneChange,
|
||||
|
|
@ -27,16 +29,15 @@ export function InitiativeActionsHub({
|
|||
actorsError,
|
||||
actorsUsedFallback,
|
||||
onReloadActors,
|
||||
sectionTitle = 'Arbeitspakete',
|
||||
sectionLead = 'Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews hängen am Arbeitspaket.',
|
||||
}) {
|
||||
return (
|
||||
<section className="card initiative-actions-hub">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Arbeitspakete</h2>
|
||||
<p className="section-lead muted">
|
||||
Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews
|
||||
hängen am Arbeitspaket.
|
||||
</p>
|
||||
<h2>{sectionTitle}</h2>
|
||||
<p className="section-lead muted">{sectionLead}</p>
|
||||
</div>
|
||||
<div className="section-actions">
|
||||
<label className="checkbox-label inline-filter">
|
||||
|
|
@ -64,6 +65,8 @@ export function InitiativeActionsHub({
|
|||
<ActionForm
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
actors={actors}
|
||||
actorsLoading={actorsLoading}
|
||||
actorsError={actorsError}
|
||||
|
|
@ -104,6 +107,8 @@ export function InitiativeActionsHub({
|
|||
initiativeId={initiativeId}
|
||||
projects={projects}
|
||||
roadmapItems={roadmapItems}
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ export function PlanActionsSection({
|
|||
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())
|
||||
|
|
@ -73,10 +75,8 @@ export function PlanActionsSection({
|
|||
<section className="card plan-actions-section">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Arbeit</h2>
|
||||
<p className="section-lead muted">
|
||||
Committete Arbeitspakete und zugehörige Aufgaben — Ausführung über die Objektseite.
|
||||
</p>
|
||||
<h2>{sectionTitle}</h2>
|
||||
<p className="section-lead muted">{sectionLead}</p>
|
||||
</div>
|
||||
<div className="section-actions">
|
||||
<label className="checkbox-label inline-filter">
|
||||
|
|
|
|||
119
frontend/src/components/WorkCyclesPanel.jsx
Normal file
119
frontend/src/components/WorkCyclesPanel.jsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { useState } from 'react'
|
||||
import { StatusBadge } from './StatusBadge.jsx'
|
||||
import { EmptyState } from './EmptyState.jsx'
|
||||
|
||||
export function WorkCyclesPanel({
|
||||
workCycles = [],
|
||||
activeWorkCycle = null,
|
||||
canManage,
|
||||
onCreate,
|
||||
onActivate,
|
||||
busy = false,
|
||||
}) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [goal, setGoal] = useState('')
|
||||
const [activateOnCreate, setActivateOnCreate] = useState(true)
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
const trimmed = title.trim()
|
||||
if (!trimmed) return
|
||||
await onCreate({
|
||||
title: trimmed,
|
||||
goal_description: goal.trim(),
|
||||
status: activateOnCreate ? 'active' : 'planned',
|
||||
})
|
||||
setTitle('')
|
||||
setGoal('')
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card work-cycles-panel">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Sprint / Zeitbox</h2>
|
||||
<p className="section-lead muted">
|
||||
Aktive Zeitbox definiert den Sprint-Backlog — committete Actions mit Zeitbox-Zuweisung
|
||||
erscheinen unter Ausführen → Sprint.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeWorkCycle ? (
|
||||
<p className="work-cycle-active-banner">
|
||||
Aktiv: <strong>{activeWorkCycle.title}</strong>
|
||||
{activeWorkCycle.goal_description && (
|
||||
<span className="muted"> — {activeWorkCycle.goal_description}</span>
|
||||
)}
|
||||
{typeof activeWorkCycle.open_action_count === 'number' && (
|
||||
<span className="muted"> ({activeWorkCycle.open_action_count} offene APs)</span>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="muted">Keine aktive Zeitbox — alle committeten Actions gelten als Product-Ist.</p>
|
||||
)}
|
||||
|
||||
{canManage && (
|
||||
<form className="form inline-form-block" onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Titel
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="z. B. Iteration R1"
|
||||
maxLength={255}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Ziel (optional)
|
||||
<input
|
||||
value={goal}
|
||||
onChange={(e) => setGoal(e.target.value)}
|
||||
placeholder="Sprint-Ziel"
|
||||
/>
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={activateOnCreate}
|
||||
onChange={(e) => setActivateOnCreate(e.target.checked)}
|
||||
/>
|
||||
Sofort aktivieren
|
||||
</label>
|
||||
<button type="submit" className="btn btn-primary" disabled={busy}>
|
||||
{busy ? 'Anlegen …' : 'Zeitbox anlegen'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{workCycles.length === 0 ? (
|
||||
<EmptyState message="Noch keine Zeitboxen angelegt." />
|
||||
) : (
|
||||
<ul className="item-list">
|
||||
{workCycles.map((cycle) => (
|
||||
<li key={cycle.id} className="list-item card-list-item work-cycle-row">
|
||||
<div>
|
||||
<strong>{cycle.title}</strong>{' '}
|
||||
<StatusBadge status={cycle.status} />
|
||||
{cycle.goal_description && (
|
||||
<p className="muted item-meta">{cycle.goal_description}</p>
|
||||
)}
|
||||
</div>
|
||||
{canManage && cycle.status !== 'active' && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-sm"
|
||||
disabled={busy}
|
||||
onClick={() => onActivate(cycle.id)}
|
||||
>
|
||||
Aktivieren
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ describe('controlNav', () => {
|
|||
|
||||
describe('workNav', () => {
|
||||
it('resolves active work sub-route', () => {
|
||||
expect(resolveWorkNavActiveKey('/work/sprint')).toBe('sprint')
|
||||
expect(resolveWorkNavActiveKey('/work/today')).toBe('today')
|
||||
expect(resolveWorkNavActiveKey('/work/mine')).toBe('mine')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
/** @type {ModeNavItem[]} */
|
||||
export const WORK_NAV_ITEMS = [
|
||||
{ key: 'sprint', to: '/work/sprint', label: 'Sprint' },
|
||||
{ key: 'today', to: '/work/today', label: 'Heute' },
|
||||
{ key: 'mine', to: '/work/mine', label: 'Meine Queue' },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@ import {
|
|||
updateInitiativeSteeringMethod,
|
||||
updateInitiativeMethodProfile,
|
||||
} from '../api/steering.js'
|
||||
import {
|
||||
listInitiativeWorkCycles,
|
||||
getActiveWorkCycle,
|
||||
createWorkCycle,
|
||||
activateWorkCycle,
|
||||
} from '../api/workCycles.js'
|
||||
import { useCapabilities } from '../hooks/useCapabilities.js'
|
||||
import { useActors } from '../hooks/useActors.js'
|
||||
import { useSession } from './SessionContext.jsx'
|
||||
|
|
@ -98,6 +104,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
const [decisions, setDecisions] = useState([])
|
||||
const [reviews, setReviews] = useState([])
|
||||
const [recurringItems, setRecurringItems] = useState([])
|
||||
const [workCycles, setWorkCycles] = useState([])
|
||||
const [activeWorkCycle, setActiveWorkCycle] = useState(null)
|
||||
const [steeringSnapshot, setSteeringSnapshot] = useState(null)
|
||||
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
|
||||
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
|
||||
|
|
@ -135,6 +143,12 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
|
||||
loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([])))
|
||||
loads.push(listInitiativeRecurring(id).then(setRecurringItems).catch(() => setRecurringItems([])))
|
||||
loads.push(listInitiativeWorkCycles(id).then(setWorkCycles).catch(() => setWorkCycles([])))
|
||||
loads.push(
|
||||
getActiveWorkCycle(id)
|
||||
.then((item) => setActiveWorkCycle(item || null))
|
||||
.catch(() => setActiveWorkCycle(null))
|
||||
)
|
||||
loads.push(
|
||||
listSteeringMethods().then(setSteeringMethods).catch(() => setSteeringMethods([]))
|
||||
)
|
||||
|
|
@ -186,6 +200,20 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
[actions, hideDone, selectedProjectId, projects]
|
||||
)
|
||||
|
||||
const sprintActions = useMemo(() => {
|
||||
if (!activeWorkCycle?.id) return []
|
||||
const cycleId = activeWorkCycle.id
|
||||
let list = hideDone
|
||||
? actions.filter((a) => a.status !== 'done' && a.status !== 'discarded')
|
||||
: actions
|
||||
list = list.filter((a) => a.work_cycle_id === cycleId)
|
||||
if (selectedProjectId) {
|
||||
const subtreeIds = collectProjectSubtreeIds(projects, selectedProjectId)
|
||||
list = list.filter((a) => a.project_id && subtreeIds.has(a.project_id))
|
||||
}
|
||||
return list
|
||||
}, [actions, hideDone, activeWorkCycle?.id, selectedProjectId, projects])
|
||||
|
||||
const actionContextById = useMemo(
|
||||
() => Object.fromEntries((steeringSnapshot?.actions || []).map((a) => [a.id, a])),
|
||||
[steeringSnapshot]
|
||||
|
|
@ -257,6 +285,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
clear_project: payload.project_id === '' || payload.project_id === null,
|
||||
roadmap_item_id: payload.roadmap_item_id || undefined,
|
||||
clear_roadmap_item: payload.roadmap_item_id === '' || payload.roadmap_item_id === null,
|
||||
work_cycle_id: payload.work_cycle_id || undefined,
|
||||
clear_work_cycle: payload.work_cycle_id === '' || payload.work_cycle_id === null,
|
||||
})
|
||||
if (capabilities.has('kairo.action.manage')) {
|
||||
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
|
||||
|
|
@ -393,7 +423,31 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
async function handleConvertBacklog(itemId) {
|
||||
setFormBusy(true)
|
||||
try {
|
||||
await convertBacklogToAction(itemId)
|
||||
await convertBacklogToAction(itemId, { assign_active_sprint: true })
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setFormBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateWorkCycle(body) {
|
||||
setFormBusy(true)
|
||||
try {
|
||||
await createWorkCycle(id, body)
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setFormBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleActivateWorkCycle(workCycleId) {
|
||||
setFormBusy(true)
|
||||
try {
|
||||
await activateWorkCycle(id, workCycleId)
|
||||
await load()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
|
|
@ -727,6 +781,9 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
decisions,
|
||||
reviews,
|
||||
recurringItems,
|
||||
workCycles,
|
||||
activeWorkCycle,
|
||||
sprintActions,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
steeringSnapshotError,
|
||||
|
|
@ -764,6 +821,8 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
|||
handleBacklogGate,
|
||||
handleBacklogStatus,
|
||||
handleConvertBacklog,
|
||||
handleCreateWorkCycle,
|
||||
handleActivateWorkCycle,
|
||||
handleDeleteBacklog,
|
||||
handleCreateRoadmapItem,
|
||||
handleRoadmapItemStatus,
|
||||
|
|
|
|||
111
frontend/src/pages/modes/PlanSprintPage.jsx
Normal file
111
frontend/src/pages/modes/PlanSprintPage.jsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { useMemo } from 'react'
|
||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||
import { WorkCyclesPanel } from '../../components/WorkCyclesPanel.jsx'
|
||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
|
||||
function PlanSprintInner() {
|
||||
const ops = useInitiativeOperations()
|
||||
const {
|
||||
initiativeId,
|
||||
activeWorkCycle,
|
||||
workCycles,
|
||||
sprintActions,
|
||||
projects,
|
||||
roadmapItems,
|
||||
capabilities,
|
||||
hideDone,
|
||||
setHideDone,
|
||||
loading,
|
||||
error,
|
||||
formBusy,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingAction,
|
||||
setEditingAction,
|
||||
actionContextById,
|
||||
actors,
|
||||
actorsLoading,
|
||||
actorsError,
|
||||
actorsUsedFallback,
|
||||
reloadActors,
|
||||
handleCreateWorkCycle,
|
||||
handleActivateWorkCycle,
|
||||
handleCreateAction,
|
||||
handleUpdateAction,
|
||||
handleQuickStatus,
|
||||
handleCreateBlockerForAction,
|
||||
} = ops
|
||||
|
||||
const createSprintAction = useMemo(
|
||||
() => async (payload) => {
|
||||
await handleCreateAction({
|
||||
...payload,
|
||||
work_cycle_id: payload.work_cycle_id || activeWorkCycle?.id || undefined,
|
||||
})
|
||||
},
|
||||
[handleCreateAction, activeWorkCycle?.id],
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Lade Sprint-Planung …" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<WorkCyclesPanel
|
||||
workCycles={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
canManage={capabilities.has('kairo.milestone.manage')}
|
||||
onCreate={handleCreateWorkCycle}
|
||||
onActivate={handleActivateWorkCycle}
|
||||
busy={formBusy}
|
||||
/>
|
||||
{activeWorkCycle && (
|
||||
<InitiativeActionsHub
|
||||
initiativeId={initiativeId}
|
||||
actions={sprintActions}
|
||||
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={createSprintAction}
|
||||
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={workCycles}
|
||||
activeWorkCycle={activeWorkCycle}
|
||||
sectionTitle="Sprint-Backlog"
|
||||
sectionLead="Arbeitspakete in der aktiven Zeitbox — committet aus dem Product Backlog (Eingang) oder direkt hier."
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function PlanSprintPage() {
|
||||
return (
|
||||
<RequireInitiativeScope lead="Sprint-Zeitboxen gehören zu einem Vorhaben im Scope.">
|
||||
<ScopedInitiativeProvider>
|
||||
<PlanSprintInner />
|
||||
</ScopedInitiativeProvider>
|
||||
</RequireInitiativeScope>
|
||||
)
|
||||
}
|
||||
|
|
@ -75,6 +75,8 @@ function PlanWorkInner() {
|
|||
onReloadActors={reloadActors}
|
||||
busy={formBusy}
|
||||
executionGraph={executionGraph}
|
||||
sectionTitle="Alle Arbeitspakete"
|
||||
sectionLead="Gesamt-Ist am Vorhaben — Sprint-fokussierte Arbeit unter Ausführen → Sprint."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { Navigate, Outlet } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
||||
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
|
||||
import { ModeShell } from '../../components/ModeShell.jsx'
|
||||
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { getActiveWorkCycle } from '../../api/workCycles.js'
|
||||
import { getInitiative } from '../../api/initiatives.js'
|
||||
|
||||
export function WorkLayout() {
|
||||
return (
|
||||
|
|
@ -24,5 +28,34 @@ export function WorkLayout() {
|
|||
}
|
||||
|
||||
export function WorkIndexRedirect() {
|
||||
return <Navigate to="/work/today" replace />
|
||||
const { initiativeId } = useProgramScope()
|
||||
const [target, setTarget] = useState('/work/today')
|
||||
|
||||
useEffect(() => {
|
||||
if (!initiativeId) {
|
||||
setTarget('/work/today')
|
||||
return undefined
|
||||
}
|
||||
let cancelled = false
|
||||
Promise.all([getInitiative(initiativeId), getActiveWorkCycle(initiativeId)])
|
||||
.then(([initiative, activeCycle]) => {
|
||||
if (cancelled) return
|
||||
if (
|
||||
initiative?.archetype_key === 'initiative.product' &&
|
||||
activeCycle?.id
|
||||
) {
|
||||
setTarget(`/work/sprint?initiative=${initiativeId}`)
|
||||
} else {
|
||||
setTarget(`/work/today?initiative=${initiativeId}`)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setTarget('/work/today')
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initiativeId])
|
||||
|
||||
return <Navigate to={target} replace />
|
||||
}
|
||||
|
|
|
|||
148
frontend/src/pages/modes/WorkSprintPage.jsx
Normal file
148
frontend/src/pages/modes/WorkSprintPage.jsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||
import { InitiativeActionsHub } from '../../components/InitiativeActionsHub.jsx'
|
||||
import { NextActionWidget } from '../../widgets/NextActionWidget.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { EmptyState } from '../../components/EmptyState.jsx'
|
||||
import { useInitiativeOperations } from '../../context/InitiativeOperationsContext.jsx'
|
||||
import { useProgramScope } from '../../context/ProgramScopeContext.jsx'
|
||||
import { scopedPath } from '../../utils/routes.js'
|
||||
|
||||
function WorkSprintInner() {
|
||||
const { initiativeId, hrefWithScope } = useProgramScope()
|
||||
const ops = useInitiativeOperations()
|
||||
const {
|
||||
activeWorkCycle,
|
||||
sprintActions,
|
||||
projects,
|
||||
roadmapItems,
|
||||
capabilities,
|
||||
hideDone,
|
||||
setHideDone,
|
||||
loading,
|
||||
error,
|
||||
formBusy,
|
||||
showActionForm,
|
||||
setShowActionForm,
|
||||
editingAction,
|
||||
setEditingAction,
|
||||
actionContextById,
|
||||
actors,
|
||||
actorsLoading,
|
||||
actorsError,
|
||||
actorsUsedFallback,
|
||||
reloadActors,
|
||||
handleCreateAction,
|
||||
handleUpdateAction,
|
||||
handleQuickStatus,
|
||||
handleCreateBlockerForAction,
|
||||
initiative,
|
||||
steeringSnapshot,
|
||||
steeringSnapshotLoading,
|
||||
} = ops
|
||||
|
||||
const defaultWorkCycleId = activeWorkCycle?.id || ''
|
||||
|
||||
const createAction = useMemo(
|
||||
() => async (payload) => {
|
||||
await handleCreateAction({
|
||||
...payload,
|
||||
work_cycle_id: payload.work_cycle_id || defaultWorkCycleId || undefined,
|
||||
})
|
||||
},
|
||||
[handleCreateAction, defaultWorkCycleId],
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return <LoadingState message="Lade Sprint-Backlog …" />
|
||||
}
|
||||
|
||||
const isProductArchetype = initiative?.archetype_key === 'initiative.product'
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{!activeWorkCycle && isProductArchetype && (
|
||||
<section className="card">
|
||||
<EmptyState
|
||||
message="Kein aktiver Sprint — lege unter Plan → Sprint eine Zeitbox an und aktiviere sie."
|
||||
/>
|
||||
<p>
|
||||
<Link to={hrefWithScope('/plan/sprint')} className="link-inline">
|
||||
Zur Sprint-Planung
|
||||
</Link>
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeWorkCycle && (
|
||||
<>
|
||||
<section className="card sprint-context-banner">
|
||||
<h2>Sprint-Backlog</h2>
|
||||
<p className="section-lead muted">
|
||||
<strong>{activeWorkCycle.title}</strong>
|
||||
{activeWorkCycle.goal_description && ` — ${activeWorkCycle.goal_description}`}
|
||||
{' · '}
|
||||
Product Backlog triagierst du unter{' '}
|
||||
<Link to={scopedPath('/plan/inbox', { initiativeId })} className="link-inline">
|
||||
Plan → Eingang
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<NextActionWidget
|
||||
scope="initiative"
|
||||
initiativeId={initiativeId}
|
||||
items={steeringSnapshot?.next_actions}
|
||||
loading={steeringSnapshotLoading}
|
||||
embedded
|
||||
title="Nächster Schritt im Sprint"
|
||||
/>
|
||||
|
||||
<InitiativeActionsHub
|
||||
initiativeId={initiativeId}
|
||||
actions={sprintActions}
|
||||
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={createAction}
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function WorkSprintPage() {
|
||||
return (
|
||||
<RequireInitiativeScope lead="Sprint-Backlog ist vorhabenspezifisch — wähle ein Product-Vorhaben im Scope.">
|
||||
<ScopedInitiativeProvider>
|
||||
<WorkSprintInner />
|
||||
</ScopedInitiativeProvider>
|
||||
</RequireInitiativeScope>
|
||||
)
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ export const PLAN_OUTLINE_NODES = [
|
|||
{ key: 'structure', to: '/plan/structure', label: 'Struktur', requiresInitiative: true },
|
||||
{ key: 'gates', to: '/plan/gates', label: 'Zielzustände', requiresInitiative: true },
|
||||
{ key: 'inbox', to: '/plan/inbox', label: 'Eingang', requiresInitiative: true },
|
||||
{ key: 'sprint', to: '/plan/sprint', label: 'Sprint', requiresInitiative: true },
|
||||
{ key: 'work', to: '/plan/work', label: 'Arbeit', requiresInitiative: true },
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ describe('planOutlineNodes', () => {
|
|||
'structure',
|
||||
'gates',
|
||||
'inbox',
|
||||
'sprint',
|
||||
'work',
|
||||
])
|
||||
})
|
||||
|
|
@ -22,6 +23,7 @@ describe('planOutlineNodes', () => {
|
|||
expect(resolvePlanOutlineActiveKey('/plan/structure')).toBe('structure')
|
||||
expect(resolvePlanOutlineActiveKey('/plan/gates')).toBe('gates')
|
||||
expect(resolvePlanOutlineActiveKey('/plan/inbox')).toBe('inbox')
|
||||
expect(resolvePlanOutlineActiveKey('/plan/sprint')).toBe('sprint')
|
||||
expect(resolvePlanOutlineActiveKey('/plan/work')).toBe('work')
|
||||
expect(resolvePlanOutlineActiveKey('/plan/portfolio')).toBe(null)
|
||||
expect(resolvePlanOutlineActiveKey('/projects/abc-123')).toBe('structure')
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import { CockpitPage } from '../pages/modes/CockpitPage.jsx'
|
|||
import { WorkLayout, WorkIndexRedirect } from '../pages/modes/WorkLayout.jsx'
|
||||
import { WorkTodayPage } from '../pages/modes/WorkTodayPage.jsx'
|
||||
import { WorkMinePage } from '../pages/modes/WorkMinePage.jsx'
|
||||
import { WorkSprintPage } from '../pages/modes/WorkSprintPage.jsx'
|
||||
import { PlanLayout, PlanIndexRedirect } from '../pages/modes/PlanLayout.jsx'
|
||||
import { PlanStructurePage } from '../pages/modes/PlanStructurePage.jsx'
|
||||
import { PlanGatesPage } from '../pages/modes/PlanGatesPage.jsx'
|
||||
import { PlanInboxPage } from '../pages/modes/PlanInboxPage.jsx'
|
||||
import { PlanWorkPage } from '../pages/modes/PlanWorkPage.jsx'
|
||||
import { PlanSprintPage } from '../pages/modes/PlanSprintPage.jsx'
|
||||
import { PlanProfilePage } from '../pages/modes/PlanProfilePage.jsx'
|
||||
import { PlanPortfolioPage } from '../pages/modes/PlanPortfolioPage.jsx'
|
||||
import { ControlLayout, ControlIndexRedirect } from '../pages/modes/ControlLayout.jsx'
|
||||
|
|
@ -129,11 +131,13 @@ export function getViewByKey(key) {
|
|||
export const MODE_ROUTE_COMPONENTS = {
|
||||
workToday: WorkTodayPage,
|
||||
workMine: WorkMinePage,
|
||||
workSprint: WorkSprintPage,
|
||||
planPortfolio: PlanPortfolioPage,
|
||||
planProfile: PlanProfilePage,
|
||||
planStructure: PlanStructurePage,
|
||||
planGates: PlanGatesPage,
|
||||
planInbox: PlanInboxPage,
|
||||
planSprint: PlanSprintPage,
|
||||
planWork: PlanWorkPage,
|
||||
controlStatus: ControlStatusPage,
|
||||
controlPlanIst: ControlPlanIstPage,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user