'use client' import { useState, useEffect, useCallback, ReactNode } from 'react' import { useTheme } from '@/lib/ThemeContext' /** * Shared scaffolding for all 15 constraint editors. * * Each editor follows the same shape: list-load on mount, optional form to * create a new entry, a table to render existing entries, delete with a * confirm. Extracted here so individual editors only carry their schema- * specific bits (form fields, table columns). */ export interface ConstraintApi { list(): Promise create(form: TForm): Promise remove(id: string): Promise } export interface CrudState { items: TItem[] loading: boolean error: string | null showForm: boolean submitting: boolean form: TForm setForm: (f: TForm | ((prev: TForm) => TForm)) => void toggleForm: () => void reload: () => Promise submit: (e?: React.FormEvent) => Promise remove: (id: string, confirmMsg?: string) => Promise setError: (msg: string | null) => void } export function useConstraintCrud( api: ConstraintApi, initialForm: TForm, opts: { onBeforeSubmit?: (form: TForm) => string | null } = {}, ): CrudState { const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [showForm, setShowForm] = useState(false) const [submitting, setSubmitting] = useState(false) const [form, setForm] = useState(initialForm) const reload = useCallback(async () => { setLoading(true) setError(null) try { const data = await api.list() setItems(data || []) } catch (e) { setError(e instanceof Error ? e.message : 'Laden fehlgeschlagen') } finally { setLoading(false) } }, [api]) useEffect(() => { reload() }, [reload]) const submit = useCallback(async (e?: React.FormEvent) => { if (e) e.preventDefault() if (opts.onBeforeSubmit) { const err = opts.onBeforeSubmit(form) if (err) { setError(err) return } } setSubmitting(true) setError(null) try { await api.create(form) setForm(initialForm) setShowForm(false) await reload() } catch (err) { setError(err instanceof Error ? err.message : 'Anlegen fehlgeschlagen') } finally { setSubmitting(false) } }, [api, form, initialForm, opts, reload]) const remove = useCallback(async (id: string, confirmMsg = 'Regel wirklich loeschen?') => { if (!confirm(confirmMsg)) return try { await api.remove(id) await reload() } catch (err) { setError(err instanceof Error ? err.message : 'Loeschen fehlgeschlagen') } }, [api, reload]) return { items, loading, error, showForm, submitting, form, setForm, toggleForm: () => setShowForm(s => !s), reload, submit, remove, setError, } } interface ShellProps { testId: string title: string description: string newLabel: string newDisabled?: boolean prereqWarning?: string | null emptyText: string loadingText?: string tableHeaders: string[] showFormButtonLabel?: string cancelLabel?: string state: { loading: boolean error: string | null showForm: boolean submitting: boolean items: { id: string }[] toggleForm: () => void submit: (e?: React.FormEvent) => void | Promise } formBody: ReactNode renderRow: (item: unknown, idx: number) => ReactNode } export function ConstraintShell({ testId, title, description, newLabel, newDisabled = false, prereqWarning, emptyText, loadingText = 'Laedt…', tableHeaders, cancelLabel = 'Abbrechen', state, formBody, renderRow, }: ShellProps) { const { isDark } = useTheme() const cardClass = isDark ? 'bg-white/10 border-white/20 text-white' : 'bg-white/80 border-black/10 text-slate-900' return (

{title}

{description}

{prereqWarning && !state.loading && (
{prereqWarning}
)} {state.error &&
{state.error}
} {state.showForm && (
{formBody}
)} {state.loading ? (
{loadingText}
) : state.items.length === 0 ? (
{emptyText}
) : (
{tableHeaders.map((h, i) => )} {state.items.map((item, idx) => renderRow(item, idx))}
{h}
)}
) } // ---------- Shared style helpers ---------- export function useShellStyles() { const { isDark } = useTheme() return { isDark, cardClass: isDark ? 'bg-white/10 border-white/20 text-white' : 'bg-white/80 border-black/10 text-slate-900', inputClass: isDark ? 'bg-white/10 border-white/20 text-white' : 'bg-white border-slate-300 text-slate-900', rowClass: isDark ? 'border-t border-white/10' : 'border-t border-slate-200', deleteBtn: 'text-red-400 hover:text-red-300 text-sm', submitBtn: 'w-full px-4 py-2 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white font-medium disabled:opacity-50', } } // ---------- Day-of-week constants reused across editors ---------- export const DAYS = [ { v: 1, label: 'Montag', short: 'Mo' }, { v: 2, label: 'Dienstag', short: 'Di' }, { v: 3, label: 'Mittwoch', short: 'Mi' }, { v: 4, label: 'Donnerstag', short: 'Do' }, { v: 5, label: 'Freitag', short: 'Fr' }, { v: 6, label: 'Samstag', short: 'Sa' }, { v: 7, label: 'Sonntag', short: 'So' }, ] export function dayLabel(v: number): string { return DAYS.find(d => d.v === v)?.label || String(v) }