AP1.10c: Profil-Modal mit Archetyp, EFS und Owner.
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 2m13s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 2m13s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
InitiativeForm laedt Felder nach gewaehltem Archetyp; Plan-Profil zeigt Standardfelder und Zusatzfelder; actor_ref und Owner-Auswahl. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
43d30944aa
commit
dbe6f4f684
|
|
@ -1,11 +1,11 @@
|
|||
export function FieldRenderer({ definitions, values, onChange, disabled = false }) {
|
||||
export function FieldRenderer({ definitions, values, onChange, disabled = false, actors = [] }) {
|
||||
if (!definitions?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset className="field-renderer">
|
||||
<legend className="field-renderer__legend">Zusatzfelder</legend>
|
||||
<legend className="field-renderer__legend">Zusatzfelder (Archetyp)</legend>
|
||||
{definitions.map((def) => (
|
||||
<DynamicField
|
||||
key={def.field_key}
|
||||
|
|
@ -13,13 +13,14 @@ export function FieldRenderer({ definitions, values, onChange, disabled = false
|
|||
value={values?.[def.field_key]}
|
||||
onChange={(next) => onChange(def.field_key, next)}
|
||||
disabled={disabled}
|
||||
actors={actors}
|
||||
/>
|
||||
))}
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
function DynamicField({ definition, value, onChange, disabled }) {
|
||||
function DynamicField({ definition, value, onChange, disabled, actors = [] }) {
|
||||
const { field_key, field_type, label, required, validation_json: validation } = definition
|
||||
const inputId = `field-${field_key}`
|
||||
|
||||
|
|
@ -83,6 +84,30 @@ function DynamicField({ definition, value, onChange, disabled }) {
|
|||
)
|
||||
}
|
||||
|
||||
if (field_type === 'actor_ref') {
|
||||
return (
|
||||
<label htmlFor={inputId}>
|
||||
{label}
|
||||
{required && ' *'}
|
||||
<select
|
||||
id={inputId}
|
||||
name={field_key}
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.value || null)}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{actors.map((actor) => (
|
||||
<option key={actor.id} value={actor.id}>
|
||||
{actor.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
const inputType =
|
||||
field_type === 'number' ? 'number' : field_type === 'date' ? 'date' : field_type === 'url' ? 'url' : 'text'
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { INITIATIVE_STATUS_LABELS, PRIORITY_LABELS } from '../constants/status.j
|
|||
import { INITIATIVE_ARCHETYPE_LABELS } from '../constants/initiativeArchetypes.js'
|
||||
import { listEntityArchetypes } from '../api/entityArchetypes.js'
|
||||
import {
|
||||
getInitiativeFieldDefinitions,
|
||||
getInitiativeFields,
|
||||
listArchetypeFieldDefinitions,
|
||||
} from '../api/entityFields.js'
|
||||
|
|
@ -13,6 +12,7 @@ import { FieldRenderer } from './FieldRenderer.jsx'
|
|||
export function InitiativeForm({
|
||||
initial = {},
|
||||
initiativeId = null,
|
||||
actors = [],
|
||||
onSubmit,
|
||||
onCancel,
|
||||
busy = false,
|
||||
|
|
@ -46,22 +46,24 @@ export function InitiativeForm({
|
|||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!archetypeKey) return undefined
|
||||
let cancelled = false
|
||||
setFieldsLoading(true)
|
||||
|
||||
async function loadFields() {
|
||||
try {
|
||||
let definitions = []
|
||||
const definitions = await listArchetypeFieldDefinitions('initiative', archetypeKey)
|
||||
let values = {}
|
||||
if (initiativeId) {
|
||||
definitions = await getInitiativeFieldDefinitions(initiativeId)
|
||||
values = await getInitiativeFields(initiativeId)
|
||||
} else if (archetypeKey) {
|
||||
definitions = await listArchetypeFieldDefinitions('initiative', archetypeKey)
|
||||
}
|
||||
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(values)
|
||||
setDynamicValues(filteredValues)
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
|
|
@ -90,6 +92,7 @@ export function InitiativeForm({
|
|||
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,
|
||||
})
|
||||
}
|
||||
|
|
@ -102,73 +105,100 @@ export function InitiativeForm({
|
|||
const selectedArchetype = (archetypes || []).find((item) => item.key === archetypeKey)
|
||||
|
||||
return (
|
||||
<form className="form workspace-form" onSubmit={handleSubmit}>
|
||||
<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}
|
||||
>
|
||||
{(archetypes || []).map((item) => (
|
||||
<option key={item.key} value={item.key}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedArchetype?.description && (
|
||||
<span className="muted form-hint">{selectedArchetype.description}</span>
|
||||
<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}
|
||||
>
|
||||
{(archetypes || []).map((item) => (
|
||||
<option key={item.key} value={item.key}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedArchetype?.description && (
|
||||
<span className="muted form-hint">{selectedArchetype.description}</span>
|
||||
)}
|
||||
</label>
|
||||
{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>
|
||||
)}
|
||||
</label>
|
||||
<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>
|
||||
|
||||
<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}
|
||||
/>
|
||||
<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 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}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
|
|||
import { Link } from 'react-router-dom'
|
||||
import { RequireInitiativeScope } from '../../components/RequireInitiativeScope.jsx'
|
||||
import { ScopedInitiativeProvider } from '../../layout/ScopedInitiativeProvider.jsx'
|
||||
import { ArchetypeBadge } from '../../components/ArchetypeBadge.jsx'
|
||||
import { StatusBadge } from '../../components/StatusBadge.jsx'
|
||||
import { LoadingState } from '../../components/LoadingState.jsx'
|
||||
import { Modal } from '../../components/Modal.jsx'
|
||||
|
|
@ -17,13 +18,15 @@ import {
|
|||
getInitiativeFields,
|
||||
} from '../../api/entityFields.js'
|
||||
|
||||
function DynamicFieldsDisplay({ initiativeId }) {
|
||||
function DynamicFieldsDisplay({ initiativeId, refreshKey = 0 }) {
|
||||
const [definitions, setDefinitions] = useState([])
|
||||
const [values, setValues] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [defs, fieldValues] = await Promise.all([
|
||||
getInitiativeFieldDefinitions(initiativeId),
|
||||
|
|
@ -38,15 +41,21 @@ function DynamicFieldsDisplay({ initiativeId }) {
|
|||
setDefinitions([])
|
||||
setValues({})
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [initiativeId])
|
||||
}, [initiativeId, refreshKey])
|
||||
|
||||
if (!definitions.length || values === null) {
|
||||
if (loading || values === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!definitions.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -57,15 +66,25 @@ function DynamicFieldsDisplay({ initiativeId }) {
|
|||
})
|
||||
|
||||
if (!visible.length) {
|
||||
return null
|
||||
return (
|
||||
<div className="plan-profile__field">
|
||||
<h3 className="plan-profile__label">Zusatzfelder (Archetyp)</h3>
|
||||
<p className="muted">Noch keine Werte für die Felder dieses Archetyps.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return visible.map((def) => (
|
||||
<div key={def.field_key} className="plan-profile__field">
|
||||
<h3 className="plan-profile__label">{def.label}</h3>
|
||||
<p>{formatFieldValue(def, values[def.field_key])}</p>
|
||||
return (
|
||||
<div className="plan-profile__section">
|
||||
<h3 className="plan-profile__section-title">Zusatzfelder (Archetyp)</h3>
|
||||
{visible.map((def) => (
|
||||
<div key={def.field_key} className="plan-profile__field">
|
||||
<h4 className="plan-profile__label">{def.label}</h4>
|
||||
<p>{formatFieldValue(def, values[def.field_key])}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
function formatFieldValue(definition, value) {
|
||||
|
|
@ -91,6 +110,7 @@ function PlanProfileInner() {
|
|||
projects,
|
||||
} = ops
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [fieldsRefreshKey, setFieldsRefreshKey] = useState(0)
|
||||
const canManage = capabilities.has('kairo.initiative.manage')
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -110,6 +130,7 @@ function PlanProfileInner() {
|
|||
const ok = await ops.handleUpdateInitiative(payload)
|
||||
if (ok) {
|
||||
refreshInitiativeMeta()
|
||||
setFieldsRefreshKey((key) => key + 1)
|
||||
setEditing(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +139,13 @@ function PlanProfileInner() {
|
|||
<>
|
||||
<section className="card plan-profile">
|
||||
<header className="plan-profile__header">
|
||||
<h2 className="card-title">{initiative.title}</h2>
|
||||
<div>
|
||||
<h2 className="card-title">{initiative.title}</h2>
|
||||
<ArchetypeBadge
|
||||
archetypeKey={initiative.archetype_key}
|
||||
archetypes={initiativeArchetypes}
|
||||
/>
|
||||
</div>
|
||||
<div className="plan-profile__header-actions">
|
||||
<StatusBadge kind="initiative" status={initiative.status} />
|
||||
{canManage && (
|
||||
|
|
@ -165,27 +192,29 @@ function PlanProfileInner() {
|
|||
<p>{initiative.goal}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted">Noch keine Kurzbeschreibung hinterlegt.</p>
|
||||
<p className="muted plan-profile__empty-hint">Noch keine Kurzbeschreibung hinterlegt.</p>
|
||||
)}
|
||||
{initiative.vision && (
|
||||
<div className="plan-profile__field">
|
||||
<h3 className="plan-profile__label">Vision</h3>
|
||||
<p>{initiative.vision}</p>
|
||||
</div>
|
||||
)}
|
||||
{initiative.target_state_summary && (
|
||||
<div className="plan-profile__field">
|
||||
<h3 className="plan-profile__label">Zielzustand</h3>
|
||||
<p>{initiative.target_state_summary}</p>
|
||||
</div>
|
||||
)}
|
||||
<DynamicFieldsDisplay initiativeId={initiative.id} key={editing ? 'closed' : 'open'} />
|
||||
<div className="plan-profile__field">
|
||||
<h3 className="plan-profile__label">Vision</h3>
|
||||
<p>{initiative.vision?.trim() ? initiative.vision : '—'}</p>
|
||||
</div>
|
||||
<div className="plan-profile__field">
|
||||
<h3 className="plan-profile__label">Zielzustand (Kurzfassung)</h3>
|
||||
<p>
|
||||
{initiative.target_state_summary?.trim() ? initiative.target_state_summary : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<DynamicFieldsDisplay
|
||||
initiativeId={initiative.id}
|
||||
refreshKey={fieldsRefreshKey}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Modal open={editing} title="Vorhaben bearbeiten" onClose={() => setEditing(false)} size="lg">
|
||||
<InitiativeForm
|
||||
initiativeId={initiative.id}
|
||||
initial={initiative}
|
||||
actors={actors}
|
||||
onSubmit={handleSave}
|
||||
onCancel={() => setEditing(false)}
|
||||
busy={formBusy}
|
||||
|
|
|
|||
|
|
@ -1595,9 +1595,37 @@
|
|||
background: var(--jk-surface-subtle, #f8fafc);
|
||||
}
|
||||
|
||||
.plan-profile__steering-edit {
|
||||
.initiative-form__section {
|
||||
border: none;
|
||||
margin: 0 0 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.initiative-form__section legend {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.plan-profile__section-title {
|
||||
margin: 1.25rem 0 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.plan-profile__empty-hint {
|
||||
margin: 0 0 1rem;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.field-renderer {
|
||||
border: none;
|
||||
margin: 0 0 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.field-renderer__legend {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.method-profile-select {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user