'use client'; import { useEffect, useRef, useState } from 'react'; interface FahrkostenRow { ID: number; Kuerzel: string; Name: string; Km: number; Anzahl: number; } const SATZ = Number(process.env.NEXT_PUBLIC_FAHRKOSTEN_SATZ ?? 15); // Der in der DB gespeicherte km-Wert gilt für die einfache Fahrt — für Hin- und Rückfahrt verdoppeln. const FAHRTEN_PRO_TERMIN = 2; function defaultAb(): string { const d = new Date(); return `${d.getFullYear()}-01-01`; } function defaultBis(): string { const d = new Date(); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${d.getFullYear()}-${mm}-${dd}`; } export default function Fahrkosten() { const [ab, setAb] = useState(defaultAb); // Leeres `bis` bedeutet „bis heute“ — das Backend setzt dann den heutigen Tag ein. const [bis, setBis] = useState(''); const [data, setData] = useState<{ ab: string; bis: string; rows: FahrkostenRow[] } | null>(null); const [errorKey, setErrorKey] = useState(null); const bisRef = useRef(null); const key = `${ab}|${bis}`; useEffect(() => { let cancelled = false; fetch(`/api/fahrkosten?ab=${ab}${bis ? `&bis=${bis}` : ''}`) .then((r) => { if (!r.ok) throw new Error(); return r.json(); }) .then((d: FahrkostenRow[]) => { if (!cancelled) setData({ ab, bis, rows: d }); }) .catch(() => { if (!cancelled) setErrorKey(key); }); return () => { cancelled = true; }; }, [ab, bis, key]); // Derive status from state tagged with the `ab|bis` it belongs to, so that // changing the range shows the loading state without a synchronous setState. const rows = data && data.ab === ab && data.bis === bis ? data.rows : null; const error = errorKey === key; const loading = !rows && !error; const gesamt = rows ? rows.reduce((s, r) => s + r.Anzahl * SATZ, 0) : 0; const gesamtKm = rows ? rows.reduce((s, r) => s + r.Anzahl * r.Km * FAHRTEN_PRO_TERMIN, 0) : 0; const thCls = 'px-3 py-2 border border-gray-300 text-xs font-semibold bg-gray-100 text-left whitespace-nowrap text-gray-900'; const thNarrowCls = `${thCls} w-16`; const tdCls = 'px-3 py-2 border border-gray-200 text-sm text-gray-900'; const tdNarrowCls = `${tdCls} w-16`; const tdNumCls = 'px-3 py-2 border border-gray-200 text-sm text-right tabular-nums w-20 text-gray-900'; const tdKmCls = 'px-3 py-2 border border-gray-200 text-sm text-right tabular-nums w-28 whitespace-nowrap text-gray-900'; const abFormatted = new Date(ab + 'T00:00:00').toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }); const bisFormatted = new Date((bis || defaultBis()) + 'T00:00:00').toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }); return (
setAb(e.target.value)} className="px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-blue-500 focus:outline-none" />
setBis(e.target.value)} className="px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-blue-500 focus:outline-none" /> {!bis && ( )}
{/* Druckkopf */}
Sternwarte Welzheim — Fahrkostenabrechnung
Führungen von {abFormatted} bis {bisFormatted} · {SATZ} € pro Führung
Ausdruck vom {new Date().toLocaleDateString('de-DE')}
{loading &&
Lade...
} {error &&
Fehler beim Laden.
} {!loading && !error && rows && ( <> {rows.length === 0 ? (
Keine Führungen in diesem Zeitraum gefunden.
) : (
{rows.map((r) => ( ))}
Name Kürzel Fahrten Fahrkosten Kilometer
{r.Name} {r.Kuerzel} {r.Anzahl} {(r.Anzahl * SATZ).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} {(r.Anzahl * r.Km * FAHRTEN_PRO_TERMIN).toLocaleString('de-DE')} km
Gesamt {rows.reduce((s, r) => s + r.Anzahl, 0)} {gesamt.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} {gesamtKm.toLocaleString('de-DE')} km
)} )}
); }