From 3cc6ab33431af2175163f757518da87db012ebed Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 19:59:37 +0000 Subject: [PATCH] feat(roadmaps): Steckbrett-Board mit SVG-Kanten, Detail-Panel und Abschluss-Automatik Co-Authored-By: Claude Fable 5 --- src/components/roadmap/NodeDetailPanel.jsx | 166 +++++++++++++++++ src/components/roadmap/RoadmapBoard.jsx | 168 +++++++++++++++++ src/components/roadmap/RoadmapNodeCard.jsx | 101 ++++++++++ src/hooks/useRoadmaps.js | 16 +- src/pages/RoadmapDetailPage.jsx | 205 +++++++++++++++++++++ src/pages/RoadmapsPage.jsx | 2 +- 6 files changed, 656 insertions(+), 2 deletions(-) create mode 100644 src/components/roadmap/NodeDetailPanel.jsx create mode 100644 src/components/roadmap/RoadmapBoard.jsx create mode 100644 src/components/roadmap/RoadmapNodeCard.jsx create mode 100644 src/pages/RoadmapDetailPage.jsx diff --git a/src/components/roadmap/NodeDetailPanel.jsx b/src/components/roadmap/NodeDetailPanel.jsx new file mode 100644 index 0000000..dac9a00 --- /dev/null +++ b/src/components/roadmap/NodeDetailPanel.jsx @@ -0,0 +1,166 @@ +import { useEffect, useState } from 'react' +import { IconTrash, IconX } from '@tabler/icons-react' +import { Card, Button, Field } from '../ui' +import { useEmployees } from '../../hooks/useEmployees' +import { NODE_KIND, NODE_STATUS, NODE_STATUS_LABELS } from '../../lib/roadmaps' + +const STATUS_ORDER = [NODE_STATUS.OPEN, NODE_STATUS.IN_PROGRESS, NODE_STATUS.DONE] + +/** + * Seitliches Detail-Panel fuer den ausgewaehlten Knoten: + * Titel, Status, Zustaendiger, Deadline, Notizen, eingehende Verbindungen. + */ +export default function NodeDetailPanel({ + node, + nodes, + readOnly, + onClose, + onSave, + onSetStatus, + onDelete, + onDisconnect, +}) { + const { employees } = useEmployees() + const [form, setForm] = useState({ title: '', assignedTo: '', deadline: '', notes: '' }) + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + setForm({ + title: node.title || '', + assignedTo: node.assignedTo || '', + deadline: node.deadline || '', + notes: node.notes || '', + }) + setError('') + }, [node.$id]) // eslint-disable-line react-hooks/exhaustive-deps + + const isStart = node.kind === NODE_KIND.START + const incoming = (node.sourceIds || []) + .map((id) => nodes.find((n) => n.$id === id)) + .filter(Boolean) + + const handleSave = async () => { + if (!form.title.trim()) { + setError('Titel darf nicht leer sein.') + return + } + setSaving(true) + setError('') + const employee = employees.find((e) => e.userId === form.assignedTo) + const result = await onSave(node, { + title: form.title.trim(), + assignedTo: form.assignedTo, + assignedName: employee?.displayName || '', + deadline: form.deadline, + notes: form.notes, + }) + setSaving(false) + if (!result.success) setError(result.error) + } + + return ( + } + > +
+ + setForm((f) => ({ ...f, title: e.target.value }))} + /> + + + {!isStart && ( + +
+ {STATUS_ORDER.map((status) => ( + + ))} +
+
+ )} + + {!isStart && ( + + + + )} + + {!isStart && ( + + setForm((f) => ({ ...f, deadline: e.target.value }))} + /> + + )} + + +