import React, { useEffect, useState } from 'react'; import { Button, Card, ErrorBox, Field, Spinner, Toggle, inputClass } from '../ui/controls'; import { Icon, type IconName } from '../ui/Icon'; import { Team } from '../team/Team'; import { SsoDirectory } from './SsoDirectory'; import { HrworksSettings } from './HrworksSettings'; import { useAuth } from '../../lib/auth'; import { download, settingsApi, type AdminSettings } from '../../lib/api'; import { useLoader } from '../../lib/hooks'; import { useRouter } from '../../lib/router'; import { useToast } from '../Toast'; type Tab = 'allgemein' | 'export' | 'sso' | 'sso-verwaltung' | 'hrworks' | 'benutzer'; const TABS: { key: Tab; label: string; icon: IconName; adminOnly: boolean }[] = [ { key: 'allgemein', label: 'Allgemein', icon: 'settings', adminOnly: true }, { key: 'export', label: 'Export: Kopf & Fuß', icon: 'document', adminOnly: true }, { key: 'sso', label: 'SSO / Entra ID', icon: 'key', adminOnly: true }, { key: 'hrworks', label: 'Abwesenheiten (HRworks)', icon: 'calendar', adminOnly: true }, // Management sieht in den Einstellungen nur diesen Reiter. { key: 'sso-verwaltung', label: 'SSO-Verwaltung', icon: 'globe', adminOnly: false }, { key: 'benutzer', label: 'Benutzerverwaltung', icon: 'team', adminOnly: true }, ]; export const Settings: React.FC = () => { const { route, navigate } = useRouter(); const { user } = useAuth(); const isAdmin = user?.role === 'admin'; const tabs = TABS.filter((t) => isAdmin || !t.adminOnly); const tab = (tabs.find((t) => t.key === route.params[0])?.key ?? tabs[0].key) as Tab; const { data, error, reload, setData } = useLoader(() => (isAdmin ? settingsApi.all() : Promise.resolve(null)), [isAdmin]); return (
{tab === 'sso-verwaltung' ? ( ) : tab === 'benutzer' ? ( ) : error && !data ? ( ) : !data ? ( ) : (
{tab === 'allgemein' && setData(s)} />} {tab === 'export' && setData(s)} />} {tab === 'sso' && } {tab === 'hrworks' && }
)}
); }; function useSaver() { const { addToast } = useToast(); const { reloadSettings } = useAuth(); const [busy, setBusy] = useState(false); const save = async (changes: Parameters[0], after?: () => void) => { setBusy(true); try { await settingsApi.save(changes); addToast('Einstellungen gespeichert.', 'success'); reloadSettings(); after?.(); } catch (err) { addToast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error'); } finally { setBusy(false); } }; return { busy, save }; } const Section: React.FC<{ title: string; hint?: string; children: React.ReactNode; footer?: React.ReactNode }> = ({ title, hint, children, footer }) => (

{title}

{hint &&

{hint}

}
{children}
{footer &&
{footer}
}
); const General: React.FC<{ settings: AdminSettings; onSaved: (s: AdminSettings) => void }> = ({ settings, onSaved }) => { const [form, setForm] = useState({ weekly_hours: String(settings.weekly_hours), hours_per_day: String(settings.hours_per_day), budget_warn_percent: String(settings.budget_warn_percent), budget_warn_days: String(settings.budget_warn_days), company_name: settings.company_name, project_code_pattern: settings.project_code_pattern, project_name_pattern: settings.project_name_pattern, }); const { busy, save } = useSaver(); const set = (patch: Partial) => setForm({ ...form, ...patch }); const n = (v: string) => Number(v.replace(',', '.')); const example = form.project_code_pattern.replaceAll('{KUERZEL}', 'KUKA').replaceAll('{JAHR}', '2026').replaceAll('{JJ}', '26').replaceAll('{NR}', '003'); const exampleName = form.project_name_pattern.replaceAll('{KUNDE}', 'KUKA').replaceAll('{KUNDENNAME}', 'KUKA AG').replaceAll('{PROJEKT}', 'PMO').replaceAll('{CODE}', example); const submit = () => { const changes = { weekly_hours: n(form.weekly_hours), hours_per_day: n(form.hours_per_day), budget_warn_percent: n(form.budget_warn_percent), budget_warn_days: n(form.budget_warn_days), company_name: form.company_name, project_code_pattern: form.project_code_pattern, project_name_pattern: form.project_name_pattern, }; save(changes, () => onSaved({ ...settings, ...changes })); }; return (
set({ weekly_hours: e.target.value })} /> set({ hours_per_day: e.target.value })} /> set({ company_name: e.target.value })} />
set({ budget_warn_percent: e.target.value })} /> set({ budget_warn_days: e.target.value })} />
Speichern } >
Platzhalter: {'{KUERZEL} {JAHR} {JJ} {NR}'} · Beispiel: {example}}> set({ project_code_pattern: e.target.value })} /> Platzhalter: {'{KUNDE} {KUNDENNAME} {PROJEKT} {CODE}'} · Beispiel: {exampleName}}> set({ project_name_pattern: e.target.value })} />
); }; const ExportSettings: React.FC<{ settings: AdminSettings; onSaved: (s: AdminSettings) => void }> = ({ settings, onSaved }) => { const [ex, setEx] = useState(settings.export); const { busy, save } = useSaver(); const { addToast } = useToast(); const set = (patch: Partial) => setEx({ ...ex, ...patch }); const onLogo = (file: File | undefined) => { if (!file) return; if (!/image\/(png|jpe?g)/.test(file.type)) return addToast('Bitte PNG oder JPG wählen.', 'error'); if (file.size > 1_800_000) return addToast('Das Logo ist zu groß (max. 1,8 MB).', 'error'); const reader = new FileReader(); reader.onload = () => set({ logo: String(reader.result) }); reader.readAsDataURL(file); }; const preview = async (format: 'pdf' | 'xlsx') => { try { await download(`/settings/export-preview?format=${format}`, `Vorschau.${format}`, format === 'pdf'); } catch (err) { addToast(err instanceof Error ? err.message : 'Vorschau fehlgeschlagen.', 'error'); } }; const text = (key: keyof typeof ex, label: string, hint?: string) => ( set({ [key]: e.target.value })} /> ); return (
} >

Die Vorschau zeigt den zuletzt gespeicherten Stand.

Logo
Logo
{ex.logo && ( )}
set({ accent: e.target.value })} /> set({ accent: e.target.value })} />
{text('title', 'Titel', 'z. B. Stundennachweis, Leistungsnachweis')} {text('subtitle', 'Kopfzeile rechts (1. Zeile)')} {text('header_right', 'Kopfzeile rechts (2. Zeile)')}