2026-07-05 06:41:23 +02:00
|
|
|
import { ACTION_STATUSES, PRIORITIES } from '../constants/status.js'
|
|
|
|
|
import { ACTION_STATUS_LABELS, PRIORITY_LABELS } from '../constants/status.js'
|
|
|
|
|
import { ActorSelect } from './ActorSelect.jsx'
|
2026-07-06 08:43:11 +02:00
|
|
|
import { GateSelect } from './GateSelect.jsx'
|
2026-07-06 08:38:58 +02:00
|
|
|
import { flattenLeafProjectOptions, getLeafProjects } from '../utils/projectTree.js'
|
2026-07-05 06:41:23 +02:00
|
|
|
|
2026-07-05 15:27:03 +02:00
|
|
|
function isoToLocalInput(iso) {
|
|
|
|
|
if (!iso) return ''
|
|
|
|
|
const d = new Date(iso)
|
|
|
|
|
const pad = (n) => String(n).padStart(2, '0')
|
|
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 06:41:23 +02:00
|
|
|
export function ActionForm({
|
|
|
|
|
initial = {},
|
2026-07-06 08:19:05 +02:00
|
|
|
projects = [],
|
2026-07-06 08:43:11 +02:00
|
|
|
roadmapItems = [],
|
2026-07-12 16:27:34 +02:00
|
|
|
workCycles = [],
|
|
|
|
|
activeWorkCycle = null,
|
2026-07-05 06:41:23 +02:00
|
|
|
actors = [],
|
2026-07-05 07:25:01 +02:00
|
|
|
actorsLoading = false,
|
|
|
|
|
actorsError = null,
|
|
|
|
|
actorsUsedFallback = false,
|
|
|
|
|
onReloadActors,
|
2026-07-05 06:41:23 +02:00
|
|
|
onSubmit,
|
|
|
|
|
onCancel,
|
|
|
|
|
busy = false,
|
|
|
|
|
submitLabel = 'Speichern',
|
|
|
|
|
}) {
|
|
|
|
|
async function handleSubmit(e) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
const form = e.target
|
|
|
|
|
const selected = actors.filter((a) => form[`actor_${a.id}`]?.checked).map((a) => a.id)
|
|
|
|
|
await onSubmit({
|
|
|
|
|
title: form.title.value.trim(),
|
|
|
|
|
description: form.description.value,
|
|
|
|
|
status: form.status.value,
|
|
|
|
|
priority: form.priority.value,
|
2026-07-05 15:27:03 +02:00
|
|
|
due_at: form.due_at.value ? new Date(form.due_at.value).toISOString() : null,
|
2026-07-06 08:19:05 +02:00
|
|
|
project_id: form.project_id?.value || undefined,
|
2026-07-06 08:43:11 +02:00
|
|
|
roadmap_item_id: form.roadmap_item_id?.value || undefined,
|
2026-07-12 16:27:34 +02:00
|
|
|
work_cycle_id: form.work_cycle_id?.value || undefined,
|
2026-07-05 06:41:23 +02:00
|
|
|
assigned_actor_ids: selected,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultActors = initial.assigned_actor_ids || []
|
2026-07-06 08:38:58 +02:00
|
|
|
const leafProjects = getLeafProjects(projects)
|
|
|
|
|
const projectOptions = flattenLeafProjectOptions(projects)
|
2026-07-12 16:27:34 +02:00
|
|
|
const cycleOptions = workCycles.filter((c) => c.status !== 'completed')
|
|
|
|
|
const defaultCycleId =
|
|
|
|
|
initial.work_cycle_id || (activeWorkCycle?.id && !initial.id ? activeWorkCycle.id : '')
|
2026-07-05 06:41:23 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form className="form workspace-form" onSubmit={handleSubmit}>
|
|
|
|
|
<label>
|
|
|
|
|
Titel
|
|
|
|
|
<input name="title" defaultValue={initial.title || ''} required maxLength={255} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
Beschreibung
|
|
|
|
|
<textarea name="description" rows={2} defaultValue={initial.description || ''} />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
Status
|
|
|
|
|
<select name="status" defaultValue={initial.status || 'open'}>
|
|
|
|
|
{ACTION_STATUSES.map((s) => (
|
|
|
|
|
<option key={s} value={s}>
|
|
|
|
|
{ACTION_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>
|
2026-07-05 15:27:03 +02:00
|
|
|
<label>
|
|
|
|
|
Fällig am (optional)
|
|
|
|
|
<input
|
|
|
|
|
name="due_at"
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
defaultValue={isoToLocalInput(initial.due_at)}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
2026-07-06 08:38:58 +02:00
|
|
|
{leafProjects.length > 0 && (
|
2026-07-06 08:19:05 +02:00
|
|
|
<label>
|
2026-07-06 08:38:58 +02:00
|
|
|
Projekt (optional, Blatt-Ebene)
|
2026-07-06 08:19:05 +02:00
|
|
|
<select name="project_id" defaultValue={initial.project_id || ''}>
|
|
|
|
|
<option value="">— keins —</option>
|
2026-07-06 08:38:58 +02:00
|
|
|
{projectOptions.map(({ project, label }) => (
|
|
|
|
|
<option key={project.id} value={project.id}>
|
|
|
|
|
{label}
|
2026-07-06 08:19:05 +02:00
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-07-06 08:43:11 +02:00
|
|
|
<GateSelect
|
|
|
|
|
roadmapItems={roadmapItems}
|
|
|
|
|
defaultValue={initial.roadmap_item_id || ''}
|
|
|
|
|
/>
|
2026-07-12 16:27:34 +02:00
|
|
|
{cycleOptions.length > 0 && (
|
|
|
|
|
<label>
|
|
|
|
|
Sprint / Zeitbox (optional)
|
|
|
|
|
<select name="work_cycle_id" defaultValue={defaultCycleId}>
|
|
|
|
|
<option value="">— kein Sprint —</option>
|
|
|
|
|
{cycleOptions.map((cycle) => (
|
|
|
|
|
<option key={cycle.id} value={cycle.id}>
|
|
|
|
|
{cycle.title}
|
|
|
|
|
{cycle.status === 'active' ? ' (aktiv)' : ''}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-07-05 07:25:01 +02:00
|
|
|
<ActorSelect
|
|
|
|
|
actors={actors}
|
|
|
|
|
defaultSelected={defaultActors}
|
|
|
|
|
loading={actorsLoading}
|
|
|
|
|
error={actorsError}
|
|
|
|
|
usedFallback={actorsUsedFallback}
|
|
|
|
|
onRetry={onReloadActors}
|
|
|
|
|
/>
|
2026-07-05 06:41:23 +02:00
|
|
|
<div className="form-actions">
|
2026-07-05 06:58:48 +02:00
|
|
|
<button type="submit" className="btn btn-primary" disabled={busy}>
|
2026-07-05 06:41:23 +02:00
|
|
|
{busy ? 'Speichern …' : submitLabel}
|
|
|
|
|
</button>
|
|
|
|
|
{onCancel && (
|
2026-07-05 06:58:48 +02:00
|
|
|
<button type="button" className="btn btn-secondary" onClick={onCancel} disabled={busy}>
|
2026-07-05 06:41:23 +02:00
|
|
|
Abbrechen
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
)
|
|
|
|
|
}
|