'use client'; import { useTranslations } from 'next-intl'; import { useCallback, useEffect, useState } from 'react'; import { FileDropArea } from '@/components/accounting/file-drop-area'; import { TabBar } from '@/components/accounting/tab-bar'; import { PageHeader } from '@/components/layout/page-header'; import { downloadBase64 } from '@/lib/download-base64'; import { exportKantineCsv, getKantineSettings, type KantinePreview, KantineRequestError, type KantineSettings, previewKantineCsv, saveKantineSettings, } from '@/lib/kantine-datev-api'; import { useCanManageModule } from '@/lib/use-module-capability'; type TabId = 'billing' | 'settings'; const INPUT_CLASS = 'w-full rounded border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring'; const BUTTON_CLASS = 'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:cursor-not-allowed disabled:opacity-50'; const euro = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }); /** * Kantinenabrechnung (quick-261002-fm5): Kantinen-CSV hochladen, pruefen und * als DATEV-Lohndatei herunterladen. Die Datei bleibt im Browser (State) und * wird fuer Vorschau UND Export an die API geschickt — die API speichert nichts. */ export default function KantineDatevPage() { const t = useTranslations('kantineDatev'); // Administratoren und Benutzer mit der Freigabestufe Verwalten (261002-icv). const canManage = useCanManageModule('kantine-datev') === true; const [tab, setTab] = useState('billing'); const [settings, setSettings] = useState(null); const [settingsError, setSettingsError] = useState(false); useEffect(() => { let cancelled = false; getKantineSettings() .then((s) => { if (!cancelled) setSettings(s); }) .catch(() => { if (!cancelled) setSettingsError(true); }); return () => { cancelled = true; }; }, []); const tabs: { id: TabId; label: string }[] = [{ id: 'billing', label: t('tabs.billing') }]; if (canManage) tabs.push({ id: 'settings', label: t('tabs.settings') }); const activeTab = tab === 'settings' && !canManage ? 'billing' : tab; return (
{activeTab === 'billing' ? ( setTab('settings')} /> ) : ( )}
); } function BillingTab({ settings, settingsError, canManage, onOpenSettings, }: { settings: KantineSettings | null; settingsError: boolean; canManage: boolean; onOpenSettings: () => void; }) { const t = useTranslations('kantineDatev'); const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); const [checking, setChecking] = useState(false); const [downloading, setDownloading] = useState(false); const [message, setMessage] = useState<{ kind: 'error' | 'ok'; text: string } | null>(null); const requestError = useCallback( (error: unknown): string => { if (error instanceof KantineRequestError) { if (error.code && t.has(`errors.${error.code}`)) return t(`errors.${error.code}`); return error.message; } return t('errors.request'); }, [t], ); const handleFile = async (selected: File) => { setFile(selected); setPreview(null); setMessage(null); setChecking(true); try { setPreview(await previewKantineCsv(selected)); } catch (error) { setMessage({ kind: 'error', text: requestError(error) }); } finally { setChecking(false); } }; const handleClear = () => { setFile(null); setPreview(null); setMessage(null); }; const handleDownload = async () => { if (!file) return; setDownloading(true); setMessage(null); try { const result = await exportKantineCsv(file); downloadBase64(result.filename, result.content, result.mimeType); setMessage({ kind: 'ok', text: t('download.done', { filename: result.filename }) }); } catch (error) { setMessage({ kind: 'error', text: requestError(error) }); } finally { setDownloading(false); } }; const notConfigured = settings !== null && !settings.configured; const canDownload = Boolean(preview?.canExport) && !downloading && !checking; return (
{settingsError &&

{t('errors.request')}

} {notConfigured && (

{canManage ? t('notConfigured.admin') : t('notConfigured.user')}

{canManage && ( )}
)}

{t('noStorage')}

{checking &&

{t('checking')}

} {preview && (
{preview.warnings.length > 0 && (

{t('warnings.title')}

    {preview.warnings.map((w) => (
  • {w.code === 'multipleMonths' && w.params?.months ? t('warnings.multipleMonths', { months: w.params.months.join(', ') }) : w.message}
  • ))}
)} {preview.errors.length > 0 && (

{t('errors.title')}

{preview.errors.map((e) => ( ))}
{t('errors.line')} {t('errors.field')} {t('errors.message')}
{e.row} {t.has(`fields.${e.field}`) ? t(`fields.${e.field}`) : e.field} {t.has(`errors.${e.code}`) ? t(`errors.${e.code}`) : e.message}
)}
)} {message && (

{message.text}

)}
); } function SummaryItem({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function SettingsTab({ settings, onSaved, }: { settings: KantineSettings | null; onSaved: (s: KantineSettings) => void; }) { const t = useTranslations('kantineDatev'); const [beraterNr, setBeraterNr] = useState(''); const [mandantNr, setMandantNr] = useState(''); const [lohnart, setLohnart] = useState(''); const [touched, setTouched] = useState(false); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ kind: 'error' | 'ok'; text: string } | null>(null); // Gespeicherte Werte einmal ins Formular uebernehmen, sobald sie geladen sind. useEffect(() => { if (settings) { setBeraterNr(settings.beraterNr ?? ''); setMandantNr(settings.mandantNr ?? ''); setLohnart(settings.lohnart ?? ''); } }, [settings]); const valid = (v: string) => /^\d{1,10}$/.test(v); const allValid = valid(beraterNr) && valid(mandantNr) && valid(lohnart); const handleSave = async (event: React.FormEvent) => { event.preventDefault(); setTouched(true); if (!allValid) return; setSaving(true); setMessage(null); try { const saved = await saveKantineSettings({ beraterNr, mandantNr, lohnart }); onSaved(saved); setMessage({ kind: 'ok', text: t('settings.saved') }); } catch (error) { setMessage({ kind: 'error', text: error instanceof KantineRequestError ? error.message : t('errors.request'), }); } finally { setSaving(false); } }; const fields = [ { id: 'beraterNr', label: t('settings.beraterNr'), value: beraterNr, set: setBeraterNr }, { id: 'mandantNr', label: t('settings.mandantNr'), value: mandantNr, set: setMandantNr }, { id: 'lohnart', label: t('settings.lohnart'), value: lohnart, set: setLohnart }, ]; return (

{t('settings.intro')}

{fields.map((f) => { const showError = touched && !valid(f.value); return (
f.set(e.target.value.trim())} className={INPUT_CLASS} /> {showError &&

{t('settings.digitsOnly')}

}
); })} {message && (

{message.text}

)}
); }