import React, { useState, useEffect, useRef } from "react"; import { STATUS_COLORS, STATUS_BG } from "../constants.js"; export function StatusBadge({ status }) { const color = STATUS_COLORS[status] || "#888"; const bg = STATUS_BG[status] || "#f5f5f5"; return ( {status} ); } export function StatusSelect({ value, options, onChange }) { const color = STATUS_COLORS[value] || "#888"; const bg = STATUS_BG[value] || "#f5f5f5"; return ( ); } export function Header({ title, action }) { return (

{title}

{action}
); } export function FormField({ label, children }) { return (
{children}
); } export function Modal({ title, onClose, onSave, saveLabel, hideSave, children, wide, overflow }) { return (
e.target === e.currentTarget && onClose()}>

{title}

{children}
{!hideSave && }
); } export function StudioLogo({ settings, size = 24 }) { if (settings.logo) { const h = settings.logoSize || 60; return {settings.name}; } return
{settings.name}
; } export function useConfirm() { const [pending, setPending] = useState(null); const askConfirm = (msg, confirmLabel = "Löschen") => new Promise(resolve => setPending({ msg, confirmLabel, resolve })); const ConfirmModalEl = pending ? (
{pending.msg}
) : null; return { askConfirm, ConfirmModalEl }; } export function DateInput({ value, onChange, style, ...props }) { const toDE = (iso) => { if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ""; return `${iso.slice(8, 10)}.${iso.slice(5, 7)}.${iso.slice(0, 4)}`; }; const toISO = (de) => { const m = de.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/); if (!m) return null; const iso = `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`; return isNaN(new Date(iso).getTime()) ? null : iso; }; const [text, setText] = React.useState(() => toDE(value)); React.useEffect(() => { const de = toDE(value); if (de !== text) setText(de); }, [value]); const handleChange = (e) => { const digits = e.target.value.replace(/\D/g, "").slice(0, 8); let fmt = ""; if (digits.length <= 2) fmt = digits; else if (digits.length <= 4) fmt = `${digits.slice(0, 2)}.${digits.slice(2)}`; else fmt = `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; setText(fmt); const iso = toISO(fmt); if (iso) onChange({ target: { value: iso } }); }; return ( { if (!toISO(text) && value) setText(toDE(value)); }} placeholder="TT.MM.JJJJ" style={style} {...props} /> ); } export function RichEditor({ value, onChange, minHeight = 420, compact = false }) { const editorRef = React.useRef(null); const isInternalChange = React.useRef(false); const lastValue = React.useRef(value); const savedRange = React.useRef(null); const colorInputRef = React.useRef(null); const [colorValue, setColorValue] = React.useState("#000000"); // Only push content into DOM when value changes from outside (template load) useEffect(() => { if (!editorRef.current) return; if (value !== lastValue.current && !isInternalChange.current) { editorRef.current.innerHTML = value; lastValue.current = value; } isInternalChange.current = false; }, [value]); const saveSelection = () => { const sel = window.getSelection(); if (sel && sel.rangeCount > 0 && editorRef.current?.contains(sel.anchorNode)) { savedRange.current = sel.getRangeAt(0).cloneRange(); } }; const restoreSelection = () => { editorRef.current?.focus(); if (!savedRange.current) { // Place cursor at end if no saved range const range = document.createRange(); range.selectNodeContents(editorRef.current); range.collapse(false); const sel = window.getSelection(); if (sel) { sel.removeAllRanges(); sel.addRange(range); } return; } const sel = window.getSelection(); if (sel) { sel.removeAllRanges(); sel.addRange(savedRange.current); } }; const exec = (cmd, val = null) => { restoreSelection(); document.execCommand(cmd, false, val); saveSelection(); // Sync content after command isInternalChange.current = true; const html = editorRef.current?.innerHTML || ""; lastValue.current = html; onChange(html); }; const TB = ({ cmd, val, title, style: s, children }) => ( ); const Sep = () =>
; return (
B I U {/* Font size */} {/* Color */} { setColorValue(e.target.value); exec("foreColor", e.target.value); }} style={{ position: "absolute", width: 0, height: 0, opacity: 0, pointerEvents: "none" }} />
{/* Editor */}
{ isInternalChange.current = true; const html = editorRef.current?.innerHTML || ""; lastValue.current = html; onChange(html); }} onKeyUp={saveSelection} onMouseUp={saveSelection} onBlur={saveSelection} onPaste={e => { e.preventDefault(); const text = e.clipboardData.getData("text/plain"); restoreSelection(); document.execCommand("insertText", false, text); saveSelection(); }} style={{ minHeight, padding: compact ? "8px 10px" : "20px 24px", outline: "none", fontSize: compact ? 12 : 13, lineHeight: 1.8, color: "var(--text)", background: "var(--surface)", fontFamily: "'DM Mono', 'Courier New', monospace", }} />
); } export function CalendarPopup({ value, onChange, onClose, align = "left", showClear = true }) { const [viewYM, setViewYM] = useState(() => { const d = value ? new Date(value + "T00:00:00") : new Date(); return { year: d.getFullYear(), month: d.getMonth() }; }); const todayStr = new Date().toISOString().slice(0, 10); const { year, month } = viewYM; const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startWeekday = (firstDay.getDay() + 6) % 7; const cells = []; for (let i = 0; i < startWeekday; i++) cells.push(null); for (let d = 1; d <= lastDay.getDate(); d++) { cells.push(`${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`); } const prevMonth = () => { const d = new Date(year, month - 1, 1); setViewYM({ year: d.getFullYear(), month: d.getMonth() }); }; const nextMonth = () => { const d = new Date(year, month + 1, 1); setViewYM({ year: d.getFullYear(), month: d.getMonth() }); }; const select = (ds) => { onChange(ds); onClose(); }; return (
e.stopPropagation()} >
{firstDay.toLocaleDateString("de-CH", { month: "long", year: "numeric" })}
{["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"].map(d => (
{d}
))}
{cells.map((ds, i) => { if (!ds) return
; const dow = new Date(ds + "T00:00:00").getDay(); const isWeekend = dow === 0 || dow === 6; const isSelected = ds === value; const isToday = ds === todayStr; return ( ); })}
{showClear && value && (
)}
); } export function DatePicker({ value, onChange, style, placeholder, align = "left", ...props }) { const [open, setOpen] = useState(false); const wrapRef = useRef(null); const toDE = (iso) => { if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ""; return `${iso.slice(8, 10)}.${iso.slice(5, 7)}.${iso.slice(0, 4)}`; }; useEffect(() => { if (!open) return; const close = (e) => { if (!wrapRef.current?.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", close); return () => document.removeEventListener("mousedown", close); }, [open]); return (
setOpen(o => !o)} placeholder={placeholder || "TT.MM.JJJJ"} style={{ cursor: "pointer", ...style }} {...props} /> {open && ( onChange({ target: { value: ds } })} onClose={() => setOpen(false)} align={align} /> )}
); } export function NavArrows({ onPrev, onNext, disabledNext = false }) { const btn = (onClick, disabled, children) => ( ); return (
{btn(onPrev, false, "‹")}
{btn(onNext, disabledNext, "›")}
); } export function useCalendarNav() { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const close = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", close); return () => document.removeEventListener("mousedown", close); }, [open]); return { open, setOpen, ref }; }