Kairo-Jinkendo/frontend/src/components/ActionForm.jsx
Lars ee1e1c0671
Some checks failed
Test Suite / pytest-backend (push) Waiting to run
Test Suite / lint-backend (push) Waiting to run
Test Suite / compose-smoke (push) Waiting to run
Test Suite / k6 /api/health Baseline (push) Blocked by required conditions
Test Suite / playwright-smoke (push) Blocked by required conditions
Deploy Development / deploy (push) Has been cancelled
fix: Sprint-AP Bearbeiten, Sprint-Verschiebung und klarere Aktionsleiste.
Behebt editingActionId-Bug in Plan/Work-Sprint; Schnellzuweisung AP zu anderem Sprint ohne Voll-Reload.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 18:04:26 +02:00

153 lines
4.9 KiB
JavaScript

import { ACTION_STATUSES, PRIORITIES } from '../constants/status.js'
import { ACTION_STATUS_LABELS, PRIORITY_LABELS } from '../constants/status.js'
import { ActorSelect } from './ActorSelect.jsx'
import { GateSelect } from './GateSelect.jsx'
import { flattenLeafProjectOptions, getLeafProjects } from '../utils/projectTree.js'
import { PLANNING_SPRINT_STATUSES } from '../utils/workCycleActions.js'
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())}`
}
export function ActionForm({
initial = {},
projects = [],
roadmapItems = [],
workCycles = [],
activeWorkCycle = null,
actors = [],
actorsLoading = false,
actorsError = null,
actorsUsedFallback = false,
onReloadActors,
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,
due_at: form.due_at.value ? new Date(form.due_at.value).toISOString() : null,
project_id: form.project_id?.value || undefined,
roadmap_item_id: form.roadmap_item_id?.value || undefined,
work_cycle_id: form.work_cycle_id?.value || undefined,
assigned_actor_ids: selected,
})
}
const defaultActors = initial.assigned_actor_ids || []
const leafProjects = getLeafProjects(projects)
const projectOptions = flattenLeafProjectOptions(projects)
const cycleOptions = workCycles.filter(
(c) => PLANNING_SPRINT_STATUSES.has(c.status) || c.id === initial.work_cycle_id,
)
const defaultCycleId =
initial.work_cycle_id || (activeWorkCycle?.id && !initial.id ? activeWorkCycle.id : '')
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>
<label>
Fällig am (optional)
<input
name="due_at"
type="datetime-local"
defaultValue={isoToLocalInput(initial.due_at)}
/>
</label>
{leafProjects.length > 0 && (
<label>
Projekt (optional, Blatt-Ebene)
<select name="project_id" defaultValue={initial.project_id || ''}>
<option value=""> keins </option>
{projectOptions.map(({ project, label }) => (
<option key={project.id} value={project.id}>
{label}
</option>
))}
</select>
</label>
)}
<GateSelect
roadmapItems={roadmapItems}
defaultValue={initial.roadmap_item_id || ''}
/>
{cycleOptions.length > 0 && (
<label>
Sprint
<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)'
: cycle.status === 'planned'
? ' (geplant)'
: cycle.status === 'reached'
? ' (abgeschlossen)'
: ''}
</option>
))}
</select>
</label>
)}
<ActorSelect
actors={actors}
defaultSelected={defaultActors}
loading={actorsLoading}
error={actorsError}
usedFallback={actorsUsedFallback}
onRetry={onReloadActors}
/>
<div className="form-actions">
<button type="submit" className="btn btn-primary" disabled={busy}>
{busy ? 'Speichern …' : submitLabel}
</button>
{onCancel && (
<button type="button" className="btn btn-secondary" onClick={onCancel} disabled={busy}>
Abbrechen
</button>
)}
</div>
</form>
)
}