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 && ( + + )} +