import { useEffect, useMemo, useState } from 'react' import { StatusBadge } from './StatusBadge.jsx' import { PriorityBadge } from './PriorityBadge.jsx' import { EmptyState } from './EmptyState.jsx' import { Modal } from './Modal.jsx' import { BacklogItemForm } from './BacklogItemForm.jsx' import { ReorderControls } from './ReorderControls.jsx' import { gateTitleById } from './GateSelect.jsx' import { computeDropPatches, computeMovePatches, sortByOrder } from '../utils/reorder.js' import { buildBacklogDisplayRows, listEpicBacklogItems } from '../utils/backlogTree.js' import { useMinWidth } from '../hooks/useMinWidth.js' import { BACKLOG_ITEM_KIND_LABELS } from '../constants/status.js' import { actionTitleById } from '../utils/actionReferences.js' const PLANNING_STATUSES = new Set(['planned', 'active', 'at_risk']) /** Backend erlaubt Convert für diese Status — UI war nur bei „Freigegeben“ sichtbar. */ const COMMITTABLE_STATUSES = new Set(['accepted', 'triaged', 'new']) const SPRINT_TARGET_STORAGE_PREFIX = 'kairo-sprint-target:' export function BacklogSection({ items, roadmapItems = [], workCycles = [], activeWorkCycle = null, featureActions = [], initiativeId = '', sprintPlanningEnabled = false, canManage, onCreate, onUpdate, onReorder, onConvert, onBulkConvert, onDelete, busy, sectionTitle = 'Product Backlog', sectionLead = 'Eingang vor dem Commit — triagieren, dann in den Sprint planen.', }) { const [modalMode, setModalMode] = useState(null) const [dragItemId, setDragItemId] = useState('') const [dropTargetId, setDropTargetId] = useState('') const [selectedIds, setSelectedIds] = useState(() => new Set()) const [sprintTargetId, setSprintTargetId] = useState('') const isDesktop = useMinWidth(1024) const canReorder = canManage && typeof onReorder === 'function' const planableSprints = useMemo( () => workCycles.filter((cycle) => PLANNING_STATUSES.has(cycle.status)), [workCycles], ) const showSprintPlanning = sprintPlanningEnabled && planableSprints.length > 0 useEffect(() => { if (!showSprintPlanning || !initiativeId) { return } const stored = sessionStorage.getItem(SPRINT_TARGET_STORAGE_PREFIX + initiativeId) if (stored && planableSprints.some((cycle) => cycle.id === stored)) { setSprintTargetId(stored) return } if (sprintTargetId && planableSprints.some((cycle) => cycle.id === sprintTargetId)) { return } const preferred = activeWorkCycle?.id || planableSprints[0]?.id || '' setSprintTargetId(preferred) }, [showSprintPlanning, initiativeId, planableSprints, activeWorkCycle?.id]) useEffect(() => { if (!initiativeId || !sprintTargetId) return sessionStorage.setItem(SPRINT_TARGET_STORAGE_PREFIX + initiativeId, sprintTargetId) }, [initiativeId, sprintTargetId]) const selectedSprint = planableSprints.find((cycle) => cycle.id === sprintTargetId) || null const convertLabel = selectedSprint ? `In Sprint „${selectedSprint.title}" planen` : 'In Arbeitspaket umwandeln' const sortedItems = useMemo( () => sortByOrder(items.filter((item) => item.status !== 'converted')), [items], ) const epicItems = useMemo(() => listEpicBacklogItems(items), [items]) const displayRows = useMemo(() => buildBacklogDisplayRows(items), [items]) const epicTitleById = useMemo(() => { const map = new Map() for (const epic of epicItems) { map.set(epic.id, epic.title) } return map }, [epicItems]) const epicHierarchyEnabled = sprintPlanningEnabled function closeModal() { setModalMode(null) } async function handleCreateSubmit(payload) { await onCreate({ title: payload.title, description: payload.description, priority: payload.priority, roadmap_item_id: payload.roadmap_item_id, item_kind: payload.item_kind, parent_action_id: payload.parent_action_id, parent_backlog_id: payload.parent_backlog_id, status: payload.status || 'new', }) closeModal() } async function handleEditSubmit(payload) { if (!modalMode?.item) return const ok = await onUpdate(modalMode.item.id, { title: payload.title, description: payload.description, status: payload.status, priority: payload.priority, roadmap_item_id: payload.roadmap_item_id, clear_roadmap_item: payload.clear_roadmap_item, item_kind: payload.item_kind, parent_action_id: payload.parent_action_id, clear_parent_action: payload.clear_parent_action, parent_backlog_id: payload.parent_backlog_id, clear_parent_backlog: payload.clear_parent_backlog, }) if (ok !== false) closeModal() } async function applyPatches(patches) { if (!patches.length) return await onReorder(patches) } async function handleMove(itemId, direction) { const patches = computeMovePatches(sortedItems, itemId, direction) await applyPatches(patches) } function handleDragStart(event, itemId) { if (!canReorder || !isDesktop) return setDragItemId(itemId) event.dataTransfer.effectAllowed = 'move' event.dataTransfer.setData('text/plain', itemId) } function handleDragEnd() { setDragItemId('') setDropTargetId('') } function handleDragOver(event, itemId) { if (!canReorder || !isDesktop || !dragItemId) return event.preventDefault() event.dataTransfer.dropEffect = 'move' setDropTargetId(itemId) } async function handleDrop(event, targetId) { event.preventDefault() if (!canReorder || !isDesktop || !dragItemId) { handleDragEnd() return } const patches = computeDropPatches(sortedItems, dragItemId, targetId) handleDragEnd() await applyPatches(patches) } const modalTitle = modalMode?.kind === 'create' ? 'Backlog-Item anlegen' : 'Backlog-Item bearbeiten' const committableItems = sortedItems.filter( (item) => COMMITTABLE_STATUSES.has(item.status) && item.item_kind !== 'epic', ) const selectedCommittable = committableItems.filter((item) => selectedIds.has(item.id)) function toggleSelected(itemId) { setSelectedIds((prev) => { const next = new Set(prev) if (next.has(itemId)) next.delete(itemId) else next.add(itemId) return next }) } function convertOptions() { return sprintTargetId ? { work_cycle_id: sprintTargetId } : {} } async function handleSingleConvert(itemId) { await onConvert(itemId, convertOptions()) setSelectedIds((prev) => { const next = new Set(prev) next.delete(itemId) return next }) } async function handleBulkPlan() { if (!selectedCommittable.length) return if (typeof onBulkConvert === 'function') { await onBulkConvert( selectedCommittable.map((item) => item.id), convertOptions(), ) } else { for (const item of selectedCommittable) { await onConvert(item.id, convertOptions()) } } setSelectedIds(new Set()) } return (

{sectionTitle}

{sectionLead}

{canManage && ( )}
{sprintPlanningEnabled && canManage && !showSprintPlanning && (

Sprint-Planung: Lege zuerst einen Sprint an.

Plan → Sprint → „Sprint anlegen“ (optional „Sofort aktivieren“). Danach erscheint hier die Sprint-Auswahl zum Committen aus dem Eingang.

)} {showSprintPlanning && canManage && (
{selectedCommittable.length > 0 && ( )}

{committableItems.length === 0 ? 'Lege Backlog-Items an — dann per Checkbox markieren und in den Sprint committen.' : 'Checkbox am Item → „In Sprint planen“. Committen erzeugt ein Arbeitspaket mit Sprint-Zuweisung.'}

)} {items.length === 0 && } {modalMode?.kind === 'create' && ( )} {modalMode?.kind === 'edit' && modalMode.item && ( epic.id !== modalMode.item.id)} epicHierarchyEnabled={epicHierarchyEnabled} onSubmit={handleEditSubmit} onCancel={closeModal} busy={busy} /> )}
) }