import React, { useState, useEffect, useRef } from "react"; import { PROTOKOLL_TYPES, PROTOKOLL_ENTRY_TYPES } from "../constants.js"; import { generateId, formatCHF, formatDate, formatHours, buildReminderLetter, getKW, getWeekNumber, formatKW, nextProtoNumber, nextProtoSeq, applyProtoNumberFormat } from "../utils.js"; import { Header, Modal, FormField, StatusBadge, useConfirm, RichEditor , DateInput } from "../components/UI.jsx"; export function MahnModal({ inv, data, update, setPrintContent, onClose, mahnMode, setMahnMode, mahnSentDate, setMahnSentDate }) { const reminders = inv.reminders || []; const nextNr = reminders.length + 1; const lastReminder = reminders.at(-1); const nextLabel = nextNr === 1 ? "Zahlungserinnerung" : `${nextNr}. Mahnung`; const nextColor = nextNr >= 3 ? "#8a1a1a" : nextNr === 2 ? "#b5621e" : "#7a6a00"; const confirm = () => { if (mahnMode.startsWith("reprint-")) { const idx = parseInt(mahnMode.split("-")[1]); const r = reminders[idx]; const { client, subject, body } = buildReminderLetter(inv, r.nr, r.sentDate || r.date, (data.persons||[]).filter(p=>p.isAuftraggeber), data.settings); setPrintContent({ type: "letter", client, subject, body, settings: data.settings }); } else { const { client, subject, body } = buildReminderLetter(inv, nextNr, mahnSentDate, (data.persons||[]).filter(p=>p.isAuftraggeber), data.settings); setPrintContent({ type: "letter", client, subject, body, settings: data.settings }); const newReminder = { nr: nextNr, date: new Date().toISOString().slice(0, 10), sentDate: mahnSentDate, daysPast: Math.floor((new Date() - new Date(inv.dueDate)) / 86400000) }; update("invoices", data.invoices.map(i => i.id === inv.id ? { ...i, status: "überfällig", reminders: [...reminders, newReminder] } : i )); } onClose(); }; return (
e.target === e.currentTarget && onClose()}>

Mahnung

Rechnung {inv.number} · {formatCHF(inv.total)}
{reminders.length > 0 && (
BISHERIGE MAHNUNGEN
{reminders.map((r, i) => (
{i === 0 ? "Zahlungserinnerung" : `${r.nr}. Mahnung`} gesendet {formatDate(r.sentDate || r.date)}
))}
)}
{reminders.map((r, i) => { const rLabel = i === 0 ? "Zahlungserinnerung" : `${r.nr}. Mahnung`; const rMode = `reprint-${i}`; return ( ); })}
); } export default function Protokolle({ data, update, saveAll, setPrintContent }) { const today = new Date().toISOString().slice(0, 10); const protokolle = data.protocols || []; const [detailId, setDetailId] = useState(() => { const id = window.__openProtokoll || null; window.__openProtokoll = null; return id; }); const [filter, setFilter] = useState({ search: "", type: "", projectId: "" }); const [sort, setSort] = useState({ col: "date", dir: -1 }); const detail = detailId ? protokolle.find(p => p.id === detailId) : null; // hooks must be called before any early return const { askConfirm, ConfirmModalEl } = useConfirm(); const deleteProtokoll = async (id) => { if (await askConfirm("Protokoll löschen?")) { saveAll({ ...data, protocols: protokolle.filter(x => x.id !== id) }); } }; // ── Detail-Ansicht ──────────────────────────────────────────── if (detail) { return setDetailId(null)} onSave={p => { saveAll({ ...data, protocols: protokolle.map(x => x.id === p.id ? p : x) }); }} onDelete={id => { saveAll({ ...data, protocols: protokolle.filter(x => x.id !== id) }); setDetailId(null); }} setPrintContent={setPrintContent} saveAll={saveAll} />; } // ── Listenansicht ───────────────────────────────────────────── const newProtokoll = () => { const defaultType = PROTOKOLL_TYPES[0]; const abbr = data.settings.protokollTypeAbbreviations || {}; const typKuerzel = abbr[defaultType] || "SO"; const seq = nextProtoSeq(protokolle); const nummer = applyProtoNumberFormat(data.settings.protokollNumberFormat || "YYYY-TT-NN", { date: today, projectNumber: "", seq, typKuerzel, }); const p = { id: generateId(), title: "", type: defaultType, date: today, time: "10:00", endTime: "", location: "", projectId: "", projectManual: "", nummer, participants: [], traktanden: [{ id: generateId(), nr: "1", title: "", items: [] }], nextDate: "", verteiler: "", createdAt: new Date().toISOString(), }; saveAll({ ...data, protocols: [...protokolle, p] }); setDetailId(p.id); }; const filtered = protokolle.filter(p => { if (filter.type && p.type !== filter.type) return false; if (filter.projectId && p.projectId !== filter.projectId) return false; if (filter.search) { const q = filter.search.toLowerCase(); const proj = data.projects.find(x => x.id === p.projectId); if (![p.title, p.nummer, p.type, proj?.name, p.location].filter(Boolean).join(" ").toLowerCase().includes(q)) return false; } return true; }); const toggleSort = col => setSort(s => ({ col, dir: s.col === col ? -s.dir : -1 })); const SortTh = ({ col, children, style }) => ( toggleSort(col)} style={{ cursor: "pointer", userSelect: "none", ...style }}> {children} {sort.col === col ? (sort.dir === 1 ? "▲" : "▼") : "⇅"} ); const sorted = [...filtered].sort((a, b) => { const va = sort.col === "date" ? (a.date || "") : sort.col === "type" ? (a.type || "") : sort.col === "nummer" ? (a.nummer || "") : (a.title || ""); const vb = sort.col === "date" ? (b.date || "") : sort.col === "type" ? (b.type || "") : sort.col === "nummer" ? (b.nummer || "") : (b.title || ""); return va.localeCompare(vb) * sort.dir; }); // Offene Aufgaben über alle Protokolle const alleTasks = protokolle.flatMap(p => (p.traktanden || []).flatMap(t => (t.items || []).filter(it => it.type === "aufgabe" && it.status !== "erledigt") .map(it => ({ ...it, protokollId: p.id, protokollNr: p.nummer, protokollTitle: p.title, protokollDate: p.date })) ) ); return (
{ConfirmModalEl}
+ Neues Protokoll } /> {/* Offene Aufgaben-Banner */} {alleTasks.length > 0 && (
→ OFFENE AUFGABEN ({alleTasks.length})
{alleTasks.slice(0, 6).map(t => { const due = t.dueDateType === "kw" ? `KW ${t.dueKW}/${t.dueYear || new Date().getFullYear()}` : t.dueDate ? formatDate(t.dueDate) : "—"; const isLate = t.dueDate && t.dueDateType === "datum" && t.dueDate < today; return (
setDetailId(t.protokollId)} style={{ fontSize: 12, padding: "5px 10px", background: isLate ? "#fdf2f2" : "var(--surface2)", border: `1px solid ${isLate ? "#e8b0b0" : "var(--border)"}`, borderRadius: 4, cursor: "pointer" }}> {t.protokollNr} {t.text} {t.responsible && → {t.responsible}} {due}
); })} {alleTasks.length > 6 &&
+{alleTasks.length - 6} weitere
}
)}
setFilter({ ...filter, search: e.target.value })} style={{ minWidth: 200 }} /> {(filter.search || filter.type || filter.projectId) && ( )}
{filtered.length} Protokolle
{sorted.length === 0 ? (
Nr.DatumTypTitelProjektTN📌
{protokolle.length === 0 ? "Noch keine Protokolle" : "Keine Treffer"}
) : (
Nr.DatumTypTitel {sorted.map(p => { const proj = data.projects.find(x => x.id === p.projectId); const anwesend = (p.participants || []).filter(x => x.status === "anwesend").length; const total = (p.participants || []).length; const offeneTasks = (p.traktanden || []).flatMap(t => (t.items || []).filter(it => it.type === "aufgabe" && it.status !== "erledigt")).length; return ( setDetailId(p.id)} style={{ cursor: "pointer" }}> ); })}
Projekt TN 📌
{p.nummer} {formatDate(p.date)} {p.type} {p.title || Kein Titel} {proj?.name || p.projectManual || "—"} {total > 0 ? `${anwesend}/${total}` : "—"} {offeneTasks > 0 && {offeneTasks}}
)}
); } export function ItemEditor({ tId, item, today, onUpdate, onRemove }) { const typeConfig = { info: { icon: "ℹ", color: "#1a4e8a", label: "Information" }, beschluss:{ icon: "✓", color: "#2d6a4f", label: "Beschluss" }, aufgabe: { icon: "→", color: "#b5621e", label: "Aufgabe" }, }; const tc = typeConfig[item.type] || typeConfig.info; return (
{tc.icon}
onUpdate({ text: html })} minHeight={60} compact /> {item.type === "beschluss" && (
onUpdate({ date: e.target.value })} style={{ height: 28, fontSize: 11, width: 140 }} />
)} {item.type === "aufgabe" && (
onUpdate({ responsible: e.target.value })} placeholder="Verantwortliche/r" style={{ height: 28, fontSize: 11, flex: "1 1 140px", maxWidth: 200 }} /> {(item.dueDateType || "kw") === "kw" ? ( <> onUpdate({ dueKW: e.target.value })} placeholder="KW" style={{ height: 28, fontSize: 11, width: 60 }} /> onUpdate({ dueYear: +e.target.value })} style={{ height: 28, fontSize: 11, width: 72 }} /> ) : ( onUpdate({ dueDate: e.target.value })} style={{ height: 28, fontSize: 11, width: 140 }} /> )}
)}
); } export function ProtokollDetail({ protokoll, data, onBack, onSave, onDelete, setPrintContent, saveAll }) { const [p, setP] = useState(() => JSON.parse(JSON.stringify(protokoll))); const isDirty = JSON.stringify(p) !== JSON.stringify(protokoll); const today = new Date().toISOString().slice(0, 10); const [showFolge, setShowFolge] = useState(false); const { askConfirm, ConfirmModalEl } = useConfirm(); const [folgeSelection, setFolgeSelection] = useState(null); // built on open const save = () => onSave(p); const setField = (k, v) => setP(prev => { const updated = { ...prev, [k]: v }; if (k === "projectId" || k === "date" || k === "type") { const abbr = data.settings.protokollTypeAbbreviations || {}; const proj = data.projects.find(x => x.id === updated.projectId); const typKuerzel = abbr[updated.type] || "SO"; const groups = (prev.nummer || "").match(/\d+/g) || []; const seq = (() => { for (let i = groups.length - 1; i >= 0; i--) { const n = parseInt(groups[i]); if (!(groups[i].length === 4 && n >= 2000 && n <= 2099)) return n; } return 1; })(); updated.nummer = applyProtoNumberFormat(data.settings.protokollNumberFormat || "YYYY-TT-NN", { date: updated.date, projectNumber: proj?.number || "", seq, typKuerzel, }); } return updated; }); // ── Teilnehmer ──────────────────────────────────────────────── const proj = data.projects.find(x => x.id === p.projectId); const projectContactPersons = proj ? (proj.projectContacts || []).flatMap(pc => { const firm = (data.persons || []).find(c => c.id === pc.contactId); if (!firm) return []; const firmPersons = firm.contacts || []; if (pc.personIds && pc.personIds.length > 0) { return firmPersons.filter(fp => pc.personIds.includes(fp.id)).map(fp => ({ id: `pc-${fp.id}`, name: fp.name, role: [fp.position, firm.name].filter(Boolean).join(" · "), source: "extern", })); } // No specific persons selected — show firm entry (if it has no contacts) or all persons if (firmPersons.length === 0) { return [{ id: `pcf-${firm.id}`, name: firm.name, role: firm.type || "Beteiligter", source: "extern" }]; } return firmPersons.map(fp => ({ id: `pc-${fp.id}`, name: fp.name, role: [fp.position, firm.name].filter(Boolean).join(" · "), source: "extern", })); }) : []; const projectMembers = proj?.internalMembers?.length ? (data.employees || []).filter(e => proj.internalMembers.includes(e.id)) : (data.employees || []); const allPersons = [ ...projectMembers.map(e => ({ id: `emp-${e.id}`, name: e.name, role: e.role || "Mitarbeiter", source: "intern" })), ...projectContactPersons, ...(data.persons || []).filter(p => p.isAuftraggeber).flatMap(c => { if (c.contacts && c.contacts.length > 0) { return c.contacts.map(ct => ({ id: `cnt-${ct.id}`, name: ct.name, role: [ct.position, c.name].filter(Boolean).join(" · "), source: "extern", })); } return [{ id: `cli-${c.id}`, name: c.name, role: "Auftraggeber", source: "extern" }]; }), ]; const addParticipant = (personId) => { if (!personId || p.participants.some(x => x.id === personId)) return; const person = allPersons.find(x => x.id === personId); if (!person) return; setP(prev => ({ ...prev, participants: [...prev.participants, { ...person, status: "anwesend" }] })); }; const addManualParticipant = () => { const name = prompt("Name der Person:"); if (!name?.trim()) return; const role = prompt("Funktion / Firma (optional):") || ""; setP(prev => ({ ...prev, participants: [...prev.participants, { id: generateId(), name: name.trim(), role, source: "manuell", status: "anwesend" }] })); }; const setParticipantStatus = (id, status) => setP(prev => ({ ...prev, participants: prev.participants.map(x => x.id === id ? { ...x, status } : x) })); const removeParticipant = (id) => setP(prev => ({ ...prev, participants: prev.participants.filter(x => x.id !== id) })); // ── Traktanden ──────────────────────────────────────────────── const addTraktandum = () => { const maxNr = Math.max(0, ...(p.traktanden || []).map(t => parseInt(t.nr) || 0)); setP(prev => ({ ...prev, traktanden: [...(prev.traktanden || []), { id: generateId(), nr: String(maxNr + 1), title: "", items: [] }] })); }; const setTraktandum = (id, changes) => setP(prev => ({ ...prev, traktanden: (prev.traktanden || []).map(t => t.id === id ? { ...t, ...changes } : t) })); const removeTraktandum = (id) => setP(prev => ({ ...prev, traktanden: (prev.traktanden || []).filter(t => t.id !== id) })); const addItem = (tId, type) => { const item = type === "aufgabe" ? { id: generateId(), type, text: "", responsible: "", dueDateType: "kw", dueKW: "", dueYear: new Date().getFullYear(), dueDate: "", status: "offen" } : type === "beschluss" ? { id: generateId(), type, text: "", date: today } : { id: generateId(), type: "info", text: "" }; setP(prev => ({ ...prev, traktanden: (prev.traktanden || []).map(t => t.id === tId ? { ...t, items: [...(t.items || []), item] } : t) })); }; const setItem = (tId, iId, changes) => setP(prev => ({ ...prev, traktanden: (prev.traktanden || []).map(t => t.id === tId ? { ...t, items: (t.items || []).map(it => it.id === iId ? { ...it, ...changes } : it) } : t) })); const removeItem = (tId, iId) => setP(prev => ({ ...prev, traktanden: (prev.traktanden || []).map(t => t.id === tId ? { ...t, items: (t.items || []).filter(it => it.id !== iId) } : t) })); const statusConfig = { anwesend: { label: "Anwesend", color: "#2d6a4f", bg: "#e8f5ee" }, entschuldigt: { label: "Entschuldigt", color: "#b5621e", bg: "#fdf0e8" }, abwesend: { label: "Abwesend", color: "#8a1a1a", bg: "#fdf2f2" }, eingeladen: { label: "Eingeladen", color: "#1a4e8a", bg: "#e8f0fa" }, }; const unaddedPersons = allPersons.filter(x => !p.participants.some(pt => pt.id === x.id)); // ── Drag & Drop ─────────────────────────────────────────────── // Pointer-based drag (reliable in Tauri/WKWebView — HTML5 drag API is not) const dragItem = React.useRef(null); // { kind: "traktandum"|"item", idx, tId? } const dragOver = React.useRef(null); // { idx, tId? } const [dragOverTraktandum, setDragOverTraktandum] = React.useState(null); const [dragOverItem, setDragOverItem] = React.useState(null); // { tId, idx } const [draggingTraktandum, setDraggingTraktandum] = React.useState(null); const [draggingItem, setDraggingItem] = React.useState(null); // { tId, idx } const commitDrag = () => { const { kind, idx: from, tId } = dragItem.current || {}; const to = dragOver.current?.idx; dragItem.current = null; dragOver.current = null; setDragOverTraktandum(null); setDragOverItem(null); setDraggingTraktandum(null); setDraggingItem(null); if (from == null || to == null || from === to) return; if (kind === "traktandum") { setP(prev => { const arr = [...(prev.traktanden || [])]; const [moved] = arr.splice(from, 1); arr.splice(to, 0, moved); return { ...prev, traktanden: arr }; }); } else if (kind === "item") { setP(prev => ({ ...prev, traktanden: (prev.traktanden || []).map(t => { if (t.id !== tId) return t; const arr = [...(t.items || [])]; const [moved] = arr.splice(from, 1); arr.splice(to, 0, moved); return { ...t, items: arr }; }), })); } }; React.useEffect(() => { const up = () => { if (dragItem.current) commitDrag(); }; window.addEventListener("mouseup", up); return () => window.removeEventListener("mouseup", up); }); const startDragTraktandum = (idx) => { dragItem.current = { kind: "traktandum", idx }; dragOver.current = { idx }; setDraggingTraktandum(idx); }; const enterTraktandum = (idx) => { if (dragItem.current?.kind !== "traktandum") return; dragOver.current = { idx }; setDragOverTraktandum(idx); }; const startDragItem = (tId, idx) => { dragItem.current = { kind: "item", idx, tId }; dragOver.current = { idx }; setDraggingItem({ tId, idx }); }; const enterItem = (tId, idx) => { if (dragItem.current?.kind !== "item" || dragItem.current?.tId !== tId) return; dragOver.current = { idx }; setDragOverItem({ tId, idx }); }; return (
{ConfirmModalEl} {/* Header */}
{isDirty && ● Ungespeichert}
setField("title", e.target.value)} placeholder="Protokolltitel…" style={{ fontSize: 28, fontFamily: "'Playfair Display', serif", fontWeight: 400, background: "none", border: "none", borderBottom: "2px solid var(--border)", borderRadius: 0, padding: "4px 0", width: "100%", outline: "none", color: "var(--text)" }} />
{p.nummer} {p.type && · {p.type}} {(() => { const proj = data.projects.find(x => x.id === p.projectId); return proj?.number ? {proj.number} : null; })()}
{/* ── LINKE SPALTE: Metadaten + Traktanden ── */}
{/* Kopfdaten */}
SITZUNGSDETAILS
setField("date", e.target.value)} /> setField("time", e.target.value)} /> setField("endTime", e.target.value)} />
setField("location", e.target.value)} placeholder="z.B. Büro Studio, Baustelle…" />
{!p.projectId && ( setField("projectManual", e.target.value)} placeholder="Projektbezeichnung" /> )}
setField("nextDate", e.target.value)} /> setField("verteiler", e.target.value)} placeholder="z.B. alle TN, Archiv…" />
{/* Traktanden – draggable */} {(p.traktanden || []).map((t, ti) => { const isDragTarget = dragOverTraktandum === ti; return (
enterTraktandum(ti)} className="card" style={{ borderLeft: "4px solid #b07848", outline: isDragTarget ? "2px dashed #b07848" : "none", outlineOffset: 2, opacity: draggingTraktandum === ti ? 0.5 : 1, transition: "outline 0.1s", }} > {/* Traktandum-Header */}
{/* Drag handle */}
{ e.preventDefault(); startDragTraktandum(ti); }} >⠿
setTraktandum(t.id, { nr: e.target.value })} style={{ width: 48, height: 32, fontSize: 13, fontWeight: 700, textAlign: "center", background: "#b07848", color: "#1a1a18", border: "none", borderRadius: 4 }} /> setTraktandum(t.id, { title: e.target.value })} placeholder="Traktandentitel…" style={{ flex: 1, height: 32, fontSize: 14, fontWeight: 500, background: "none", border: "none", borderBottom: "1.5px solid var(--border)", borderRadius: 0, outline: "none", color: "var(--text)" }} /> {(p.traktanden || []).length > 1 && ( )}
{/* Items */} {(t.items || []).map((item, ii) => { const isItemTarget = dragOverItem?.tId === t.id && dragOverItem?.idx === ii; return (
enterItem(t.id, ii)} style={{ outline: isItemTarget ? "2px dashed #b07848" : "none", outlineOffset: 1, borderRadius: 4, opacity: draggingItem?.tId === t.id && draggingItem?.idx === ii ? 0.4 : 1, transition: "outline 0.1s", }} >
{/* Item drag handle */}
{ e.preventDefault(); startDragItem(t.id, ii); }} >⠿
setItem(t.id, item.id, changes)} onRemove={() => removeItem(t.id, item.id)} />
); })} {/* Item-Buttons */}
{[ { type: "info", icon: "ℹ", label: "Info", color: "#1a4e8a" }, { type: "beschluss", icon: "✓", label: "Beschluss", color: "#2d6a4f" }, { type: "aufgabe", icon: "→", label: "Aufgabe", color: "#b5621e" }, ].map(btn => ( ))}
); })}
{/* ── RECHTE SPALTE: Teilnehmer + Aufgaben-Zusammenfassung ── */}
{/* Teilnehmer */}
TEILNEHMER ({p.participants.length})
{/* Hinzufügen */}
{/* Teilnehmerliste */} {p.participants.length === 0 ? (
Noch keine Teilnehmer
) : (
{p.participants.map(tn => { const sc = statusConfig[tn.status] || statusConfig.anwesend; return (
{tn.name}
{tn.role &&
{tn.role}
}
); })}
)} {/* Anwesenheits-Statistik */} {p.participants.length > 0 && (
{Object.entries(statusConfig).map(([k, v]) => { const count = p.participants.filter(x => x.status === k).length; return count > 0 ? (
{count} {v.label}
) : null; })}
)}
{/* Aufgaben-Überblick */} {(() => { const tasks = (p.traktanden || []).flatMap(t => (t.items || []).filter(it => it.type === "aufgabe")); if (tasks.length === 0) return null; const offen = tasks.filter(t => t.status !== "erledigt"); const erledigt = tasks.filter(t => t.status === "erledigt"); return (
AUFGABEN ({tasks.length})
{offen.length} offen
{erledigt.length} erledigt
0 ? (erledigt.length / tasks.length) * 100 : 0}%`, height: "100%", background: "#2d6a4f", borderRadius: 3 }} />
{offen.map(t => { const due = t.dueDateType === "kw" ? (t.dueKW ? `KW ${t.dueKW}/${t.dueYear || new Date().getFullYear()}` : "—") : t.dueDate ? formatDate(t.dueDate) : "—"; const isLate = t.dueDate && t.dueDateType === "datum" && t.dueDate < today; return (
{t.text || "—"}
{t.responsible && → {t.responsible}} {due}
); })}
); })()} {/* Beschlüsse-Überblick */} {(() => { const beschluesse = (p.traktanden || []).flatMap(t => (t.items || []).filter(it => it.type === "beschluss")); if (beschluesse.length === 0) return null; return (
BESCHLÜSSE ({beschluesse.length})
{beschluesse.map(b => (
{b.text || "—"}
{b.date ? formatDate(b.date) : "—"}
))}
); })()}
{/* ── Folgesitzung-Dialog ── */} {showFolge && (() => { // Initialisiere Auswahl beim ersten Öffnen if (!folgeSelection) { const sel = (p.traktanden || []).map(t => ({ tId: t.id, tTitle: t.title, tNr: t.nr, include: true, items: (t.items || []).map(it => ({ id: it.id, type: it.type, text: it.text, // Aufgaben: offen/in Arbeit → automatisch übernommen; erledigt → nicht include: it.type !== "aufgabe" ? false : (it.status || "offen") !== "erledigt", isErledigt: it.type === "aufgabe" && (it.status || "offen") === "erledigt", original: it, })), })); setFolgeSelection(sel); return null; } const toggleTraktandum = (tId) => setFolgeSelection(prev => prev.map(t => t.tId === tId ? { ...t, include: !t.include } : t)); const toggleItem = (tId, iId) => setFolgeSelection(prev => prev.map(t => t.tId === tId ? { ...t, items: t.items.map(it => it.id === iId ? { ...it, include: !it.include } : it) } : t)); const createFolge = () => { const allProts = data.protocols || []; const abbr = data.settings.protokollTypeAbbreviations || {}; const typKuerzel = abbr[p.type] || "SO"; const folgeDate = p.nextDate || new Date().toISOString().slice(0, 10); const proj = data.projects.find(x => x.id === p.projectId); const seq = nextProtoSeq(allProts); const newNummer = applyProtoNumberFormat(data.settings.protokollNumberFormat || "YYYY-TT-NN", { date: folgeDate, projectNumber: proj?.number || "", seq, typKuerzel, }); const newTraktanden = folgeSelection .filter(t => t.include) .map(t => ({ id: generateId(), nr: t.tNr, title: t.tTitle, items: t.items .filter(it => it.include) .map(it => ({ ...it.original, id: generateId(), status: it.original.type === "aufgabe" ? (it.original.status === "erledigt" ? "erledigt" : it.original.status) : it.original.status })), })); if (newTraktanden.length === 0) newTraktanden.push({ id: generateId(), nr: "1", title: "", items: [] }); const folge = { id: generateId(), title: "", type: p.type, date: p.nextDate || new Date().toISOString().slice(0, 10), time: p.time || "10:00", endTime: p.endTime || "", location: p.location || "", projectId: p.projectId, projectManual: p.projectManual || "", nummer: newNummer, participants: (p.participants || []).map(pt => ({ ...pt, status: "eingeladen" })), traktanden: newTraktanden, vorgaenger: p.id, nextDate: "", verteiler: p.verteiler || "", createdAt: new Date().toISOString(), }; const updated = { ...data, protocols: [...allProts, folge] }; // Save and navigate to new protokoll if (typeof saveAll === "function") saveAll(updated); setShowFolge(false); setFolgeSelection(null); onSave(p); // save current first setTimeout(() => { window.__openProtokoll = folge.id; window.dispatchEvent(new CustomEvent("openProtokoll", { detail: { id: folge.id } })); }, 100); }; const typeIcons = { info: "ℹ", beschluss: "✅", aufgabe: "📌" }; const typeColors = { info: "#1a4e8a", beschluss: "#2d6a4f", aufgabe: "#b5621e" }; return ( { setShowFolge(false); setFolgeSelection(null); }} onSave={createFolge} saveLabel="Folgesitzung erstellen" wide>
Wähle welche Traktanden und Punkte übernommen werden sollen. ✓ Offene Aufgaben sind vorausgewählt
{folgeSelection.map(t => (
{/* Traktandum-Header */}
0 ? "1px solid var(--border2)" : "none", cursor: "pointer" }} onClick={() => toggleTraktandum(t.tId)}> toggleTraktandum(t.tId)} onClick={e => e.stopPropagation()} style={{ width: "auto", flexShrink: 0 }} /> {t.tNr} {t.tTitle || Kein Titel} {t.items.length} Punkte
{/* Items */} {t.items.map(it => (
toggleItem(t.tId, it.id)} style={{ width: "auto", flexShrink: 0, marginTop: 3 }} /> {typeIcons[it.type]}
{it.text || Leer} {it.isErledigt && ✓ erledigt} {it.original?.responsible && → {it.original.responsible}}
))}
))}
Alle Teilnehmer werden übernommen mit Status «Eingeladen». Datum wird auf «Nächste Sitzung» ({p.nextDate ? formatDate(p.nextDate) : "nicht gesetzt"}) gesetzt.
); })()}
); } // ─── LIEFERSCHEINE ──────────────────────────────────────────────────