2026-07-10 15:41:42 +02:00
|
|
|
import {
|
|
|
|
|
MILESTONE_STATUSES,
|
|
|
|
|
MILESTONE_STATUS_LABELS,
|
|
|
|
|
ROADMAP_ITEM_TYPE_LABELS,
|
|
|
|
|
SEQUENCING_MODE_LABELS,
|
|
|
|
|
} from '../constants/status.js'
|
|
|
|
|
|
|
|
|
|
const ITEM_TYPES = ['milestone', 'review_gate', 'maturity_stage']
|
|
|
|
|
const SEQUENCING_MODES = ['sequential', 'parallel', 'optional']
|
|
|
|
|
const EDITABLE_STATUSES = MILESTONE_STATUSES.filter(
|
|
|
|
|
(s) => !['reached', 'moved', 'discarded'].includes(s),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
export function RoadmapItemForm({
|
|
|
|
|
initial = {},
|
|
|
|
|
onSubmit,
|
|
|
|
|
onCancel,
|
|
|
|
|
busy = false,
|
|
|
|
|
submitLabel = 'Anlegen',
|
|
|
|
|
mode = 'create',
|
|
|
|
|
}) {
|
|
|
|
|
async function handleSubmit(e) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
const form = e.target
|
|
|
|
|
const payload = {
|
|
|
|
|
title: form.title.value.trim(),
|
|
|
|
|
goal_description: form.goal_description.value.trim(),
|
|
|
|
|
target_date: form.target_date.value || undefined,
|
|
|
|
|
item_type: form.item_type.value,
|
|
|
|
|
sequencing_mode: form.sequencing_mode.value,
|
|
|
|
|
}
|
2026-07-11 07:53:38 +02:00
|
|
|
if (mode === 'create') {
|
|
|
|
|
const initialCriterion = form.initial_criterion_title?.value?.trim()
|
|
|
|
|
if (initialCriterion) {
|
|
|
|
|
payload.initial_criterion_title = initialCriterion
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-10 15:41:42 +02:00
|
|
|
if (mode === 'edit') {
|
|
|
|
|
payload.status = form.status.value
|
|
|
|
|
}
|
|
|
|
|
await onSubmit(payload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusLocked = ['reached', 'moved', 'discarded'].includes(initial.status)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form className="form workspace-form roadmap-item-form" onSubmit={handleSubmit}>
|
|
|
|
|
{mode === 'create' && (
|
|
|
|
|
<label>
|
|
|
|
|
Typ
|
|
|
|
|
<select name="item_type" defaultValue={initial.item_type || 'milestone'}>
|
|
|
|
|
{ITEM_TYPES.map((t) => (
|
|
|
|
|
<option key={t} value={t}>
|
|
|
|
|
{ROADMAP_ITEM_TYPE_LABELS[t] || t}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
<label>
|
|
|
|
|
Titel
|
|
|
|
|
<input
|
|
|
|
|
name="title"
|
|
|
|
|
defaultValue={initial.title || ''}
|
|
|
|
|
maxLength={255}
|
|
|
|
|
required
|
|
|
|
|
placeholder="z. B. MVP nutzbar im Alltag"
|
|
|
|
|
disabled={statusLocked}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
2026-07-11 07:53:38 +02:00
|
|
|
Zielkontext (optional)
|
2026-07-10 15:41:42 +02:00
|
|
|
<textarea
|
|
|
|
|
name="goal_description"
|
|
|
|
|
rows={3}
|
|
|
|
|
defaultValue={initial.goal_description || ''}
|
2026-07-11 07:53:38 +02:00
|
|
|
placeholder="Kurzer Kontext — prüfbare Akzeptanz legst du als Kriterien an."
|
2026-07-10 15:41:42 +02:00
|
|
|
disabled={statusLocked}
|
|
|
|
|
/>
|
2026-07-11 07:53:38 +02:00
|
|
|
<span className="muted form-hint">
|
|
|
|
|
Die Checkliste auf der Gate-Detailseite ist führend für Verify.
|
|
|
|
|
</span>
|
2026-07-10 15:41:42 +02:00
|
|
|
</label>
|
2026-07-11 07:53:38 +02:00
|
|
|
{mode === 'create' && (
|
|
|
|
|
<label>
|
|
|
|
|
Erstes Kriterium (optional)
|
|
|
|
|
<input
|
|
|
|
|
name="initial_criterion_title"
|
|
|
|
|
maxLength={500}
|
|
|
|
|
placeholder="z. B. Demo mit Stakeholdern durchgeführt"
|
|
|
|
|
/>
|
|
|
|
|
<span className="muted form-hint">
|
|
|
|
|
Ohne Angabe wird „Gate allgemein“ als Standard-Kriterium angelegt.
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-07-10 15:41:42 +02:00
|
|
|
<div className="form-row form-row--2">
|
|
|
|
|
<label>
|
|
|
|
|
Abfolge
|
|
|
|
|
<select
|
|
|
|
|
name="sequencing_mode"
|
|
|
|
|
defaultValue={initial.sequencing_mode || 'sequential'}
|
|
|
|
|
disabled={statusLocked}
|
|
|
|
|
>
|
|
|
|
|
{SEQUENCING_MODES.map((m) => (
|
|
|
|
|
<option key={m} value={m}>
|
|
|
|
|
{SEQUENCING_MODE_LABELS[m] || m}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
Zieltermin
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
name="target_date"
|
|
|
|
|
defaultValue={initial.target_date || ''}
|
|
|
|
|
disabled={statusLocked}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
{mode === 'edit' && (
|
|
|
|
|
<label>
|
|
|
|
|
Status
|
|
|
|
|
<select name="status" defaultValue={initial.status || 'planned'} disabled={statusLocked}>
|
|
|
|
|
{EDITABLE_STATUSES.map((s) => (
|
|
|
|
|
<option key={s} value={s}>
|
|
|
|
|
{MILESTONE_STATUS_LABELS[s]}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
<div className="form-actions">
|
|
|
|
|
<button type="submit" className="btn btn-primary" disabled={busy || statusLocked}>
|
|
|
|
|
{busy ? 'Speichern …' : submitLabel}
|
|
|
|
|
</button>
|
|
|
|
|
{onCancel && (
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={onCancel} disabled={busy}>
|
|
|
|
|
Abbrechen
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
)
|
|
|
|
|
}
|