AP2.0e rotiert Recurring bei maturity_stage reached. Maturity-Strategie priorisiert Routinen. Team zeigt Namen und Agent-Formular. Archetyp-Hints in Kontrolle und Stufe-A-Gruppierung bei Anlage. Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
8.5 KiB
JavaScript
269 lines
8.5 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { INITIATIVE_STATUSES, PRIORITIES } 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 { listEntityArchetypes, getArchetypeStarterPreview } from '../api/entityArchetypes.js'
|
|
import {
|
|
getInitiativeFields,
|
|
listArchetypeFieldDefinitions,
|
|
} from '../api/entityFields.js'
|
|
import { FieldRenderer } from './FieldRenderer.jsx'
|
|
|
|
export function InitiativeForm({
|
|
initial = {},
|
|
initiativeId = null,
|
|
actors = [],
|
|
onSubmit,
|
|
onCancel,
|
|
busy = false,
|
|
submitLabel = 'Speichern',
|
|
}) {
|
|
const [archetypes, setArchetypes] = useState(null)
|
|
const [archetypeKey, setArchetypeKey] = useState(initial.archetype_key || 'initiative.generic')
|
|
const [fieldDefinitions, setFieldDefinitions] = useState([])
|
|
const [dynamicValues, setDynamicValues] = useState({})
|
|
const [fieldsLoading, setFieldsLoading] = useState(false)
|
|
const [starterPreview, setStarterPreview] = useState(null)
|
|
const [starterLoading, setStarterLoading] = useState(false)
|
|
const isCreate = !initiativeId
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
listEntityArchetypes('initiative')
|
|
.then((items) => {
|
|
if (!cancelled) setArchetypes(items)
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) {
|
|
setArchetypes(
|
|
Object.entries(INITIATIVE_ARCHETYPE_LABELS).map(([key, label]) => ({
|
|
key,
|
|
label,
|
|
})),
|
|
)
|
|
}
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!archetypeKey) return undefined
|
|
let cancelled = false
|
|
setFieldsLoading(true)
|
|
|
|
async function loadFields() {
|
|
try {
|
|
const definitions = await listArchetypeFieldDefinitions('initiative', archetypeKey)
|
|
let values = {}
|
|
if (initiativeId) {
|
|
values = await getInitiativeFields(initiativeId)
|
|
}
|
|
const validKeys = new Set(definitions.map((def) => def.field_key))
|
|
const filteredValues = Object.fromEntries(
|
|
Object.entries(values || {}).filter(([key]) => validKeys.has(key)),
|
|
)
|
|
if (!cancelled) {
|
|
setFieldDefinitions(definitions)
|
|
setDynamicValues(filteredValues)
|
|
}
|
|
} catch {
|
|
if (!cancelled) {
|
|
setFieldDefinitions([])
|
|
setDynamicValues({})
|
|
}
|
|
} finally {
|
|
if (!cancelled) setFieldsLoading(false)
|
|
}
|
|
}
|
|
|
|
loadFields()
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [initiativeId, archetypeKey])
|
|
|
|
useEffect(() => {
|
|
if (!isCreate || !archetypeKey) {
|
|
setStarterPreview(null)
|
|
return undefined
|
|
}
|
|
let cancelled = false
|
|
setStarterLoading(true)
|
|
getArchetypeStarterPreview(archetypeKey)
|
|
.then((preview) => {
|
|
if (!cancelled) setStarterPreview(preview)
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setStarterPreview(null)
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setStarterLoading(false)
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [isCreate, archetypeKey])
|
|
|
|
async function handleSubmit(e) {
|
|
e.preventDefault()
|
|
const form = e.target
|
|
await onSubmit({
|
|
title: form.title.value.trim(),
|
|
archetype_key: form.archetype_key.value,
|
|
goal: form.goal.value,
|
|
vision: form.vision.value,
|
|
target_state_summary: form.target_state_summary.value,
|
|
status: form.status.value,
|
|
priority: form.priority.value,
|
|
owner_actor_id: form.owner_actor_id?.value || undefined,
|
|
dynamicFields: dynamicValues,
|
|
apply_starter_kit: isCreate && starterPreview?.supported !== false,
|
|
})
|
|
}
|
|
|
|
function handleDynamicChange(fieldKey, value) {
|
|
setDynamicValues((prev) => ({ ...prev, [fieldKey]: value }))
|
|
}
|
|
|
|
const formReady = archetypes && !fieldsLoading
|
|
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 (
|
|
<form className="form workspace-form initiative-form" onSubmit={handleSubmit}>
|
|
<fieldset className="initiative-form__section">
|
|
<legend>Identität</legend>
|
|
<label>
|
|
Titel
|
|
<input name="title" defaultValue={initial.title || ''} required maxLength={255} />
|
|
</label>
|
|
<label>
|
|
Archetyp
|
|
<select
|
|
name="archetype_key"
|
|
value={archetypeKey}
|
|
onChange={(e) => setArchetypeKey(e.target.value)}
|
|
disabled={!archetypes}
|
|
>
|
|
{stufeA.length > 0 && (
|
|
<optgroup label="MVP Stufe A (empfohlen)">
|
|
{stufeA.map((item) => (
|
|
<option key={item.key} value={item.key}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</optgroup>
|
|
)}
|
|
{otherArchetypes.length > 0 && (
|
|
<optgroup label="Weitere Archetypen">
|
|
{otherArchetypes.map((item) => (
|
|
<option key={item.key} value={item.key}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</optgroup>
|
|
)}
|
|
</select>
|
|
{selectedArchetype?.description && (
|
|
<span className="muted form-hint">{selectedArchetype.description}</span>
|
|
)}
|
|
</label>
|
|
{isCreate && starterPreview?.supported && (
|
|
<div className="starter-kit-preview card-list-item compact">
|
|
<p className="muted form-hint">
|
|
<strong>Starter-Struktur:</strong>{' '}
|
|
{starterLoading
|
|
? ' wird geladen …'
|
|
: starterPreview.items.map((item) => item.title).join(' · ')}
|
|
</p>
|
|
{starterPreview.guidance && (
|
|
<p className="muted form-hint">{starterPreview.guidance}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
{actors.length > 0 && (
|
|
<label>
|
|
Verantwortlich (Owner)
|
|
<select name="owner_actor_id" defaultValue={initial.owner_actor_id || ''}>
|
|
<option value="">—</option>
|
|
{actors.map((actor) => (
|
|
<option key={actor.id} value={actor.id}>
|
|
{actor.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
)}
|
|
</fieldset>
|
|
|
|
<fieldset className="initiative-form__section">
|
|
<legend>Programm</legend>
|
|
<label>
|
|
Kurzbeschreibung (Ziel)
|
|
<textarea name="goal" rows={2} defaultValue={initial.goal || ''} />
|
|
</label>
|
|
<label>
|
|
Vision
|
|
<textarea name="vision" rows={4} defaultValue={initial.vision || ''} />
|
|
</label>
|
|
<label>
|
|
Zielzustand (Kurzfassung)
|
|
<textarea
|
|
name="target_state_summary"
|
|
rows={2}
|
|
defaultValue={initial.target_state_summary || ''}
|
|
/>
|
|
</label>
|
|
</fieldset>
|
|
|
|
<FieldRenderer
|
|
definitions={fieldDefinitions}
|
|
values={dynamicValues}
|
|
onChange={handleDynamicChange}
|
|
disabled={busy || fieldsLoading}
|
|
actors={actors}
|
|
/>
|
|
|
|
<fieldset className="initiative-form__section">
|
|
<legend>Steuerung</legend>
|
|
<div className="form-row form-row--2">
|
|
<label>
|
|
Status
|
|
<select name="status" defaultValue={initial.status || 'active'}>
|
|
{INITIATIVE_STATUSES.map((s) => (
|
|
<option key={s} value={s}>
|
|
{INITIATIVE_STATUS_LABELS[s]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Priorität
|
|
<select name="priority" defaultValue={initial.priority || 'normal'}>
|
|
{PRIORITIES.map((p) => (
|
|
<option key={p} value={p}>
|
|
{PRIORITY_LABELS[p]}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</fieldset>
|
|
|
|
<div className="form-actions">
|
|
<button type="submit" className="btn btn-primary" disabled={busy || !formReady}>
|
|
{busy ? 'Speichern …' : submitLabel}
|
|
</button>
|
|
{onCancel && (
|
|
<button type="button" className="btn btn-secondary" onClick={onCancel} disabled={busy}>
|
|
Abbrechen
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
)
|
|
}
|