From ba869e373db7f070159c38eaf3749ed21c13e9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Fri, 10 Jul 2026 15:42:28 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Fahrkosten=20'Bis=20Datum'=20Filter=20(?= =?UTF-8?q?Default=20heute)=20=E2=80=94=20Version=201.11.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/fahrkosten/route.ts | 6 ++- components/Fahrkosten.tsx | 73 ++++++++++++++++++++++++++++++------- lib/phpdb.ts | 5 ++- package.json | 2 +- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/app/api/fahrkosten/route.ts b/app/api/fahrkosten/route.ts index a765d3c..3325b9f 100644 --- a/app/api/fahrkosten/route.ts +++ b/app/api/fahrkosten/route.ts @@ -13,9 +13,13 @@ export async function GET(req: Request) { if (!ab || !/^\d{4}-\d{2}-\d{2}$/.test(ab)) { return NextResponse.json({ error: 'Parameter ab (YYYY-MM-DD) fehlt' }, { status: 400 }); } + const bis = searchParams.get('bis') ?? new Date().toISOString().slice(0, 10); + if (!/^\d{4}-\d{2}-\d{2}$/.test(bis)) { + return NextResponse.json({ error: 'Parameter bis (YYYY-MM-DD) ungültig' }, { status: 400 }); + } try { - const rows = await phpdb.getFahrkosten(ab); + const rows = await phpdb.getFahrkosten(ab, bis); return NextResponse.json(rows); } catch (error) { console.error('GET /api/fahrkosten:', error); diff --git a/components/Fahrkosten.tsx b/components/Fahrkosten.tsx index 6a6e234..bdaf853 100644 --- a/components/Fahrkosten.tsx +++ b/components/Fahrkosten.tsx @@ -1,53 +1,76 @@ 'use client'; -import { useEffect, useState } from 'react'; +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); - const [rows, setRows] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); + // 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; - setLoading(true); - setError(''); - fetch(`/api/fahrkosten?ab=${ab}`) + fetch(`/api/fahrkosten?ab=${ab}${bis ? `&bis=${bis}` : ''}`) .then((r) => { if (!r.ok) throw new Error(); return r.json(); }) - .then((d: FahrkostenRow[]) => { if (!cancelled) { setRows(d); setLoading(false); } }) - .catch(() => { if (!cancelled) { setError('Fehler beim Laden.'); setLoading(false); } }); + .then((d: FahrkostenRow[]) => { if (!cancelled) setData({ ab, bis, rows: d }); }) + .catch(() => { if (!cancelled) setErrorKey(key); }); return () => { cancelled = true; }; - }, [ab]); + }, [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 ab {abFormatted} · {SATZ} € pro Führung
+
Führungen von {abFormatted} bis {bisFormatted} · {SATZ} € pro Führung
Ausdruck vom {new Date().toLocaleDateString('de-DE')}
{loading &&
Lade...
} - {error &&
{error}
} + {error &&
Fehler beim Laden.
} {!loading && !error && rows && ( <> @@ -78,8 +120,9 @@ export default function Fahrkosten() { Name Kürzel - Führungen + Fahrten Fahrkosten + Kilometer @@ -89,6 +132,7 @@ export default function Fahrkosten() { {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 ))} @@ -97,6 +141,7 @@ export default function Fahrkosten() { Gesamt {rows.reduce((s, r) => s + r.Anzahl, 0)} {gesamt.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} + {gesamtKm.toLocaleString('de-DE')} km diff --git a/lib/phpdb.ts b/lib/phpdb.ts index 838a4a0..8616c69 100644 --- a/lib/phpdb.ts +++ b/lib/phpdb.ts @@ -148,11 +148,12 @@ export interface FahrkostenRow { ID: number; Kuerzel: string; Name: string; + Km: number; Anzahl: number; } -export async function getFahrkosten(ab: string): Promise { - return call('LB_FAHRKOSTEN', { ab }); +export async function getFahrkosten(ab: string, bis: string): Promise { + return call('LB_FAHRKOSTEN', { ab, bis }); } export interface StatistikResult { diff --git a/package.json b/package.json index 5deac96..3147f38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "logbuch", - "version": "1.10.3", + "version": "1.11.0", "private": true, "scripts": { "dev": "next dev",