Compare commits
No commits in common. "82a5b9adeccf9e7e191746345f4e9ddf54901f90" and "2b0d5dd3d90b7faf479877f700441dbb25fa2c4a" have entirely different histories.
82a5b9adec
...
2b0d5dd3d9
|
|
@ -20,11 +20,6 @@ class ServiceTokenCreateRequest(BaseModel):
|
||||||
expires_in_days: Optional[int] = Field(default=365, ge=1, le=730)
|
expires_in_days: Optional[int] = Field(default=365, ge=1, le=730)
|
||||||
|
|
||||||
|
|
||||||
class ActorCreateRequest(BaseModel):
|
|
||||||
name: str = Field(min_length=1, max_length=255)
|
|
||||||
actor_type: Literal["human", "agent", "working_group", "external_system"] = "agent"
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def list_actors(
|
def list_actors(
|
||||||
actor_type: Optional[Literal["human", "agent", "working_group", "external_system"]] = None,
|
actor_type: Optional[Literal["human", "agent", "working_group", "external_system"]] = None,
|
||||||
|
|
@ -43,26 +38,6 @@ def list_actors(
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.post("", status_code=201)
|
|
||||||
def create_actor(
|
|
||||||
body: ActorCreateRequest,
|
|
||||||
ctx: TenantContext = Depends(require_capability("kairo.actor.manage")),
|
|
||||||
):
|
|
||||||
if body.actor_type == "human":
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Human-Actors werden über User-Provisioning angelegt",
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return actor_service.create_actor(
|
|
||||||
tenant_id=ctx.tenant_id,
|
|
||||||
actor_type=body.actor_type,
|
|
||||||
name=body.name.strip(),
|
|
||||||
)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{actor_id}")
|
@router.get("/{actor_id}")
|
||||||
def get_actor(
|
def get_actor(
|
||||||
actor_id: str,
|
actor_id: str,
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,6 @@ class BacklogUpdateRequest(BaseModel):
|
||||||
|
|
||||||
class BacklogConvertRequest(BaseModel):
|
class BacklogConvertRequest(BaseModel):
|
||||||
assigned_actor_ids: list[str] = Field(default_factory=list)
|
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}")
|
@router.get("/{backlog_item_id}")
|
||||||
|
|
@ -105,8 +103,6 @@ def convert_backlog_to_action(
|
||||||
backlog_item_id=backlog_item_id,
|
backlog_item_id=backlog_item_id,
|
||||||
user_id=ctx.user_id,
|
user_id=ctx.user_id,
|
||||||
assigned_actor_ids=assigned,
|
assigned_actor_ids=assigned,
|
||||||
work_cycle_id=body.work_cycle_id,
|
|
||||||
assign_active_sprint=body.assign_active_sprint,
|
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
detail = str(exc)
|
detail = str(exc)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from entity_archetypes import resolve_default_method_key
|
from entity_archetypes import resolve_default_method_key
|
||||||
|
|
@ -223,7 +222,6 @@ def _apply_a1(
|
||||||
title="Tägliche Übung",
|
title="Tägliche Übung",
|
||||||
description="Regelmäßige Übung für die aktuelle Reifegrad-Stufe",
|
description="Regelmäßige Übung für die aktuelle Reifegrad-Stufe",
|
||||||
interval_days=1,
|
interval_days=1,
|
||||||
next_due_at=datetime.now(timezone.utc),
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,8 +295,6 @@ def convert_backlog_to_action(
|
||||||
backlog_item_id: str,
|
backlog_item_id: str,
|
||||||
user_id: Optional[str] = None,
|
user_id: Optional[str] = None,
|
||||||
assigned_actor_ids: Optional[list[str]] = None,
|
assigned_actor_ids: Optional[list[str]] = None,
|
||||||
work_cycle_id: Optional[str] = None,
|
|
||||||
assign_active_sprint: bool = True,
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
existing = get_backlog_item(tenant_id=tenant_id, backlog_item_id=backlog_item_id)
|
existing = get_backlog_item(tenant_id=tenant_id, backlog_item_id=backlog_item_id)
|
||||||
if not existing:
|
if not existing:
|
||||||
|
|
@ -306,16 +304,6 @@ def convert_backlog_to_action(
|
||||||
if existing["status"] not in ("accepted", "triaged", "new"):
|
if existing["status"] not in ("accepted", "triaged", "new"):
|
||||||
raise ValueError("Backlog-Item kann in diesem Status nicht konvertiert werden")
|
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(
|
action = create_action(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
initiative_id=existing["initiative_id"],
|
initiative_id=existing["initiative_id"],
|
||||||
|
|
@ -323,7 +311,6 @@ def convert_backlog_to_action(
|
||||||
description=existing["description"] or "",
|
description=existing["description"] or "",
|
||||||
priority=existing["priority"],
|
priority=existing["priority"],
|
||||||
roadmap_item_id=existing.get("roadmap_item_id"),
|
roadmap_item_id=existing.get("roadmap_item_id"),
|
||||||
work_cycle_id=resolved_cycle_id,
|
|
||||||
assigned_actor_ids=assigned_actor_ids or [],
|
assigned_actor_ids=assigned_actor_ids or [],
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
"""AP2.0e — Recurring transition when a maturity_stage is reached."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
from services.recurring import (
|
|
||||||
create_recurring_element,
|
|
||||||
list_recurring_for_initiative,
|
|
||||||
update_recurring_element,
|
|
||||||
)
|
|
||||||
from services.roadmap import get_roadmap_item, list_roadmap_items_for_initiative, update_roadmap_item
|
|
||||||
|
|
||||||
|
|
||||||
def on_maturity_stage_reached(
|
|
||||||
*,
|
|
||||||
tenant_id: str,
|
|
||||||
initiative_id: str,
|
|
||||||
reached_item_id: str,
|
|
||||||
user_id: Optional[str] = None,
|
|
||||||
) -> Optional[dict[str, Any]]:
|
|
||||||
"""Pause old routines, activate next stage, seed new routine for next stage."""
|
|
||||||
item = get_roadmap_item(tenant_id=tenant_id, item_id=reached_item_id)
|
|
||||||
if not item or item.get("item_type") != "maturity_stage":
|
|
||||||
return None
|
|
||||||
|
|
||||||
stages = [
|
|
||||||
s
|
|
||||||
for s in list_roadmap_items_for_initiative(
|
|
||||||
tenant_id=tenant_id, initiative_id=initiative_id
|
|
||||||
)
|
|
||||||
if s.get("item_type") == "maturity_stage"
|
|
||||||
]
|
|
||||||
stages.sort(key=lambda s: (s.get("sort_order", 0), s.get("title", "")))
|
|
||||||
|
|
||||||
reached_index = next(
|
|
||||||
(index for index, stage in enumerate(stages) if stage["id"] == reached_item_id),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if reached_index is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
next_stage = None
|
|
||||||
for stage in stages[reached_index + 1 :]:
|
|
||||||
if stage.get("status") in {"planned", "active", "at_risk"}:
|
|
||||||
next_stage = stage
|
|
||||||
break
|
|
||||||
|
|
||||||
paused: list[str] = []
|
|
||||||
for recurring in list_recurring_for_initiative(
|
|
||||||
tenant_id=tenant_id, initiative_id=initiative_id
|
|
||||||
):
|
|
||||||
if recurring.get("status") == "active":
|
|
||||||
update_recurring_element(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
recurring_id=recurring["id"],
|
|
||||||
status="paused",
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
paused.append(recurring["title"])
|
|
||||||
|
|
||||||
created_recurring = None
|
|
||||||
if next_stage:
|
|
||||||
update_roadmap_item(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
item_id=next_stage["id"],
|
|
||||||
status="active",
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
created_recurring = create_recurring_element(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
initiative_id=initiative_id,
|
|
||||||
title=f"Übung — {next_stage['title']}",
|
|
||||||
description=f"Routine für {next_stage['title']} (automatisch nach Stufenübergang)",
|
|
||||||
status="active",
|
|
||||||
interval_days=1,
|
|
||||||
next_due_at=datetime.now(timezone.utc),
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"reached_stage_id": reached_item_id,
|
|
||||||
"next_stage_id": next_stage["id"] if next_stage else None,
|
|
||||||
"paused_recurring": paused,
|
|
||||||
"new_recurring_id": created_recurring["id"] if created_recurring else None,
|
|
||||||
}
|
|
||||||
|
|
@ -708,17 +708,6 @@ def verify_reached(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
details={"roadmap_item_id": item_id, "verify_reason": verify_reason},
|
details={"roadmap_item_id": item_id, "verify_reason": verify_reason},
|
||||||
)
|
)
|
||||||
if row.get("item_type") == "maturity_stage":
|
|
||||||
from services.maturity_stage_transition import on_maturity_stage_reached
|
|
||||||
|
|
||||||
transition = on_maturity_stage_reached(
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
initiative_id=initiative_id,
|
|
||||||
reached_item_id=item_id,
|
|
||||||
user_id=user_id,
|
|
||||||
)
|
|
||||||
if transition:
|
|
||||||
row["maturity_transition"] = transition
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,25 +25,6 @@ def _resolve_method_key(ctx: TenantContext, initiative_id: str | None) -> str:
|
||||||
return "generic_operating"
|
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(
|
def evaluate(
|
||||||
ctx: TenantContext,
|
ctx: TenantContext,
|
||||||
kind: SignalKind = "attention",
|
kind: SignalKind = "attention",
|
||||||
|
|
@ -54,6 +35,10 @@ def evaluate(
|
||||||
if kind == "attention":
|
if kind == "attention":
|
||||||
return default_rules.get_attention_items(ctx)
|
return default_rules.get_attention_items(ctx)
|
||||||
|
|
||||||
strategy_key = _resolve_next_action_strategy_key(ctx, initiative_id)
|
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 = get_next_action_strategy(strategy_key) or default_strategy
|
strategy = get_next_action_strategy(strategy_key) or default_strategy
|
||||||
return strategy.evaluate(ctx, initiative_id=initiative_id, limit=limit)
|
return strategy.evaluate(ctx, initiative_id=initiative_id, limit=limit)
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@ from steering.strategies.next_action.execution_ready import (
|
||||||
from steering.strategies.next_action.product_milestone_driven import (
|
from steering.strategies.next_action.product_milestone_driven import (
|
||||||
ProductMilestoneDrivenStrategy,
|
ProductMilestoneDrivenStrategy,
|
||||||
)
|
)
|
||||||
from steering.strategies.next_action.recurring_helpers import (
|
|
||||||
active_recurring_candidates,
|
|
||||||
recurring_due_candidates,
|
|
||||||
)
|
|
||||||
from steering.strategies.next_action.registry import (
|
from steering.strategies.next_action.registry import (
|
||||||
get_next_action_strategy,
|
get_next_action_strategy,
|
||||||
register_next_action_strategy,
|
register_next_action_strategy,
|
||||||
|
|
@ -43,37 +39,26 @@ class MaturityProgressionStrategy:
|
||||||
if not initiative_id:
|
if not initiative_id:
|
||||||
return _default.evaluate(ctx, limit=limit)
|
return _default.evaluate(ctx, limit=limit)
|
||||||
|
|
||||||
recurring = recurring_due_candidates(ctx, initiative_id, limit=limit)
|
|
||||||
if not recurring:
|
|
||||||
recurring = active_recurring_candidates(ctx, initiative_id, limit=1)
|
|
||||||
|
|
||||||
remaining = limit - len(recurring)
|
|
||||||
gate_scope = first_active_gate_id(ctx, initiative_id)
|
gate_scope = first_active_gate_id(ctx, initiative_id)
|
||||||
ready: list[dict[str, Any]] = []
|
|
||||||
if remaining > 0:
|
|
||||||
ready = list_execution_ready_candidates(
|
ready = list_execution_ready_candidates(
|
||||||
ctx,
|
ctx,
|
||||||
initiative_id,
|
initiative_id,
|
||||||
limit=remaining,
|
limit=limit,
|
||||||
scope_roadmap_item_id=gate_scope,
|
scope_roadmap_item_id=gate_scope,
|
||||||
prefer_critical_path=True,
|
prefer_critical_path=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
merged = merge_candidates(recurring, ready, limit=limit)
|
remaining = limit - len(ready)
|
||||||
if len(merged) >= limit:
|
|
||||||
return merged[:limit]
|
|
||||||
|
|
||||||
gate_items: list[dict[str, Any]] = []
|
gate_items: list[dict[str, Any]] = []
|
||||||
rest_limit = limit - len(merged)
|
if remaining > 0:
|
||||||
if rest_limit > 0:
|
|
||||||
gate_items = _milestone.evaluate(
|
gate_items = _milestone.evaluate(
|
||||||
ctx, initiative_id=initiative_id, limit=rest_limit
|
ctx, initiative_id=initiative_id, limit=remaining
|
||||||
)
|
)
|
||||||
gate_items = [
|
gate_items = [
|
||||||
item for item in gate_items if item.get("kind") == "review_milestone"
|
item for item in gate_items if item.get("kind") == "review_milestone"
|
||||||
]
|
]
|
||||||
|
|
||||||
merged = merge_candidates(merged, gate_items, limit=limit)
|
merged = merge_candidates(ready, gate_items, limit=limit)
|
||||||
if len(merged) >= limit:
|
if len(merged) >= limit:
|
||||||
return merged[:limit]
|
return merged[:limit]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from steering.strategies.next_action.recurring_helpers import recurring_due_candidates
|
from db import get_connection
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
from steering.signals import default_rules
|
from steering.signals import default_rules
|
||||||
from steering.strategies.next_action.default_strategy import DefaultNextActionStrategy
|
from steering.strategies.next_action.default_strategy import DefaultNextActionStrategy
|
||||||
from steering.strategies.next_action.execution_ready import (
|
from steering.strategies.next_action.execution_ready import (
|
||||||
|
|
@ -20,6 +21,43 @@ from tenant_context import TenantContext
|
||||||
_default = DefaultNextActionStrategy()
|
_default = DefaultNextActionStrategy()
|
||||||
|
|
||||||
|
|
||||||
|
def _recurring_due_candidates(
|
||||||
|
ctx: TenantContext, initiative_id: str, *, limit: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
conn = get_connection()
|
||||||
|
try:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
'recurring_due' AS kind,
|
||||||
|
re.title AS title,
|
||||||
|
'Wiederkehrendes Element fällig' AS summary,
|
||||||
|
re.initiative_id,
|
||||||
|
NULL::uuid AS action_id,
|
||||||
|
NULL::uuid AS backlog_item_id,
|
||||||
|
'recurring_due' AS reason_code,
|
||||||
|
'Rhythmus-Element bearbeiten' AS recommended_action
|
||||||
|
FROM recurring_elements re
|
||||||
|
WHERE re.tenant_id = %s AND re.initiative_id = %s
|
||||||
|
AND re.status = 'active'
|
||||||
|
AND re.next_due_at IS NOT NULL
|
||||||
|
AND re.next_due_at <= NOW()
|
||||||
|
ORDER BY re.next_due_at ASC
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(ctx.tenant_id, initiative_id, limit),
|
||||||
|
)
|
||||||
|
items: list[dict[str, Any]] = []
|
||||||
|
for row in cur.fetchall():
|
||||||
|
item = dict(row)
|
||||||
|
item["initiative_id"] = str(item["initiative_id"])
|
||||||
|
items.append(item)
|
||||||
|
return items
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
class RecurringControlStrategy:
|
class RecurringControlStrategy:
|
||||||
key = "recurring_control"
|
key = "recurring_control"
|
||||||
|
|
||||||
|
|
@ -35,7 +73,7 @@ class RecurringControlStrategy:
|
||||||
if not initiative_id:
|
if not initiative_id:
|
||||||
return _default.evaluate(ctx, limit=limit)
|
return _default.evaluate(ctx, limit=limit)
|
||||||
|
|
||||||
recurring = recurring_due_candidates(ctx, initiative_id, limit=limit)
|
recurring = _recurring_due_candidates(ctx, initiative_id, limit=limit)
|
||||||
remaining = limit - len(recurring)
|
remaining = limit - len(recurring)
|
||||||
ready: list[dict[str, Any]] = []
|
ready: list[dict[str, Any]] = []
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
"""Shared recurring NextAction helpers."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from db import get_connection
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from tenant_context import TenantContext
|
|
||||||
|
|
||||||
|
|
||||||
def recurring_due_candidates(
|
|
||||||
ctx: TenantContext, initiative_id: str, *, limit: int
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
conn = get_connection()
|
|
||||||
try:
|
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
'recurring_due' AS kind,
|
|
||||||
re.title AS title,
|
|
||||||
'Wiederkehrendes Element fällig' AS summary,
|
|
||||||
re.initiative_id,
|
|
||||||
NULL::uuid AS action_id,
|
|
||||||
NULL::uuid AS backlog_item_id,
|
|
||||||
'recurring_due' AS reason_code,
|
|
||||||
'Rhythmus-Element bearbeiten' AS recommended_action
|
|
||||||
FROM recurring_elements re
|
|
||||||
WHERE re.tenant_id = %s AND re.initiative_id = %s
|
|
||||||
AND re.status = 'active'
|
|
||||||
AND re.next_due_at IS NOT NULL
|
|
||||||
AND re.next_due_at <= NOW()
|
|
||||||
ORDER BY re.next_due_at ASC
|
|
||||||
LIMIT %s
|
|
||||||
""",
|
|
||||||
(ctx.tenant_id, initiative_id, limit),
|
|
||||||
)
|
|
||||||
items: list[dict[str, Any]] = []
|
|
||||||
for row in cur.fetchall():
|
|
||||||
item = dict(row)
|
|
||||||
item["initiative_id"] = str(item["initiative_id"])
|
|
||||||
items.append(item)
|
|
||||||
return items
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def active_recurring_candidates(
|
|
||||||
ctx: TenantContext, initiative_id: str, *, limit: int = 1
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Active recurring as 'heutige Übung' when nothing is due yet."""
|
|
||||||
conn = get_connection()
|
|
||||||
try:
|
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
'recurring_due' AS kind,
|
|
||||||
re.title AS title,
|
|
||||||
'Heutige Übung / Routine' AS summary,
|
|
||||||
re.initiative_id,
|
|
||||||
NULL::uuid AS action_id,
|
|
||||||
NULL::uuid AS backlog_item_id,
|
|
||||||
'recurring_active' AS reason_code,
|
|
||||||
'Routine ausführen' AS recommended_action
|
|
||||||
FROM recurring_elements re
|
|
||||||
WHERE re.tenant_id = %s AND re.initiative_id = %s
|
|
||||||
AND re.status = 'active'
|
|
||||||
ORDER BY re.next_due_at ASC NULLS LAST, re.title
|
|
||||||
LIMIT %s
|
|
||||||
""",
|
|
||||||
(ctx.tenant_id, initiative_id, limit),
|
|
||||||
)
|
|
||||||
items: list[dict[str, Any]] = []
|
|
||||||
for row in cur.fetchall():
|
|
||||||
item = dict(row)
|
|
||||||
item["initiative_id"] = str(item["initiative_id"])
|
|
||||||
items.append(item)
|
|
||||||
return items
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
"""AP2.0e — maturity stage transition on verify reached."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from tests.factories import provision_user_in_tenant
|
|
||||||
from tests.test_initiatives_actions import _auth, _login
|
|
||||||
|
|
||||||
|
|
||||||
def test_maturity_stage_reached_rotates_recurring(client):
|
|
||||||
user = provision_user_in_tenant(tenant_role="admin")
|
|
||||||
token = _login(client, user)
|
|
||||||
|
|
||||||
created = client.post(
|
|
||||||
"/api/initiatives",
|
|
||||||
json={
|
|
||||||
"title": "Spagat Test",
|
|
||||||
"archetype_key": "initiative.maturity_journey",
|
|
||||||
},
|
|
||||||
headers=_auth(token),
|
|
||||||
)
|
|
||||||
assert created.status_code == 201
|
|
||||||
initiative_id = created.json()["id"]
|
|
||||||
|
|
||||||
items = client.get(
|
|
||||||
f"/api/initiatives/{initiative_id}/roadmap/items",
|
|
||||||
headers=_auth(token),
|
|
||||||
).json()
|
|
||||||
stage1 = next(i for i in items if i["title"] == "Stufe 1 — Basis")
|
|
||||||
|
|
||||||
client.post(
|
|
||||||
f"/api/initiatives/{initiative_id}/evidence",
|
|
||||||
json={
|
|
||||||
"title": "Stufe 1 geschafft",
|
|
||||||
"roadmap_item_id": stage1["id"],
|
|
||||||
"status": "accepted",
|
|
||||||
},
|
|
||||||
headers=_auth(token),
|
|
||||||
)
|
|
||||||
|
|
||||||
verify = client.post(
|
|
||||||
f"/api/roadmap-items/{stage1['id']}/verify-reached",
|
|
||||||
headers=_auth(token),
|
|
||||||
)
|
|
||||||
assert verify.status_code == 200
|
|
||||||
body = verify.json()
|
|
||||||
assert body["status"] == "reached"
|
|
||||||
assert body.get("maturity_transition", {}).get("new_recurring_id")
|
|
||||||
|
|
||||||
recurring = client.get(
|
|
||||||
f"/api/initiatives/{initiative_id}/recurring",
|
|
||||||
headers=_auth(token),
|
|
||||||
).json()
|
|
||||||
titles = {r["title"]: r["status"] for r in recurring}
|
|
||||||
assert titles.get("Tägliche Übung") == "paused"
|
|
||||||
assert any(t.startswith("Übung — Stufe 2") for t in titles)
|
|
||||||
|
|
||||||
items_after = client.get(
|
|
||||||
f"/api/initiatives/{initiative_id}/roadmap/items",
|
|
||||||
headers=_auth(token),
|
|
||||||
).json()
|
|
||||||
stage2 = next(i for i in items_after if i["title"] == "Stufe 2 — Aufbau")
|
|
||||||
assert stage2["status"] == "active"
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
"""Actor create API."""
|
|
||||||
|
|
||||||
from tests.factories import provision_user_in_tenant
|
|
||||||
from tests.test_initiatives_actions import _auth, _login
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_agent_actor(client):
|
|
||||||
user = provision_user_in_tenant(tenant_role="admin")
|
|
||||||
token = _login(client, user)
|
|
||||||
|
|
||||||
created = client.post(
|
|
||||||
"/api/actors",
|
|
||||||
json={"name": "Cursor Agent", "actor_type": "agent"},
|
|
||||||
headers=_auth(token),
|
|
||||||
)
|
|
||||||
assert created.status_code == 201
|
|
||||||
assert created.json()["name"] == "Cursor Agent"
|
|
||||||
assert created.json()["actor_type"] == "agent"
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
"""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.2-ap2.2d"
|
APP_VERSION = "0.20.0-ap2.2a"
|
||||||
DB_SCHEMA_VERSION = "025"
|
DB_SCHEMA_VERSION = "025"
|
||||||
APP_NAME = "jinkendo-kairo"
|
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 |
|
| A1 Reifegrad | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2c + AP2.0e |
|
||||||
| A2 Linear | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2b |
|
| A2 Linear | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2b |
|
||||||
| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
| B2b Product | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2d |
|
||||||
| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d ✓ |
|
| B3 Sprint-Profil | ◐ | ◐ | ◐ | ◐ | AP2.2d |
|
||||||
| B2a Programm | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2e |
|
| B2a Programm | ◐ AP2.2a | ◐ | ◐ | ◐ | AP2.2e |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -71,11 +71,11 @@ Legende: **Anlage** · **Struktur** · **Ist** · **Kontrolle** · **UI-Default*
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Phase 0 DOC + Kurskorrektur (dieses Dokument) ← jetzt
|
Phase 0 DOC + Kurskorrektur (dieses Dokument) ← jetzt
|
||||||
Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ✓
|
Phase 1 AP2.2a Archetyp-geführte Anlage + Starter-Kits ← NÄCHSTES CODE
|
||||||
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) ◐ (Hints + Recurring)
|
Phase 2 AP1.9d Methoden-Default-Ansichten (Anti-Todo-Wand) parallel möglich
|
||||||
Phase 3 AP2.2b A2 Linear End-to-End ◐
|
Phase 3 AP2.2b A2 Linear End-to-End
|
||||||
AP2.2c A1 Reifegrad + AP2.0e ◐ Code
|
AP2.2c A1 Reifegrad + AP2.0e
|
||||||
AP2.2d B2b Product + B3 Sprint ✓
|
AP2.2d B2b Product + B3 Sprint
|
||||||
AP2.2e B2a Programm (optional vor AP2.1)
|
AP2.2e B2a Programm (optional vor AP2.1)
|
||||||
Phase 4 AP1.7b Operational API — Pflege-Parität
|
Phase 4 AP1.7b Operational API — Pflege-Parität
|
||||||
Phase 5 AP2.1 Validation Report v0.3 (alle Stufe-A-Szenarien)
|
Phase 5 AP2.1 Validation Report v0.3 (alle Stufe-A-Szenarien)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "kairo-jinkendo-frontend",
|
"name": "kairo-jinkendo-frontend",
|
||||||
"version": "0.20.2-ap2.2d",
|
"version": "0.20.0-ap2.2a",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ function AppRoutes() {
|
||||||
<Route path="/work" element={<WorkLayout />}>
|
<Route path="/work" element={<WorkLayout />}>
|
||||||
<Route index element={<WorkIndexRedirect />} />
|
<Route index element={<WorkIndexRedirect />} />
|
||||||
<Route path="today" element={<MODE_ROUTE_COMPONENTS.workToday />} />
|
<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 path="mine" element={<MODE_ROUTE_COMPONENTS.workMine />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|
@ -74,7 +73,6 @@ function AppRoutes() {
|
||||||
<Route path="structure" element={<MODE_ROUTE_COMPONENTS.planStructure />} />
|
<Route path="structure" element={<MODE_ROUTE_COMPONENTS.planStructure />} />
|
||||||
<Route path="gates" element={<MODE_ROUTE_COMPONENTS.planGates />} />
|
<Route path="gates" element={<MODE_ROUTE_COMPONENTS.planGates />} />
|
||||||
<Route path="inbox" element={<MODE_ROUTE_COMPONENTS.planInbox />} />
|
<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 path="work" element={<MODE_ROUTE_COMPONENTS.planWork />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,6 @@ export function getActor(id) {
|
||||||
return apiFetch(`/api/actors/${id}`)
|
return apiFetch(`/api/actors/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createActor(body) {
|
|
||||||
return apiFetch('/api/actors', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fallback wenn Actor Directory nicht erreichbar (AP0.6-Kompatibilität). */
|
/** Fallback wenn Actor Directory nicht erreichbar (AP0.6-Kompatibilität). */
|
||||||
export function actorsFromContext(context) {
|
export function actorsFromContext(context) {
|
||||||
if (!context?.actor?.id) return []
|
if (!context?.actor?.id) return []
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
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,8 +15,6 @@ export function ActionForm({
|
||||||
initial = {},
|
initial = {},
|
||||||
projects = [],
|
projects = [],
|
||||||
roadmapItems = [],
|
roadmapItems = [],
|
||||||
workCycles = [],
|
|
||||||
activeWorkCycle = null,
|
|
||||||
actors = [],
|
actors = [],
|
||||||
actorsLoading = false,
|
actorsLoading = false,
|
||||||
actorsError = null,
|
actorsError = null,
|
||||||
|
|
@ -39,7 +37,6 @@ export function ActionForm({
|
||||||
due_at: form.due_at.value ? new Date(form.due_at.value).toISOString() : null,
|
due_at: form.due_at.value ? new Date(form.due_at.value).toISOString() : null,
|
||||||
project_id: form.project_id?.value || undefined,
|
project_id: form.project_id?.value || undefined,
|
||||||
roadmap_item_id: form.roadmap_item_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,
|
assigned_actor_ids: selected,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -47,9 +44,6 @@ export function ActionForm({
|
||||||
const defaultActors = initial.assigned_actor_ids || []
|
const defaultActors = initial.assigned_actor_ids || []
|
||||||
const leafProjects = getLeafProjects(projects)
|
const leafProjects = getLeafProjects(projects)
|
||||||
const projectOptions = flattenLeafProjectOptions(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 (
|
return (
|
||||||
<form className="form workspace-form" onSubmit={handleSubmit}>
|
<form className="form workspace-form" onSubmit={handleSubmit}>
|
||||||
|
|
@ -106,20 +100,6 @@ export function ActionForm({
|
||||||
roadmapItems={roadmapItems}
|
roadmapItems={roadmapItems}
|
||||||
defaultValue={initial.roadmap_item_id || ''}
|
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
|
<ActorSelect
|
||||||
actors={actors}
|
actors={actors}
|
||||||
defaultSelected={defaultActors}
|
defaultSelected={defaultActors}
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,6 @@ export function ActionHubCard({
|
||||||
initiativeId,
|
initiativeId,
|
||||||
projects = [],
|
projects = [],
|
||||||
roadmapItems = [],
|
roadmapItems = [],
|
||||||
workCycles = [],
|
|
||||||
activeWorkCycle = null,
|
|
||||||
detailMode = false,
|
detailMode = false,
|
||||||
}) {
|
}) {
|
||||||
const due = formatDue(action.due_at)
|
const due = formatDue(action.due_at)
|
||||||
|
|
@ -68,8 +66,6 @@ export function ActionHubCard({
|
||||||
initial={action}
|
initial={action}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
roadmapItems={roadmapItems}
|
roadmapItems={roadmapItems}
|
||||||
workCycles={workCycles}
|
|
||||||
activeWorkCycle={activeWorkCycle}
|
|
||||||
actors={actors}
|
actors={actors}
|
||||||
actorsLoading={actorsLoading}
|
actorsLoading={actorsLoading}
|
||||||
actorsError={actorsError}
|
actorsError={actorsError}
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { scopedPath } from '../utils/routes.js'
|
|
||||||
|
|
||||||
const HINTS = {
|
|
||||||
'initiative.maturity_journey': {
|
|
||||||
text: 'Reifegrad: Heutige Übung (Rhythmus) und aktive Stufe steuern den nächsten Schritt.',
|
|
||||||
link: { to: '/control/journey', label: 'Rhythmen & Journey' },
|
|
||||||
},
|
|
||||||
'initiative.linear_project': {
|
|
||||||
text: 'Linear: Nächster Schritt am kritischen Pfad — Abhängigkeiten im Gate-Graph.',
|
|
||||||
link: { to: '/plan/gates', label: 'Gate-Graph öffnen' },
|
|
||||||
},
|
|
||||||
'initiative.product': {
|
|
||||||
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.',
|
|
||||||
link: { to: '/plan/gates', label: 'Phasen / Gates' },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ArchetypeSteeringHints({ archetypeKey }) {
|
|
||||||
const hint = HINTS[archetypeKey]
|
|
||||||
if (!hint) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className="steering-archetype-hints muted">
|
|
||||||
{hint.text}{' '}
|
|
||||||
<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,8 +19,6 @@ export function BacklogSection({
|
||||||
onConvert,
|
onConvert,
|
||||||
onDelete,
|
onDelete,
|
||||||
busy,
|
busy,
|
||||||
sectionTitle = 'Product Backlog',
|
|
||||||
sectionLead = 'Eingang vor dem Commit — triagieren, dann in Arbeitspaket (Sprint) umwandeln.',
|
|
||||||
}) {
|
}) {
|
||||||
const [modalMode, setModalMode] = useState(null)
|
const [modalMode, setModalMode] = useState(null)
|
||||||
const [dragItemId, setDragItemId] = useState('')
|
const [dragItemId, setDragItemId] = useState('')
|
||||||
|
|
@ -108,8 +106,10 @@ export function BacklogSection({
|
||||||
<section className="card backlog-section">
|
<section className="card backlog-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>{sectionTitle}</h2>
|
<h2>Backlog</h2>
|
||||||
<p className="section-lead muted">{sectionLead}</p>
|
<p className="section-lead muted">
|
||||||
|
Reihenfolge per Drag & Drop (Desktop) oder ↑/↓ (Mobile).
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{canManage && (
|
{canManage && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ export function InitiativeActionsHub({
|
||||||
actions,
|
actions,
|
||||||
projects = [],
|
projects = [],
|
||||||
roadmapItems = [],
|
roadmapItems = [],
|
||||||
workCycles = [],
|
|
||||||
activeWorkCycle = null,
|
|
||||||
actionContextById,
|
actionContextById,
|
||||||
hideDone,
|
hideDone,
|
||||||
onHideDoneChange,
|
onHideDoneChange,
|
||||||
|
|
@ -29,15 +27,16 @@ export function InitiativeActionsHub({
|
||||||
actorsError,
|
actorsError,
|
||||||
actorsUsedFallback,
|
actorsUsedFallback,
|
||||||
onReloadActors,
|
onReloadActors,
|
||||||
sectionTitle = 'Arbeitspakete',
|
|
||||||
sectionLead = 'Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews hängen am Arbeitspaket.',
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="card initiative-actions-hub">
|
<section className="card initiative-actions-hub">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>{sectionTitle}</h2>
|
<h2>Arbeitspakete</h2>
|
||||||
<p className="section-lead muted">{sectionLead}</p>
|
<p className="section-lead muted">
|
||||||
|
Committete operative Einheit — Aufgaben im Detail. Blocker, Nachweise und Reviews
|
||||||
|
hängen am Arbeitspaket.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-actions">
|
<div className="section-actions">
|
||||||
<label className="checkbox-label inline-filter">
|
<label className="checkbox-label inline-filter">
|
||||||
|
|
@ -65,8 +64,6 @@ export function InitiativeActionsHub({
|
||||||
<ActionForm
|
<ActionForm
|
||||||
projects={projects}
|
projects={projects}
|
||||||
roadmapItems={roadmapItems}
|
roadmapItems={roadmapItems}
|
||||||
workCycles={workCycles}
|
|
||||||
activeWorkCycle={activeWorkCycle}
|
|
||||||
actors={actors}
|
actors={actors}
|
||||||
actorsLoading={actorsLoading}
|
actorsLoading={actorsLoading}
|
||||||
actorsError={actorsError}
|
actorsError={actorsError}
|
||||||
|
|
@ -107,8 +104,6 @@ export function InitiativeActionsHub({
|
||||||
initiativeId={initiativeId}
|
initiativeId={initiativeId}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
roadmapItems={roadmapItems}
|
roadmapItems={roadmapItems}
|
||||||
workCycles={workCycles}
|
|
||||||
activeWorkCycle={activeWorkCycle}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { INITIATIVE_STATUSES, PRIORITIES } from '../constants/status.js'
|
import { INITIATIVE_STATUSES, PRIORITIES } from '../constants/status.js'
|
||||||
import { INITIATIVE_STATUS_LABELS, PRIORITY_LABELS } from '../constants/status.js'
|
import { INITIATIVE_STATUS_LABELS, PRIORITY_LABELS } from '../constants/status.js'
|
||||||
import { INITIATIVE_ARCHETYPE_LABELS, STUFE_A_ARCHETYPE_KEYS } from '../constants/initiativeArchetypes.js'
|
import { INITIATIVE_ARCHETYPE_LABELS } from '../constants/initiativeArchetypes.js'
|
||||||
import { listEntityArchetypes, getArchetypeStarterPreview } from '../api/entityArchetypes.js'
|
import { listEntityArchetypes, getArchetypeStarterPreview } from '../api/entityArchetypes.js'
|
||||||
import {
|
import {
|
||||||
getInitiativeFields,
|
getInitiativeFields,
|
||||||
|
|
@ -129,8 +129,6 @@ export function InitiativeForm({
|
||||||
|
|
||||||
const formReady = archetypes && !fieldsLoading
|
const formReady = archetypes && !fieldsLoading
|
||||||
const selectedArchetype = (archetypes || []).find((item) => item.key === archetypeKey)
|
const selectedArchetype = (archetypes || []).find((item) => item.key === archetypeKey)
|
||||||
const stufeA = (archetypes || []).filter((item) => STUFE_A_ARCHETYPE_KEYS.has(item.key))
|
|
||||||
const otherArchetypes = (archetypes || []).filter((item) => !STUFE_A_ARCHETYPE_KEYS.has(item.key))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="form workspace-form initiative-form" onSubmit={handleSubmit}>
|
<form className="form workspace-form initiative-form" onSubmit={handleSubmit}>
|
||||||
|
|
@ -148,24 +146,11 @@ export function InitiativeForm({
|
||||||
onChange={(e) => setArchetypeKey(e.target.value)}
|
onChange={(e) => setArchetypeKey(e.target.value)}
|
||||||
disabled={!archetypes}
|
disabled={!archetypes}
|
||||||
>
|
>
|
||||||
{stufeA.length > 0 && (
|
{(archetypes || []).map((item) => (
|
||||||
<optgroup label="MVP Stufe A (empfohlen)">
|
|
||||||
{stufeA.map((item) => (
|
|
||||||
<option key={item.key} value={item.key}>
|
<option key={item.key} value={item.key}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</optgroup>
|
|
||||||
)}
|
|
||||||
{otherArchetypes.length > 0 && (
|
|
||||||
<optgroup label="Weitere Archetypen">
|
|
||||||
{otherArchetypes.map((item) => (
|
|
||||||
<option key={item.key} value={item.key}>
|
|
||||||
{item.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</optgroup>
|
|
||||||
)}
|
|
||||||
</select>
|
</select>
|
||||||
{selectedArchetype?.description && (
|
{selectedArchetype?.description && (
|
||||||
<span className="muted form-hint">{selectedArchetype.description}</span>
|
<span className="muted form-hint">{selectedArchetype.description}</span>
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ export function PlanActionsSection({
|
||||||
onReloadActors,
|
onReloadActors,
|
||||||
busy,
|
busy,
|
||||||
executionGraph = null,
|
executionGraph = null,
|
||||||
sectionTitle = 'Arbeit',
|
|
||||||
sectionLead = 'Committete Arbeitspakete und zugehörige Aufgaben — Ausführung über die Objektseite.',
|
|
||||||
}) {
|
}) {
|
||||||
const [showCreate, setShowCreate] = useState(false)
|
const [showCreate, setShowCreate] = useState(false)
|
||||||
const [expandedActions, setExpandedActions] = useState(() => new Set())
|
const [expandedActions, setExpandedActions] = useState(() => new Set())
|
||||||
|
|
@ -75,8 +73,10 @@ export function PlanActionsSection({
|
||||||
<section className="card plan-actions-section">
|
<section className="card plan-actions-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>{sectionTitle}</h2>
|
<h2>Arbeit</h2>
|
||||||
<p className="section-lead muted">{sectionLead}</p>
|
<p className="section-lead muted">
|
||||||
|
Committete Arbeitspakete und zugehörige Aufgaben — Ausführung über die Objektseite.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="section-actions">
|
<div className="section-actions">
|
||||||
<label className="checkbox-label inline-filter">
|
<label className="checkbox-label inline-filter">
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
} from '../constants/operating.js'
|
} from '../constants/operating.js'
|
||||||
import { MILESTONE_STATUS_LABELS } from '../constants/status.js'
|
import { MILESTONE_STATUS_LABELS } from '../constants/status.js'
|
||||||
import { methodDescriptionForKey } from '../utils/archetypes.js'
|
import { methodDescriptionForKey } from '../utils/archetypes.js'
|
||||||
import { ArchetypeSteeringHints } from './ArchetypeSteeringHints.jsx'
|
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { scopedPath } from '../utils/routes.js'
|
import { scopedPath } from '../utils/routes.js'
|
||||||
|
|
||||||
|
|
@ -107,7 +106,6 @@ export function SteeringSnapshotPanel({
|
||||||
{steering_guidance && (
|
{steering_guidance && (
|
||||||
<p className="steering-guidance">{steering_guidance}</p>
|
<p className="steering-guidance">{steering_guidance}</p>
|
||||||
)}
|
)}
|
||||||
<ArchetypeSteeringHints archetypeKey={archetype_key} />
|
|
||||||
</div>
|
</div>
|
||||||
<span className="badge status-badge status-active steering-lifecycle-badge">
|
<span className="badge status-badge status-active steering-lifecycle-badge">
|
||||||
{displayLabel}
|
{displayLabel}
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
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,7 +13,6 @@ describe('controlNav', () => {
|
||||||
|
|
||||||
describe('workNav', () => {
|
describe('workNav', () => {
|
||||||
it('resolves active work sub-route', () => {
|
it('resolves active work sub-route', () => {
|
||||||
expect(resolveWorkNavActiveKey('/work/sprint')).toBe('sprint')
|
|
||||||
expect(resolveWorkNavActiveKey('/work/today')).toBe('today')
|
expect(resolveWorkNavActiveKey('/work/today')).toBe('today')
|
||||||
expect(resolveWorkNavActiveKey('/work/mine')).toBe('mine')
|
expect(resolveWorkNavActiveKey('/work/mine')).toBe('mine')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
/** @type {ModeNavItem[]} */
|
/** @type {ModeNavItem[]} */
|
||||||
export const WORK_NAV_ITEMS = [
|
export const WORK_NAV_ITEMS = [
|
||||||
{ key: 'sprint', to: '/work/sprint', label: 'Sprint' },
|
|
||||||
{ key: 'today', to: '/work/today', label: 'Heute' },
|
{ key: 'today', to: '/work/today', label: 'Heute' },
|
||||||
{ key: 'mine', to: '/work/mine', label: 'Meine Queue' },
|
{ key: 'mine', to: '/work/mine', label: 'Meine Queue' },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
/** MVP Stufe A — geführte Referenz-Archetypen (AP2.2a). */
|
/** Fallback-Labels bis API geladen (AP1.10a / AP2.0). */
|
||||||
export const STUFE_A_ARCHETYPE_KEYS = new Set([
|
|
||||||
'initiative.maturity_journey',
|
|
||||||
'initiative.linear_project',
|
|
||||||
'initiative.product',
|
|
||||||
'initiative.program',
|
|
||||||
])
|
|
||||||
export const INITIATIVE_ARCHETYPE_LABELS = {
|
export const INITIATIVE_ARCHETYPE_LABELS = {
|
||||||
'initiative.generic': 'Allgemeines Vorhaben',
|
'initiative.generic': 'Allgemeines Vorhaben',
|
||||||
'initiative.maturity_journey': 'Reifegrad-Entwicklung',
|
'initiative.maturity_journey': 'Reifegrad-Entwicklung',
|
||||||
|
|
|
||||||
|
|
@ -71,12 +71,6 @@ import {
|
||||||
updateInitiativeSteeringMethod,
|
updateInitiativeSteeringMethod,
|
||||||
updateInitiativeMethodProfile,
|
updateInitiativeMethodProfile,
|
||||||
} from '../api/steering.js'
|
} from '../api/steering.js'
|
||||||
import {
|
|
||||||
listInitiativeWorkCycles,
|
|
||||||
getActiveWorkCycle,
|
|
||||||
createWorkCycle,
|
|
||||||
activateWorkCycle,
|
|
||||||
} from '../api/workCycles.js'
|
|
||||||
import { useCapabilities } from '../hooks/useCapabilities.js'
|
import { useCapabilities } from '../hooks/useCapabilities.js'
|
||||||
import { useActors } from '../hooks/useActors.js'
|
import { useActors } from '../hooks/useActors.js'
|
||||||
import { useSession } from './SessionContext.jsx'
|
import { useSession } from './SessionContext.jsx'
|
||||||
|
|
@ -104,8 +98,6 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
const [decisions, setDecisions] = useState([])
|
const [decisions, setDecisions] = useState([])
|
||||||
const [reviews, setReviews] = useState([])
|
const [reviews, setReviews] = useState([])
|
||||||
const [recurringItems, setRecurringItems] = useState([])
|
const [recurringItems, setRecurringItems] = useState([])
|
||||||
const [workCycles, setWorkCycles] = useState([])
|
|
||||||
const [activeWorkCycle, setActiveWorkCycle] = useState(null)
|
|
||||||
const [steeringSnapshot, setSteeringSnapshot] = useState(null)
|
const [steeringSnapshot, setSteeringSnapshot] = useState(null)
|
||||||
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
|
const [steeringSnapshotLoading, setSteeringSnapshotLoading] = useState(false)
|
||||||
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
|
const [steeringSnapshotError, setSteeringSnapshotError] = useState(null)
|
||||||
|
|
@ -143,12 +135,6 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
|
loads.push(listInitiativeDecisions(id).then(setDecisions).catch(() => setDecisions([])))
|
||||||
loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([])))
|
loads.push(listInitiativeReviews(id).then(setReviews).catch(() => setReviews([])))
|
||||||
loads.push(listInitiativeRecurring(id).then(setRecurringItems).catch(() => setRecurringItems([])))
|
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(
|
loads.push(
|
||||||
listSteeringMethods().then(setSteeringMethods).catch(() => setSteeringMethods([]))
|
listSteeringMethods().then(setSteeringMethods).catch(() => setSteeringMethods([]))
|
||||||
)
|
)
|
||||||
|
|
@ -200,20 +186,6 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
[actions, hideDone, selectedProjectId, projects]
|
[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(
|
const actionContextById = useMemo(
|
||||||
() => Object.fromEntries((steeringSnapshot?.actions || []).map((a) => [a.id, a])),
|
() => Object.fromEntries((steeringSnapshot?.actions || []).map((a) => [a.id, a])),
|
||||||
[steeringSnapshot]
|
[steeringSnapshot]
|
||||||
|
|
@ -285,8 +257,6 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
clear_project: payload.project_id === '' || payload.project_id === null,
|
clear_project: payload.project_id === '' || payload.project_id === null,
|
||||||
roadmap_item_id: payload.roadmap_item_id || undefined,
|
roadmap_item_id: payload.roadmap_item_id || undefined,
|
||||||
clear_roadmap_item: payload.roadmap_item_id === '' || payload.roadmap_item_id === null,
|
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')) {
|
if (capabilities.has('kairo.action.manage')) {
|
||||||
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
|
await setActionAssignments(actionId, payload.assigned_actor_ids || [])
|
||||||
|
|
@ -423,31 +393,7 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
async function handleConvertBacklog(itemId) {
|
async function handleConvertBacklog(itemId) {
|
||||||
setFormBusy(true)
|
setFormBusy(true)
|
||||||
try {
|
try {
|
||||||
await convertBacklogToAction(itemId, { assign_active_sprint: true })
|
await convertBacklogToAction(itemId)
|
||||||
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()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
|
|
@ -781,9 +727,6 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
decisions,
|
decisions,
|
||||||
reviews,
|
reviews,
|
||||||
recurringItems,
|
recurringItems,
|
||||||
workCycles,
|
|
||||||
activeWorkCycle,
|
|
||||||
sprintActions,
|
|
||||||
steeringSnapshot,
|
steeringSnapshot,
|
||||||
steeringSnapshotLoading,
|
steeringSnapshotLoading,
|
||||||
steeringSnapshotError,
|
steeringSnapshotError,
|
||||||
|
|
@ -821,8 +764,6 @@ export function InitiativeOperationsProvider({ children, initiativeId: initiativ
|
||||||
handleBacklogGate,
|
handleBacklogGate,
|
||||||
handleBacklogStatus,
|
handleBacklogStatus,
|
||||||
handleConvertBacklog,
|
handleConvertBacklog,
|
||||||
handleCreateWorkCycle,
|
|
||||||
handleActivateWorkCycle,
|
|
||||||
handleDeleteBacklog,
|
handleDeleteBacklog,
|
||||||
handleCreateRoadmapItem,
|
handleCreateRoadmapItem,
|
||||||
handleRoadmapItemStatus,
|
handleRoadmapItemStatus,
|
||||||
|
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
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,8 +75,6 @@ function PlanWorkInner() {
|
||||||
onReloadActors={reloadActors}
|
onReloadActors={reloadActors}
|
||||||
busy={formBusy}
|
busy={formBusy}
|
||||||
executionGraph={executionGraph}
|
executionGraph={executionGraph}
|
||||||
sectionTitle="Alle Arbeitspakete"
|
|
||||||
sectionLead="Gesamt-Ist am Vorhaben — Sprint-fokussierte Arbeit unter Ausführen → Sprint."
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,108 +1,35 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { getActorWorkload } from '../../api/workspace.js'
|
import { getActorWorkload } from '../../api/workspace.js'
|
||||||
import { createActor, listActors } from '../../api/actors.js'
|
|
||||||
import { useSession } from '../../context/SessionContext.jsx'
|
|
||||||
import { ModeShell } from '../../components/ModeShell.jsx'
|
import { ModeShell } from '../../components/ModeShell.jsx'
|
||||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||||
import { ErrorState } from '../../components/ErrorState.jsx'
|
import { ErrorState } from '../../components/ErrorState.jsx'
|
||||||
import { scopedPath } from '../../utils/routes.js'
|
import { scopedPath } from '../../utils/routes.js'
|
||||||
|
|
||||||
const ACTOR_TYPE_LABELS = {
|
|
||||||
human: 'Person',
|
|
||||||
agent: 'Agent / Vibe-Coder',
|
|
||||||
working_group: 'Gruppe',
|
|
||||||
external_system: 'Externes System',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TeamPage() {
|
export function TeamPage() {
|
||||||
const { hasCapability } = useSession()
|
|
||||||
const [rows, setRows] = useState(null)
|
const [rows, setRows] = useState(null)
|
||||||
const [actors, setActors] = useState([])
|
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [agentName, setAgentName] = useState('')
|
|
||||||
const [busy, setBusy] = useState(false)
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const [workload, directory] = await Promise.all([
|
|
||||||
getActorWorkload(),
|
|
||||||
listActors({ include_inactive: false }),
|
|
||||||
])
|
|
||||||
setRows(workload)
|
|
||||||
setActors(directory)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
let cancelled = false
|
||||||
|
getActorWorkload()
|
||||||
|
.then((data) => {
|
||||||
|
if (!cancelled) setRows(data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!cancelled) setError(err.message)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function handleCreateAgent(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
const name = agentName.trim()
|
|
||||||
if (!name) return
|
|
||||||
setBusy(true)
|
|
||||||
try {
|
|
||||||
await createActor({ name, actor_type: 'agent' })
|
|
||||||
setAgentName('')
|
|
||||||
await load()
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message)
|
|
||||||
} finally {
|
|
||||||
setBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const canManage = hasCapability('kairo.actor.manage')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<div className="app-page">
|
||||||
<ModeShell modeKey="team" />
|
<ModeShell modeKey="team" />
|
||||||
{rows === null && !error && <LoadingState message="Lade Team …" />}
|
{rows === null && !error && <LoadingState message="Lade Team-Auslastung …" />}
|
||||||
{error && <ErrorState message={error} onRetry={load} />}
|
{error && <ErrorState message={error} />}
|
||||||
{rows && (
|
{rows && (
|
||||||
<>
|
|
||||||
<section className="card">
|
|
||||||
<h2 className="card-title">Actor-Verzeichnis</h2>
|
|
||||||
{actors.length === 0 ? (
|
|
||||||
<p className="muted">Keine Actors im Tenant.</p>
|
|
||||||
) : (
|
|
||||||
<ul className="team-workload-list">
|
|
||||||
{actors.map((actor) => (
|
|
||||||
<li key={actor.id} className="team-workload-list__item">
|
|
||||||
<div>
|
|
||||||
<strong>{actor.name}</strong>
|
|
||||||
<span className="muted">
|
|
||||||
{' '}
|
|
||||||
· {ACTOR_TYPE_LABELS[actor.actor_type] || actor.actor_type}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
{canManage && (
|
|
||||||
<form className="form inline-form team-agent-form" onSubmit={handleCreateAgent}>
|
|
||||||
<label>
|
|
||||||
Agent anlegen (Vibe-Coder)
|
|
||||||
<input
|
|
||||||
value={agentName}
|
|
||||||
onChange={(e) => setAgentName(e.target.value)}
|
|
||||||
placeholder="z. B. Cursor Agent"
|
|
||||||
maxLength={255}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button type="submit" className="btn btn-primary btn-sm" disabled={busy}>
|
|
||||||
{busy ? 'Anlegen …' : 'Agent anlegen'}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<h2 className="card-title">Auslastung nach Actor</h2>
|
<h2 className="card-title">Auslastung nach Actor</h2>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
|
|
@ -112,7 +39,7 @@ export function TeamPage() {
|
||||||
{rows.map((row) => (
|
{rows.map((row) => (
|
||||||
<li key={row.actor_id} className="team-workload-list__item">
|
<li key={row.actor_id} className="team-workload-list__item">
|
||||||
<div>
|
<div>
|
||||||
<strong>{row.display_name || row.actor_name || row.actor_id}</strong>
|
<strong>{row.actor_name || row.actor_id}</strong>
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
{' '}
|
{' '}
|
||||||
· {row.open_actions ?? 0} offen
|
· {row.open_actions ?? 0} offen
|
||||||
|
|
@ -128,8 +55,10 @@ export function TeamPage() {
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
<p className="page-footer-link muted">
|
||||||
|
AP1.9b erweitert Team um Filter nach Vorhaben und direkte Zuweisungslinks.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import { Navigate, Outlet } from 'react-router-dom'
|
import { Navigate, Outlet } from 'react-router-dom'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
import { ModeAreaShell } from '../../components/ModeAreaShell.jsx'
|
||||||
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
|
import { ModeAreaNav } from '../../components/ModeAreaNav.jsx'
|
||||||
import { ModeShell } from '../../components/ModeShell.jsx'
|
import { ModeShell } from '../../components/ModeShell.jsx'
|
||||||
import { WORK_NAV_ITEMS, resolveWorkNavActiveKey } from '../../config/workNav.js'
|
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() {
|
export function WorkLayout() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -28,34 +24,5 @@ export function WorkLayout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkIndexRedirect() {
|
export function WorkIndexRedirect() {
|
||||||
const { initiativeId } = useProgramScope()
|
return <Navigate to="/work/today" replace />
|
||||||
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 />
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
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,7 +11,6 @@ export const PLAN_OUTLINE_NODES = [
|
||||||
{ key: 'structure', to: '/plan/structure', label: 'Struktur', requiresInitiative: true },
|
{ key: 'structure', to: '/plan/structure', label: 'Struktur', requiresInitiative: true },
|
||||||
{ key: 'gates', to: '/plan/gates', label: 'Zielzustände', requiresInitiative: true },
|
{ key: 'gates', to: '/plan/gates', label: 'Zielzustände', requiresInitiative: true },
|
||||||
{ key: 'inbox', to: '/plan/inbox', label: 'Eingang', 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 },
|
{ key: 'work', to: '/plan/work', label: 'Arbeit', requiresInitiative: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ describe('planOutlineNodes', () => {
|
||||||
'structure',
|
'structure',
|
||||||
'gates',
|
'gates',
|
||||||
'inbox',
|
'inbox',
|
||||||
'sprint',
|
|
||||||
'work',
|
'work',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
@ -23,7 +22,6 @@ describe('planOutlineNodes', () => {
|
||||||
expect(resolvePlanOutlineActiveKey('/plan/structure')).toBe('structure')
|
expect(resolvePlanOutlineActiveKey('/plan/structure')).toBe('structure')
|
||||||
expect(resolvePlanOutlineActiveKey('/plan/gates')).toBe('gates')
|
expect(resolvePlanOutlineActiveKey('/plan/gates')).toBe('gates')
|
||||||
expect(resolvePlanOutlineActiveKey('/plan/inbox')).toBe('inbox')
|
expect(resolvePlanOutlineActiveKey('/plan/inbox')).toBe('inbox')
|
||||||
expect(resolvePlanOutlineActiveKey('/plan/sprint')).toBe('sprint')
|
|
||||||
expect(resolvePlanOutlineActiveKey('/plan/work')).toBe('work')
|
expect(resolvePlanOutlineActiveKey('/plan/work')).toBe('work')
|
||||||
expect(resolvePlanOutlineActiveKey('/plan/portfolio')).toBe(null)
|
expect(resolvePlanOutlineActiveKey('/plan/portfolio')).toBe(null)
|
||||||
expect(resolvePlanOutlineActiveKey('/projects/abc-123')).toBe('structure')
|
expect(resolvePlanOutlineActiveKey('/projects/abc-123')).toBe('structure')
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ import { CockpitPage } from '../pages/modes/CockpitPage.jsx'
|
||||||
import { WorkLayout, WorkIndexRedirect } from '../pages/modes/WorkLayout.jsx'
|
import { WorkLayout, WorkIndexRedirect } from '../pages/modes/WorkLayout.jsx'
|
||||||
import { WorkTodayPage } from '../pages/modes/WorkTodayPage.jsx'
|
import { WorkTodayPage } from '../pages/modes/WorkTodayPage.jsx'
|
||||||
import { WorkMinePage } from '../pages/modes/WorkMinePage.jsx'
|
import { WorkMinePage } from '../pages/modes/WorkMinePage.jsx'
|
||||||
import { WorkSprintPage } from '../pages/modes/WorkSprintPage.jsx'
|
|
||||||
import { PlanLayout, PlanIndexRedirect } from '../pages/modes/PlanLayout.jsx'
|
import { PlanLayout, PlanIndexRedirect } from '../pages/modes/PlanLayout.jsx'
|
||||||
import { PlanStructurePage } from '../pages/modes/PlanStructurePage.jsx'
|
import { PlanStructurePage } from '../pages/modes/PlanStructurePage.jsx'
|
||||||
import { PlanGatesPage } from '../pages/modes/PlanGatesPage.jsx'
|
import { PlanGatesPage } from '../pages/modes/PlanGatesPage.jsx'
|
||||||
import { PlanInboxPage } from '../pages/modes/PlanInboxPage.jsx'
|
import { PlanInboxPage } from '../pages/modes/PlanInboxPage.jsx'
|
||||||
import { PlanWorkPage } from '../pages/modes/PlanWorkPage.jsx'
|
import { PlanWorkPage } from '../pages/modes/PlanWorkPage.jsx'
|
||||||
import { PlanSprintPage } from '../pages/modes/PlanSprintPage.jsx'
|
|
||||||
import { PlanProfilePage } from '../pages/modes/PlanProfilePage.jsx'
|
import { PlanProfilePage } from '../pages/modes/PlanProfilePage.jsx'
|
||||||
import { PlanPortfolioPage } from '../pages/modes/PlanPortfolioPage.jsx'
|
import { PlanPortfolioPage } from '../pages/modes/PlanPortfolioPage.jsx'
|
||||||
import { ControlLayout, ControlIndexRedirect } from '../pages/modes/ControlLayout.jsx'
|
import { ControlLayout, ControlIndexRedirect } from '../pages/modes/ControlLayout.jsx'
|
||||||
|
|
@ -131,13 +129,11 @@ export function getViewByKey(key) {
|
||||||
export const MODE_ROUTE_COMPONENTS = {
|
export const MODE_ROUTE_COMPONENTS = {
|
||||||
workToday: WorkTodayPage,
|
workToday: WorkTodayPage,
|
||||||
workMine: WorkMinePage,
|
workMine: WorkMinePage,
|
||||||
workSprint: WorkSprintPage,
|
|
||||||
planPortfolio: PlanPortfolioPage,
|
planPortfolio: PlanPortfolioPage,
|
||||||
planProfile: PlanProfilePage,
|
planProfile: PlanProfilePage,
|
||||||
planStructure: PlanStructurePage,
|
planStructure: PlanStructurePage,
|
||||||
planGates: PlanGatesPage,
|
planGates: PlanGatesPage,
|
||||||
planInbox: PlanInboxPage,
|
planInbox: PlanInboxPage,
|
||||||
planSprint: PlanSprintPage,
|
|
||||||
planWork: PlanWorkPage,
|
planWork: PlanWorkPage,
|
||||||
controlStatus: ControlStatusPage,
|
controlStatus: ControlStatusPage,
|
||||||
controlPlanIst: ControlPlanIstPage,
|
controlPlanIst: ControlPlanIstPage,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user