2026-07-05 15:11:15 +02:00
|
|
|
import { useState } from 'react'
|
|
|
|
|
import {
|
|
|
|
|
BACKLOG_STATUSES,
|
|
|
|
|
BACKLOG_STATUS_LABELS,
|
|
|
|
|
} from '../constants/status.js'
|
|
|
|
|
import { StatusBadge } from './StatusBadge.jsx'
|
|
|
|
|
import { PriorityBadge } from './PriorityBadge.jsx'
|
|
|
|
|
import { EmptyState } from './EmptyState.jsx'
|
|
|
|
|
|
2026-07-06 08:43:11 +02:00
|
|
|
import { GateSelect, gateTitleById } from './GateSelect.jsx'
|
|
|
|
|
|
2026-07-05 15:11:15 +02:00
|
|
|
export function BacklogSection({
|
|
|
|
|
items,
|
2026-07-06 08:43:11 +02:00
|
|
|
roadmapItems = [],
|
2026-07-05 15:11:15 +02:00
|
|
|
canManage,
|
|
|
|
|
onCreate,
|
2026-07-06 08:43:11 +02:00
|
|
|
onUpdateGate,
|
2026-07-05 15:11:15 +02:00
|
|
|
onUpdateStatus,
|
|
|
|
|
onConvert,
|
|
|
|
|
onDelete,
|
|
|
|
|
busy,
|
|
|
|
|
}) {
|
|
|
|
|
const [title, setTitle] = useState('')
|
2026-07-06 08:43:11 +02:00
|
|
|
const [gateId, setGateId] = useState('')
|
2026-07-05 15:11:15 +02:00
|
|
|
const [showForm, setShowForm] = useState(false)
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(e) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (!title.trim()) return
|
2026-07-06 08:43:11 +02:00
|
|
|
await onCreate({
|
|
|
|
|
title: title.trim(),
|
|
|
|
|
roadmap_item_id: gateId || undefined,
|
|
|
|
|
})
|
2026-07-05 15:11:15 +02:00
|
|
|
setTitle('')
|
2026-07-06 08:43:11 +02:00
|
|
|
setGateId('')
|
2026-07-05 15:11:15 +02:00
|
|
|
setShowForm(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="card">
|
|
|
|
|
<div className="section-header">
|
|
|
|
|
<h2>Backlog</h2>
|
|
|
|
|
{canManage && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn btn-primary btn-block-mobile"
|
|
|
|
|
onClick={() => setShowForm((v) => !v)}
|
|
|
|
|
>
|
|
|
|
|
{showForm ? 'Abbrechen' : 'Backlog-Item'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showForm && canManage && (
|
|
|
|
|
<form className="inline-form-block" onSubmit={handleSubmit}>
|
|
|
|
|
<label>
|
|
|
|
|
Titel
|
|
|
|
|
<input
|
|
|
|
|
value={title}
|
|
|
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
|
|
|
maxLength={255}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
2026-07-06 08:43:11 +02:00
|
|
|
{roadmapItems.length > 0 && (
|
|
|
|
|
<label>
|
|
|
|
|
Zielzustand (Gate)
|
|
|
|
|
<select value={gateId} onChange={(e) => setGateId(e.target.value)}>
|
|
|
|
|
<option value="">— keins —</option>
|
|
|
|
|
{roadmapItems.map((item) => (
|
|
|
|
|
<option key={item.id} value={item.id}>
|
|
|
|
|
{item.title}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-07-05 15:11:15 +02:00
|
|
|
<button type="submit" className="btn btn-primary" disabled={busy}>
|
|
|
|
|
Anlegen
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{items.length === 0 && <EmptyState message="Backlog ist leer." />}
|
|
|
|
|
|
|
|
|
|
<ul className="item-list">
|
|
|
|
|
{items.map((item) => (
|
|
|
|
|
<li key={item.id} className="list-item card-list-item">
|
|
|
|
|
<div className="list-item-main">
|
|
|
|
|
<strong>{item.title}</strong>
|
|
|
|
|
{item.description && (
|
|
|
|
|
<p className="list-item-desc">{item.description}</p>
|
|
|
|
|
)}
|
2026-07-06 08:43:11 +02:00
|
|
|
{item.roadmap_item_id && (
|
|
|
|
|
<p className="list-item-sub muted">
|
|
|
|
|
Gate: {gateTitleById(roadmapItems, item.roadmap_item_id) || item.roadmap_item_id}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2026-07-05 15:11:15 +02:00
|
|
|
</div>
|
|
|
|
|
<div className="list-item-meta action-controls">
|
|
|
|
|
<StatusBadge kind="backlog" status={item.status} />
|
|
|
|
|
<PriorityBadge priority={item.priority} />
|
2026-07-06 08:43:11 +02:00
|
|
|
{canManage && item.status !== 'converted' && roadmapItems.length > 0 && (
|
|
|
|
|
<select
|
|
|
|
|
className="inline-select"
|
|
|
|
|
value={item.roadmap_item_id || ''}
|
|
|
|
|
onChange={(e) => onUpdateGate(item.id, e.target.value)}
|
|
|
|
|
aria-label="Gate-Zuordnung"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Gate …</option>
|
|
|
|
|
{roadmapItems.map((gate) => (
|
|
|
|
|
<option key={gate.id} value={gate.id}>
|
|
|
|
|
{gate.title}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
)}
|
2026-07-05 15:11:15 +02:00
|
|
|
{canManage && item.status !== 'converted' && (
|
|
|
|
|
<>
|
|
|
|
|
<select
|
|
|
|
|
className="inline-select"
|
|
|
|
|
value={item.status}
|
|
|
|
|
onChange={(e) => onUpdateStatus(item.id, e.target.value)}
|
|
|
|
|
aria-label="Backlog-Status"
|
|
|
|
|
>
|
|
|
|
|
{BACKLOG_STATUSES.filter((s) => s !== 'converted').map((s) => (
|
|
|
|
|
<option key={s} value={s}>
|
|
|
|
|
{BACKLOG_STATUS_LABELS[s]}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
{item.status === 'accepted' && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
onClick={() => onConvert(item.id)}
|
|
|
|
|
disabled={busy}
|
|
|
|
|
>
|
|
|
|
|
In Maßnahme umwandeln
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
onClick={() => onDelete(item.id)}
|
|
|
|
|
>
|
|
|
|
|
Löschen
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|