diff --git a/backend/data_layer/initiative_snapshot.py b/backend/data_layer/initiative_snapshot.py
index 44ae0b1..7dccada 100644
--- a/backend/data_layer/initiative_snapshot.py
+++ b/backend/data_layer/initiative_snapshot.py
@@ -11,7 +11,8 @@ from psycopg2.extras import RealDictCursor
from db import get_connection
from data_layer.attention import get_next_action_candidates_for_initiative
-from entity_archetypes.registry import archetype_label
+ from entity_archetypes.registry import archetype_label
+ from method_profiles.registry import get_method_profile
from steering.context import get_steering_context_dto
from steering.signals.snapshot_signals import derive_initiative_signals
from services.initiatives import get_initiative
@@ -301,6 +302,11 @@ def get_initiative_steering_snapshot(
if isinstance(metadata, str):
metadata = {}
method_profile_key = metadata.get("method_profile_key")
+ method_profile_label = None
+ if method_profile_key:
+ profile = get_method_profile(method_profile_key)
+ if profile:
+ method_profile_label = profile.get("label")
initiative_archetype = initiative.get("archetype_key") or "initiative.generic"
return {
@@ -312,6 +318,7 @@ def get_initiative_steering_snapshot(
entity_type="initiative", archetype_key=initiative_archetype
),
"method_profile_key": method_profile_key,
+ "method_profile_label": method_profile_label,
"lifecycle_state": steering["lifecycle_state"],
"lifecycle_label": steering["lifecycle_label"],
"method_key": steering["method_key"],
diff --git a/backend/routers/initiatives.py b/backend/routers/initiatives.py
index 46ab5b4..30a1585 100644
--- a/backend/routers/initiatives.py
+++ b/backend/routers/initiatives.py
@@ -38,6 +38,7 @@ class InitiativeCreateRequest(BaseModel):
status: Literal["active", "paused", "completed", "archived"] = "active"
priority: Literal["low", "normal", "high"] = "normal"
owner_actor_id: Optional[str] = None
+ method_profile_key: Optional[str] = Field(default=None, min_length=1)
class InitiativeUpdateRequest(BaseModel):
@@ -160,6 +161,7 @@ def create_initiative(
priority=body.priority,
owner_actor_id=owner_actor_id,
user_id=ctx.user_id,
+ method_profile_key=body.method_profile_key,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
diff --git a/backend/routers/steering.py b/backend/routers/steering.py
index 5442e21..128b73c 100644
--- a/backend/routers/steering.py
+++ b/backend/routers/steering.py
@@ -1,12 +1,13 @@
-"""Steering API — methods and context (AP1.1)."""
+"""Steering API — methods, profiles and context (AP1.1 / AP2.0)."""
from __future__ import annotations
from typing import Optional
from capabilities import require_capability
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
+from method_profiles.registry import list_method_profiles
from services import initiatives as initiative_service
from services import steering_context as sc_service
from steering.context import get_steering_context_dto
@@ -17,13 +18,16 @@ router = APIRouter(prefix="/api/steering", tags=["steering"])
class SteeringContextUpdateRequest(BaseModel):
- method_key: str = Field(..., min_length=1)
+ method_key: Optional[str] = Field(default=None, min_length=1)
+ method_profile_key: Optional[str] = Field(default=None, min_length=1)
+ clear_method_profile: bool = False
@router.get("/methods")
def list_steering_methods(
ctx: TenantContext = Depends(require_capability("kairo.initiative.read")),
):
+ _ = ctx
return [
{
"key": m.key,
@@ -36,25 +40,65 @@ def list_steering_methods(
]
+@router.get("/method-profiles")
+def list_steering_method_profiles(
+ initiative_archetype_key: Optional[str] = Query(default=None),
+ ctx: TenantContext = Depends(require_capability("kairo.initiative.read")),
+):
+ _ = ctx
+ return [
+ {
+ "key": p["key"],
+ "label": p["label"],
+ "description": p["description"],
+ "initiative_archetype_key": p["initiative_archetype_key"],
+ "method_key": p["method_key"],
+ }
+ for p in list_method_profiles(initiative_archetype_key=initiative_archetype_key)
+ ]
+
+
@router.patch("/initiatives/{initiative_id}/context")
def patch_initiative_steering_context(
initiative_id: str,
body: SteeringContextUpdateRequest,
ctx: TenantContext = Depends(require_capability("kairo.initiative.manage")),
):
- if not initiative_service.get_initiative(
+ initiative = initiative_service.get_initiative(
tenant_id=ctx.tenant_id, initiative_id=initiative_id
- ):
+ )
+ if not initiative:
raise HTTPException(status_code=404, detail="Vorhaben nicht gefunden")
- if not body.method_key:
- raise HTTPException(status_code=400, detail="method_key erforderlich")
- try:
- sc_service.update_method_key(
- tenant_id=ctx.tenant_id,
- initiative_id=initiative_id,
- method_key=body.method_key,
- user_id=ctx.user_id,
+
+ if not body.method_key and not body.method_profile_key and not body.clear_method_profile:
+ raise HTTPException(
+ status_code=400,
+ detail="method_key, method_profile_key oder clear_method_profile erforderlich",
)
+
+ try:
+ if body.clear_method_profile:
+ sc_service.update_method_profile_key(
+ tenant_id=ctx.tenant_id,
+ initiative_id=initiative_id,
+ method_profile_key=None,
+ user_id=ctx.user_id,
+ )
+ elif body.method_profile_key:
+ sc_service.update_method_profile_key(
+ tenant_id=ctx.tenant_id,
+ initiative_id=initiative_id,
+ method_profile_key=body.method_profile_key,
+ user_id=ctx.user_id,
+ )
+ elif body.method_key:
+ sc_service.update_method_key(
+ tenant_id=ctx.tenant_id,
+ initiative_id=initiative_id,
+ method_key=body.method_key,
+ user_id=ctx.user_id,
+ )
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
+
return get_steering_context_dto(ctx, initiative_id=initiative_id)
diff --git a/backend/services/initiatives.py b/backend/services/initiatives.py
index 0cc4c25..e18e314 100644
--- a/backend/services/initiatives.py
+++ b/backend/services/initiatives.py
@@ -73,6 +73,7 @@ def create_initiative(
status: InitiativeStatus = "active",
priority: Priority = "normal",
user_id: Optional[str] = None,
+ method_profile_key: Optional[str] = None,
) -> dict[str, Any]:
title = title.strip()
if not title:
@@ -125,6 +126,7 @@ def create_initiative(
initiative_id=row["id"],
user_id=user_id,
archetype_key=archetype_key,
+ method_profile_key=method_profile_key,
)
return row
diff --git a/backend/services/steering_context.py b/backend/services/steering_context.py
index 02852aa..3f3d14f 100644
--- a/backend/services/steering_context.py
+++ b/backend/services/steering_context.py
@@ -261,6 +261,89 @@ def update_method_key(
return result
+def update_method_profile_key(
+ *,
+ tenant_id: str,
+ initiative_id: str,
+ method_profile_key: Optional[str],
+ user_id: Optional[str] = None,
+) -> dict[str, Any]:
+ """Setzt oder entfernt method_profile_key; passt method_key aus Profile/Archetyp an."""
+ from entity_archetypes.registry import resolve_default_method_key
+ from method_profiles.registry import get_method_profile
+ from steering.methods.registry import get_method
+
+ initiative = get_initiative(tenant_id=tenant_id, initiative_id=initiative_id)
+ if not initiative:
+ raise ValueError("Vorhaben nicht gefunden")
+
+ existing = get_steering_context(tenant_id=tenant_id, initiative_id=initiative_id)
+ if not existing:
+ raise ValueError("SteeringContext nicht gefunden")
+
+ metadata = dict(existing.get("lifecycle_metadata") or {})
+ previous_profile = metadata.get("method_profile_key")
+
+ if method_profile_key:
+ profile = get_method_profile(method_profile_key)
+ if not profile:
+ raise ValueError(f"Unbekannte Ausprägung: {method_profile_key}")
+ if profile["initiative_archetype_key"] != initiative["archetype_key"]:
+ raise ValueError("Ausprägung passt nicht zum Archetyp des Vorhabens")
+ metadata["method_profile_key"] = method_profile_key
+ resolved_method = profile["method_key"]
+ else:
+ metadata.pop("method_profile_key", None)
+ resolved_method = resolve_default_method_key(initiative["archetype_key"])
+
+ method = get_method(resolved_method)
+ if not method:
+ raise ValueError(f"Unbekannte Methode: {resolved_method}")
+
+ conn = get_connection()
+ try:
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
+ cur.execute(
+ """
+ UPDATE steering_contexts
+ SET method_key = %s,
+ method_version = %s,
+ lifecycle_metadata = %s,
+ updated_at = NOW()
+ WHERE tenant_id = %s AND initiative_id = %s
+ RETURNING id, tenant_id, initiative_id, method_key, method_version,
+ lifecycle_state, lifecycle_metadata, created_at, updated_at
+ """,
+ (
+ method.key,
+ method.version,
+ Json(metadata),
+ tenant_id,
+ initiative_id,
+ ),
+ )
+ row = cur.fetchone()
+ if not row:
+ raise ValueError("SteeringContext nicht gefunden")
+ result = _serialize_row(dict(row))
+ conn.commit()
+ finally:
+ conn.close()
+
+ log_audit(
+ "steering_context.method_profile_changed",
+ user_id=user_id,
+ tenant_id=tenant_id,
+ details={
+ "initiative_id": initiative_id,
+ "from_profile": previous_profile,
+ "to_profile": method_profile_key,
+ "method_key": method.key,
+ },
+ )
+ return result
+
+
def create_context_for_new_initiative(
*,
tenant_id: str,
@@ -275,6 +358,13 @@ def create_context_for_new_initiative(
method_key = resolve_default_method_key(archetype_key)
lifecycle_metadata: dict[str, Any] = {}
if method_profile_key:
+ from method_profiles.registry import get_method_profile
+
+ profile = get_method_profile(method_profile_key)
+ if not profile:
+ raise ValueError(f"Unbekannte Ausprägung: {method_profile_key}")
+ if profile["initiative_archetype_key"] != archetype_key:
+ raise ValueError("Ausprägung passt nicht zum Archetyp des Vorhabens")
profile_method = resolve_method_for_profile(method_profile_key)
if profile_method:
method_key = profile_method
diff --git a/backend/steering/context.py b/backend/steering/context.py
index 23d10a2..693b0f6 100644
--- a/backend/steering/context.py
+++ b/backend/steering/context.py
@@ -22,10 +22,23 @@ def get_steering_context_dto(
initiative_id=initiative_id,
)
method = get_method(row["method_key"])
+ metadata = row.get("lifecycle_metadata") or {}
+ if isinstance(metadata, str):
+ metadata = {}
+ method_profile_key = metadata.get("method_profile_key")
+ method_profile_label = None
+ if method_profile_key:
+ from method_profiles.registry import get_method_profile
+
+ profile = get_method_profile(method_profile_key)
+ if profile:
+ method_profile_label = profile.get("label")
return {
**row,
"lifecycle_label": lifecycle_label(row["lifecycle_state"]),
"method_label": method.label if method else row["method_key"],
+ "method_profile_key": method_profile_key,
+ "method_profile_label": method_profile_label,
}
diff --git a/backend/steering/signals/default_rules.py b/backend/steering/signals/default_rules.py
index a3d8b12..9809f33 100644
--- a/backend/steering/signals/default_rules.py
+++ b/backend/steering/signals/default_rules.py
@@ -63,7 +63,7 @@ def _blocked_actions(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'blocked_action' AS kind,
'critical' AS severity,
a.title AS title,
- 'Maßnahme ist blockiert' AS summary,
+ 'Maßnahme ist blockiert' AS summary,
'action' AS scope_type,
a.id AS scope_id,
a.initiative_id,
@@ -136,7 +136,7 @@ def _high_priority_actions(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'high_priority_action' AS kind,
'warning' AS severity,
a.title AS title,
- 'High-Priority Maßnahme offen' AS summary,
+ 'High-Priority Maßnahme offen' AS summary,
'action' AS scope_type,
a.id AS scope_id,
a.initiative_id,
@@ -165,7 +165,7 @@ def _unassigned_actions(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'unassigned_action' AS kind,
'warning' AS severity,
a.title AS title,
- 'Maßnahme ohne Zuweisung' AS summary,
+ 'Maßnahme ohne Zuweisung' AS summary,
'action' AS scope_type,
a.id AS scope_id,
a.initiative_id,
@@ -196,7 +196,7 @@ def _initiatives_without_next_action(cur, ctx: TenantContext) -> list[dict[str,
'initiative_without_next_action' AS kind,
'info' AS severity,
i.title AS title,
- 'Keine offene nächste Maßnahme' AS summary,
+ 'Keine offene nächste Maßnahme' AS summary,
'initiative' AS scope_type,
i.id AS scope_id,
i.id AS initiative_id,
@@ -229,7 +229,7 @@ def _stale_initiatives(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'stale_initiative' AS kind,
'info' AS severity,
i.title AS title,
- 'Vorhaben seit über 14 Tagen unverändert' AS summary,
+ 'Vorhaben seit über 14 Tagen unverändert' AS summary,
'initiative' AS scope_type,
i.id AS scope_id,
i.id AS initiative_id,
@@ -284,7 +284,7 @@ def _actions_review_required(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'action_review_required' AS kind,
'warning' AS severity,
a.title AS title,
- 'Ma├ƒnahme wartet auf Review ÔÇö kein geplantes Review verkn├╝pft' AS summary,
+ 'Maßnahme wartet auf Review — kein geplantes Review verknüpft' AS summary,
'action' AS scope_type,
a.id AS scope_id,
a.initiative_id,
@@ -319,7 +319,7 @@ def _overdue_actions(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'overdue_action' AS kind,
'warning' AS severity,
a.title AS title,
- 'Maßnahme überfällig' AS summary,
+ 'Maßnahme überfällig' AS summary,
'action' AS scope_type,
a.id AS scope_id,
a.initiative_id,
@@ -350,7 +350,7 @@ def _reviews_due(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'review_due' AS kind,
'warning' AS severity,
r.title AS title,
- 'Review fällig' AS summary,
+ 'Review fällig' AS summary,
'review' AS scope_type,
r.id AS scope_id,
r.initiative_id,
@@ -388,7 +388,7 @@ def _recurring_due(cur, ctx: TenantContext) -> list[dict[str, Any]]:
'recurring_due' AS kind,
'info' AS severity,
re.title AS title,
- 'Wiederkehrendes Element fällig' AS summary,
+ 'Wiederkehrendes Element fällig' AS summary,
'recurring_element' AS scope_type,
re.id AS scope_id,
re.initiative_id,
@@ -413,7 +413,7 @@ def _recurring_due(cur, ctx: TenantContext) -> list[dict[str, Any]]:
def get_attention_items(ctx: TenantContext) -> list[dict[str, Any]]:
- """Regelbasierte Attention Items ÔÇö tenant-scoped, erkl├ñrbar."""
+ """Regelbasierte Attention Items — tenant-scoped, erklärbar."""
conn = get_connection()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
@@ -439,7 +439,7 @@ def get_attention_items(ctx: TenantContext) -> list[dict[str, Any]]:
def get_next_action_candidates(
ctx: TenantContext, *, limit: int = 10
) -> list[dict[str, Any]]:
- """Regelbasierte NextActionCandidates ÔÇö limitiert, tenant-scoped."""
+ """Regelbasierte NextActionCandidates — limitiert, tenant-scoped."""
if limit < 1:
limit = 1
if limit > 50:
@@ -454,7 +454,7 @@ def get_next_action_candidates(
SELECT
'resolve_blocker' AS kind,
b.title AS title,
- 'Blocker klären oder Status aktualisieren' AS summary,
+ 'Blocker klären oder Status aktualisieren' AS summary,
b.initiative_id,
b.action_id,
NULL::uuid AS backlog_item_id,
@@ -486,7 +486,7 @@ def get_next_action_candidates(
a.id AS action_id,
NULL::uuid AS backlog_item_id,
'action_unassigned' AS reason_code,
- 'Maßnahme zuweisen' AS recommended_action
+ 'Maßnahme zuweisen' AS recommended_action
FROM actions a
WHERE a.tenant_id = %s
AND a.status IN ('open', 'ready', 'in_progress')
@@ -512,12 +512,12 @@ def get_next_action_candidates(
SELECT
'convert_backlog' AS kind,
bi.title AS title,
- 'Freigegebenes Backlog-Item in Maßnahme umwandeln' AS summary,
+ 'Freigegebenes Backlog-Item in Maßnahme umwandeln' AS summary,
bi.initiative_id,
NULL::uuid AS action_id,
bi.id AS backlog_item_id,
'backlog_accepted_not_converted' AS reason_code,
- 'In Maßnahme umwandeln' AS recommended_action
+ 'In Maßnahme umwandeln' AS recommended_action
FROM backlog_items bi
WHERE bi.tenant_id = %s
AND bi.status = 'accepted'
@@ -540,12 +540,12 @@ def get_next_action_candidates(
SELECT
'create_action' AS kind,
i.title AS title,
- 'Nächste Maßnahme für Vorhaben anlegen' AS summary,
+ 'Nächste Maßnahme für Vorhaben anlegen' AS summary,
i.id AS initiative_id,
NULL::uuid AS action_id,
NULL::uuid AS backlog_item_id,
'initiative_no_open_action' AS reason_code,
- 'Maßnahme anlegen' AS recommended_action
+ 'Maßnahme anlegen' AS recommended_action
FROM initiatives i
WHERE i.tenant_id = %s
AND i.status IN ('active', 'paused')
@@ -573,7 +573,7 @@ def get_next_action_candidates(
def get_next_action_candidates_for_initiative(
ctx: TenantContext, *, initiative_id: str, limit: int = 5
) -> list[dict[str, Any]]:
- """NextActionCandidates f├╝r ein Vorhaben ÔÇö tenant-scoped."""
+ """NextActionCandidates für ein Vorhaben — tenant-scoped."""
if limit < 1:
limit = 1
if limit > 20:
@@ -588,7 +588,7 @@ def get_next_action_candidates_for_initiative(
SELECT
'resolve_blocker' AS kind,
b.title AS title,
- 'Blocker klären oder Status aktualisieren' AS summary,
+ 'Blocker klären oder Status aktualisieren' AS summary,
b.initiative_id,
b.action_id,
NULL::uuid AS backlog_item_id,
@@ -621,7 +621,7 @@ def get_next_action_candidates_for_initiative(
a.id AS action_id,
NULL::uuid AS backlog_item_id,
'action_unassigned' AS reason_code,
- 'Maßnahme zuweisen' AS recommended_action
+ 'Maßnahme zuweisen' AS recommended_action
FROM actions a
WHERE a.tenant_id = %s AND a.initiative_id = %s
AND a.status IN ('open', 'ready', 'in_progress')
@@ -647,12 +647,12 @@ def get_next_action_candidates_for_initiative(
SELECT
'convert_backlog' AS kind,
bi.title AS title,
- 'Freigegebenes Backlog-Item in Maßnahme umwandeln' AS summary,
+ 'Freigegebenes Backlog-Item in Maßnahme umwandeln' AS summary,
bi.initiative_id,
NULL::uuid AS action_id,
bi.id AS backlog_item_id,
'backlog_accepted_not_converted' AS reason_code,
- 'In Maßnahme umwandeln' AS recommended_action
+ 'In Maßnahme umwandeln' AS recommended_action
FROM backlog_items bi
WHERE bi.tenant_id = %s AND bi.initiative_id = %s
AND bi.status = 'accepted'
@@ -693,12 +693,12 @@ def get_next_action_candidates_for_initiative(
{
"kind": "create_action",
"title": init_row["title"],
- "summary": "Nächste Maßnahme für Vorhaben anlegen",
+ "summary": "Nächste Maßnahme für Vorhaben anlegen",
"initiative_id": initiative_id,
"action_id": None,
"backlog_item_id": None,
"reason_code": "initiative_no_open_action",
- "recommended_action": "Maßnahme anlegen",
+ "recommended_action": "Maßnahme anlegen",
}
)
diff --git a/backend/tests/test_ap20_method_profiles.py b/backend/tests/test_ap20_method_profiles.py
new file mode 100644
index 0000000..a648bb2
--- /dev/null
+++ b/backend/tests/test_ap20_method_profiles.py
@@ -0,0 +1,73 @@
+"""Tests for method profile API — AP2.0 capture slice."""
+
+from __future__ import annotations
+
+from tests.factories import provision_user_in_tenant
+from tests.test_initiatives_actions import _auth, _create_initiative, _login
+
+
+def test_method_profiles_list_for_product_archetype(client):
+ user = provision_user_in_tenant(tenant_role="member")
+ token = _login(client, user)
+
+ res = client.get(
+ "/api/steering/method-profiles?initiative_archetype_key=initiative.product",
+ headers=_auth(token),
+ )
+ assert res.status_code == 200
+ keys = {item["key"] for item in res.json()}
+ assert "product.kairo_dev" in keys
+
+
+def test_create_initiative_with_method_profile(client):
+ user = provision_user_in_tenant(tenant_role="member")
+ token = _login(client, user)
+
+ created = client.post(
+ "/api/initiatives",
+ json={
+ "title": "Kairo Dev",
+ "archetype_key": "initiative.product",
+ "method_profile_key": "product.kairo_dev",
+ },
+ headers=_auth(token),
+ )
+ assert created.status_code == 201
+ initiative_id = created.json()["id"]
+
+ snap = client.get(
+ f"/api/initiatives/{initiative_id}/steering-snapshot",
+ headers=_auth(token),
+ )
+ assert snap.status_code == 200
+ body = snap.json()
+ assert body["method_profile_key"] == "product.kairo_dev"
+ assert body.get("method_profile_label") == "Kairo Entwicklung"
+ assert body["method_key"] == "continuous_product"
+
+
+def test_patch_method_profile_on_initiative(client):
+ user = provision_user_in_tenant(tenant_role="member")
+ token = _login(client, user)
+
+ created = _create_initiative(
+ client,
+ token,
+ title="Product X",
+ archetype_key="initiative.product",
+ )
+ initiative_id = created.json()["id"]
+
+ patched = client.patch(
+ f"/api/steering/initiatives/{initiative_id}/context",
+ json={"method_profile_key": "product.kairo_dev"},
+ headers=_auth(token),
+ )
+ assert patched.status_code == 200
+ assert patched.json()["method_key"] == "continuous_product"
+
+ snap = client.get(
+ f"/api/initiatives/{initiative_id}/steering-snapshot",
+ headers=_auth(token),
+ )
+ assert snap.json()["method_profile_label"] == "Kairo Entwicklung"
diff --git a/frontend/src/api/methodProfiles.js b/frontend/src/api/methodProfiles.js
new file mode 100644
index 0000000..8f6674e
--- /dev/null
+++ b/frontend/src/api/methodProfiles.js
@@ -0,0 +1,10 @@
+import { apiFetch } from './client.js'
+
+export function listMethodProfiles(initiativeArchetypeKey) {
+ const params = new URLSearchParams()
+ if (initiativeArchetypeKey) {
+ params.set('initiative_archetype_key', initiativeArchetypeKey)
+ }
+ const qs = params.toString()
+ return apiFetch(`/api/steering/method-profiles${qs ? `?${qs}` : ''}`)
+}
diff --git a/frontend/src/api/steering.js b/frontend/src/api/steering.js
index f505012..65fece5 100644
--- a/frontend/src/api/steering.js
+++ b/frontend/src/api/steering.js
@@ -10,3 +10,13 @@ export function updateInitiativeSteeringMethod(initiativeId, methodKey) {
body: JSON.stringify({ method_key: methodKey }),
})
}
+
+export function updateInitiativeMethodProfile(initiativeId, methodProfileKey) {
+ const body = methodProfileKey
+ ? { method_profile_key: methodProfileKey }
+ : { clear_method_profile: true }
+ return apiFetch(`/api/steering/initiatives/${initiativeId}/context`, {
+ method: 'PATCH',
+ body: JSON.stringify(body),
+ })
+}
diff --git a/frontend/src/components/ArchetypeBadge.jsx b/frontend/src/components/ArchetypeBadge.jsx
new file mode 100644
index 0000000..b479ce1
--- /dev/null
+++ b/frontend/src/components/ArchetypeBadge.jsx
@@ -0,0 +1,11 @@
+import { archetypeLabel } from '../utils/archetypes.js'
+
+export function ArchetypeBadge({ archetypeKey, archetypes = [], title }) {
+ if (!archetypeKey) return null
+ const label = archetypeLabel(archetypeKey, archetypes)
+ return (
+
+ {label}
+
+ )
+}
diff --git a/frontend/src/components/InitiativeForm.jsx b/frontend/src/components/InitiativeForm.jsx
index 40cc3af..515dc4d 100644
--- a/frontend/src/components/InitiativeForm.jsx
+++ b/frontend/src/components/InitiativeForm.jsx
@@ -8,7 +8,10 @@ import {
getInitiativeFields,
listArchetypeFieldDefinitions,
} from '../api/entityFields.js'
+import { listSteeringMethods } from '../api/steering.js'
+import { listMethodProfiles } from '../api/methodProfiles.js'
import { FieldRenderer } from './FieldRenderer.jsx'
+import { InitiativeSteeringMeta } from './InitiativeSteeringMeta.jsx'
export function InitiativeForm({
initial = {},
@@ -23,6 +26,42 @@ export function InitiativeForm({
const [fieldDefinitions, setFieldDefinitions] = useState([])
const [dynamicValues, setDynamicValues] = useState({})
const [fieldsLoading, setFieldsLoading] = useState(false)
+ const [steeringMethods, setSteeringMethods] = useState([])
+ const [methodProfiles, setMethodProfiles] = useState([])
+ const [methodProfileKey, setMethodProfileKey] = useState('')
+
+ useEffect(() => {
+ let cancelled = false
+ listSteeringMethods()
+ .then((items) => {
+ if (!cancelled) setSteeringMethods(Array.isArray(items) ? items : [])
+ })
+ .catch(() => {
+ if (!cancelled) setSteeringMethods([])
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ useEffect(() => {
+ let cancelled = false
+ setMethodProfileKey('')
+ if (!archetypeKey) {
+ setMethodProfiles([])
+ return undefined
+ }
+ listMethodProfiles(archetypeKey)
+ .then((items) => {
+ if (!cancelled) setMethodProfiles(Array.isArray(items) ? items : [])
+ })
+ .catch(() => {
+ if (!cancelled) setMethodProfiles([])
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [archetypeKey])
useEffect(() => {
let cancelled = false
@@ -91,6 +130,7 @@ export function InitiativeForm({
status: form.status.value,
priority: form.priority.value,
dynamicFields: dynamicValues,
+ ...(methodProfileKey ? { method_profile_key: methodProfileKey } : {}),
})
}
@@ -121,6 +161,30 @@ export function InitiativeForm({
))}
+ {methodProfiles.length > 0 && (
+
+ )}
+
- Attention:{' '} + Aufmerksamkeit:{' '} {(attention_items.length > 0 ? attention_items.map((a) => a.label) : signals.map((s) => SIGNAL_LABELS[s] || s) @@ -146,19 +180,25 @@ export function SteeringSnapshotPanel({
Aktive und pausierte Vorhaben im Tenant.
++ Alle aktiven Vorhaben — Archetyp und Steuerung im Profil und unter Kontrolle einsehen. +
{error}
} +{initiativeArchetypeLabel(initiative.archetype_key)}
+{ownerName}
++ {projects.length} Projekt{projects.length === 1 ? '' : 'e'} ·{' '} + + Struktur pflegen + +
+
+
{item.goal}
)}