diff --git a/app/MainClient.tsx b/app/MainClient.tsx index 53f16da..9d85fb9 100644 --- a/app/MainClient.tsx +++ b/app/MainClient.tsx @@ -174,7 +174,7 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) { {activeTab === 'statistik' && (
- Statistik {activeKuppel}-Kuppel + Statistik (alle Kuppeln)
- +
)} diff --git a/app/api/statistik/route.ts b/app/api/statistik/route.ts index d41e79c..4be1943 100644 --- a/app/api/statistik/route.ts +++ b/app/api/statistik/route.ts @@ -7,61 +7,68 @@ export async function GET(request: NextRequest) { if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); const { searchParams } = new URL(request.url); - const kuppel = searchParams.get('kuppel') || 'West'; const year = parseInt(searchParams.get('year') || String(new Date().getFullYear()), 10); try { - // 1) Monatliche Besucherzahlen nach ArtFuehrung const monthlyRows = await query( - `SELECT - MONTH(Beginn) AS monat, - ArtFuehrung, - SUM(Besucher) AS besucher, - COUNT(*) AS anzahl - FROM logbuch - WHERE Kuppel = ? AND YEAR(Beginn) = ? - GROUP BY MONTH(Beginn), ArtFuehrung - ORDER BY monat, ArtFuehrung`, - [kuppel, year] - ) as { monat: number; ArtFuehrung: string; besucher: number; anzahl: number }[]; + 'SELECT' + + ' MONTH(Beginn) AS monat,' + + " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF') THEN 1 END) AS tageFuehrungen," + + " COUNT(CASE WHEN ArtFuehrung = 'Beob' THEN 1 END) AS tageBeob," + + " COUNT(CASE WHEN ArtFuehrung = 'TD' THEN 1 END) AS tageTD," + + " COUNT(CASE WHEN ArtFuehrung = 'Sonst' THEN 1 END) AS tageSonst," + + " COUNT(CASE WHEN ArtFuehrung = 'BEOS' THEN 1 END) AS tageBEOS," + + " COUNT(CASE WHEN ArtFuehrung = 'ToT' THEN 1 END) AS tagesToT," + + " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT') THEN 1 END) AS tageGesamt," + + " SUM(CASE WHEN ArtFuehrung = 'RF' THEN Besucher ELSE 0 END) AS besucherRF," + + " SUM(CASE WHEN ArtFuehrung = 'SF' THEN Besucher ELSE 0 END) AS besucherSF," + + " SUM(CASE WHEN ArtFuehrung = 'SonF' THEN Besucher ELSE 0 END) AS besucherSonF," + + " SUM(CASE WHEN ArtFuehrung = 'PrF' THEN Besucher ELSE 0 END) AS besucherPrF," + + " SUM(CASE WHEN ArtFuehrung = 'ToT' THEN Besucher ELSE 0 END) AS besucherToT," + + " SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS besucherGesamt" + + ' FROM logbuch' + + ' WHERE YEAR(Beginn) = ?' + + ' GROUP BY MONTH(Beginn)' + + ' ORDER BY monat', + [year] + ) as { + monat: number; + tageFuehrungen: number; tageBeob: number; tageTD: number; tageSonst: number; tageBEOS: number; tagesToT: number; tageGesamt: number; + besucherRF: number; besucherSF: number; besucherSonF: number; besucherPrF: number; + besucherToT: number; besucherGesamt: number; + }[]; - // 2) Kumulierte Besucher im Jahr const cumulativeRows = await query( - `SELECT SUM(Besucher) AS total FROM logbuch WHERE Kuppel = ? AND YEAR(Beginn) = ?`, - [kuppel, year] - ) as { total: number | null }[]; - - // 3) Anzahl Führungstage (distinct Datum) - const tageRows = await query( - `SELECT COUNT(DISTINCT DATE(Beginn)) AS tage FROM logbuch WHERE Kuppel = ? AND YEAR(Beginn) = ?`, - [kuppel, year] - ) as { tage: number }[]; - - // 4) Kumulierte Besucher über alle Kuppeln - const allCumulativeRows = await query( - `SELECT SUM(Besucher) AS total FROM logbuch WHERE YEAR(Beginn) = ?`, + "SELECT SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS total" + + ' FROM logbuch WHERE YEAR(Beginn) = ?', [year] ) as { total: number | null }[]; - // 5) Führungstage über alle Kuppeln - const allTageRows = await query( - `SELECT COUNT(DISTINCT DATE(Beginn)) AS tage FROM logbuch WHERE YEAR(Beginn) = ?`, + const tageRows = await query( + "SELECT COUNT(*) AS tage FROM logbuch WHERE YEAR(Beginn) = ? AND ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT')", [year] ) as { tage: number }[]; return NextResponse.json({ monthly: monthlyRows.map((r) => ({ monat: Number(r.monat), - ArtFuehrung: r.ArtFuehrung, - besucher: Number(r.besucher), - anzahl: Number(r.anzahl), + tageFuehrungen: Number(r.tageFuehrungen), + tageBeob: Number(r.tageBeob), + tageTD: Number(r.tageTD), + tageSonst: Number(r.tageSonst), + tageBEOS: Number(r.tageBEOS), + tagesToT: Number(r.tagesToT), + tageGesamt: Number(r.tageGesamt), + besucherRF: Number(r.besucherRF), + besucherSF: Number(r.besucherSF), + besucherSonF: Number(r.besucherSonF), + besucherPrF: Number(r.besucherPrF), + besucherToT: Number(r.besucherToT), + besucherGesamt: Number(r.besucherGesamt), })), cumulative: Number(cumulativeRows[0]?.total ?? 0), tage: Number(tageRows[0]?.tage ?? 0), - allCumulative: Number(allCumulativeRows[0]?.total ?? 0), - allTage: Number(allTageRows[0]?.tage ?? 0), year, - kuppel, }); } catch (error) { console.error('GET /api/statistik:', error); diff --git a/components/LogbuchForm.tsx b/components/LogbuchForm.tsx index eaa30aa..25ab750 100644 --- a/components/LogbuchForm.tsx +++ b/components/LogbuchForm.tsx @@ -134,12 +134,13 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved } }, [editEntry]); - // Objekte-Vorauswahl je nach Art der Führung + // Objekte-Vorauswahl je nach Art der Führung; Besucher zurücksetzen wenn nicht relevant useEffect(() => { if (artFuehrung === SONNE_ART) { setObjekte([{ ID: null, Name: 'Sonne' }]); } else if (NO_OBJEKTE_ARTEN.includes(artFuehrung)) { setObjekte([]); + setBesucher(''); } }, [artFuehrung]); diff --git a/components/Statistik.tsx b/components/Statistik.tsx index 97ebb0a..9439d96 100644 --- a/components/Statistik.tsx +++ b/components/Statistik.tsx @@ -1,24 +1,29 @@ 'use client'; -import { useEffect, useMemo, useState } from 'react'; -import type { ArtFuehrung, Kuppel } from '@/types/logbuch'; -import { artLabel } from '@/types/logbuch'; +import { useEffect, useState } from 'react'; -interface MonthlyRow { +interface MonthRow { monat: number; - ArtFuehrung: string; - besucher: number; - anzahl: number; + tageFuehrungen: number; + tageBeob: number; + tageTD: number; + tageSonst: number; + tageBEOS: number; + tagesToT: number; + tageGesamt: number; + besucherRF: number; + besucherSF: number; + besucherSonF: number; + besucherPrF: number; + besucherToT: number; + besucherGesamt: number; } interface StatsData { - monthly: MonthlyRow[]; + monthly: MonthRow[]; cumulative: number; tage: number; - allCumulative: number; - allTage: number; year: number; - kuppel: Kuppel; } const MONATE = [ @@ -26,73 +31,44 @@ const MONATE = [ 'Juli','August','September','Oktober','November','Dezember', ]; -interface Props { - kuppel: Kuppel; +function n(v: number) { + return v > 0 ? v.toLocaleString('de-DE') : ''; } -export default function Statistik({ kuppel }: Props) { +export default function Statistik() { const [year, setYear] = useState(new Date().getFullYear()); const [data, setData] = useState(null); - const [fetchError, setFetchError] = useState<{ year: number; kuppel: Kuppel } | null>(null); + const [fetchError, setFetchError] = useState(null); - const error = fetchError?.year === year && fetchError?.kuppel === kuppel - ? 'Fehler beim Laden der Statistik.' - : ''; - const loading = !error && (!data || data.year !== year || data.kuppel !== kuppel); + const error = fetchError === year ? 'Fehler beim Laden der Statistik.' : ''; + const loading = !error && (!data || data.year !== year); useEffect(() => { let cancelled = false; - fetch(`/api/statistik?kuppel=${encodeURIComponent(kuppel)}&year=${year}`) + fetch(`/api/statistik?year=${year}`) .then((r) => { if (!r.ok) throw new Error(); return r.json(); }) .then((d: StatsData) => { if (!cancelled) { setData(d); setFetchError(null); } }) - .catch(() => { if (!cancelled) setFetchError({ year, kuppel }); }); + .catch(() => { if (!cancelled) setFetchError(year); }); return () => { cancelled = true; }; - }, [kuppel, year]); - - const { arten, matrix, monatTotal, artTotal, grandTotal, anzahlTotal } = useMemo(() => { - if (!data) { - return { arten: [] as string[], matrix: [] as (number | null)[][], monatTotal: [] as number[], artTotal: [] as number[], grandTotal: 0, anzahlTotal: [] as number[] }; - } - - const artenSet = new Set(); - data.monthly.forEach((r) => artenSet.add(r.ArtFuehrung)); - const arten = Array.from(artenSet).sort(); - - const matrix: (number | null)[][] = []; - const monatTotal: number[] = []; - const anzahlTotal: number[] = []; - const artTotal: number[] = new Array(arten.length).fill(0); - let grandTotal = 0; - - for (let m = 1; m <= 12; m++) { - const row: (number | null)[] = []; - let mSum = 0; - let aSum = 0; - arten.forEach((art, idx) => { - const found = data.monthly.find((r) => r.monat === m && r.ArtFuehrung === art); - const val = found ? found.besucher : null; - row.push(val); - if (val !== null) { - mSum += val; - artTotal[idx] += val; - aSum += found!.anzahl; - } - }); - matrix.push(row); - monatTotal.push(mSum); - anzahlTotal.push(aSum); - grandTotal += mSum; - } - - return { arten, matrix, monatTotal, artTotal, grandTotal, anzahlTotal }; - }, [data]); + }, [year]); if (loading) return
Lade Statistik...
; if (error) return
{error}
; - const headCls = 'px-3 py-2 border border-gray-300 text-xs font-semibold bg-gray-100 whitespace-nowrap'; - const cellCls = 'px-3 py-2 border border-gray-200 text-sm text-right tabular-nums'; - const labelCls = 'px-3 py-2 border border-gray-200 text-sm text-left whitespace-nowrap'; + const rows = data!.monthly; + + function col(key: keyof MonthRow): number { + return rows.reduce((s, r) => s + (r[key] as number), 0); + } + + const thTop = 'px-3 py-2 border border-gray-300 text-xs font-semibold bg-gray-100 text-center'; + const thSub = 'px-3 py-2 border border-gray-300 text-xs font-semibold bg-gray-50 whitespace-nowrap'; + const thDiv = 'px-3 py-2 border border-gray-300 border-l-4 border-l-gray-400 text-xs font-semibold bg-gray-50 whitespace-nowrap'; + const td = 'px-3 py-2 border border-gray-200 text-sm text-right tabular-nums'; + const tdDiv = 'px-3 py-2 border border-gray-200 border-l-4 border-l-gray-400 text-sm text-right tabular-nums'; + const tdL = 'px-3 py-2 border border-gray-200 text-sm text-left whitespace-nowrap'; + const tdSum = 'px-3 py-2 border border-gray-200 text-sm text-right tabular-nums font-semibold bg-gray-50'; + const tdSumDiv = 'px-3 py-2 border border-gray-200 border-l-4 border-l-gray-400 text-sm text-right tabular-nums font-semibold bg-gray-50'; return (
@@ -112,63 +88,80 @@ export default function Statistik({ kuppel }: Props) { - - - {arten.map((art) => ( - - ))} - + + + + + + + + + + + + + + + + + + {MONATE.map((name, idx) => { - const mSum = monatTotal[idx]; - const aSum = anzahlTotal[idx]; + const m = idx + 1; + const r = rows.find((row) => row.monat === m); + const hasData = r && r.tageGesamt > 0; + const cls = hasData ? '' : 'text-gray-400'; return ( - 0 ? '' : 'text-gray-400'}> - - - {arten.map((_, aIdx) => { - const val = matrix[idx][aIdx]; - return ( - - ); - })} - + + + + + + + + + + + + + + + ); })} - - - - {artTotal.map((t, i) => ( - - ))} - + + + + + + + + + + + + + + +
MonatFührungen{artLabel(art as ArtFuehrung)}GesamtMonatBesucherAnzahl
RFSFSonFPrFToTGesamtFühr.Beob.TDSonst.BEOSToTGesamt
{name}{aSum > 0 ? aSum : ''} - {val !== null && val > 0 ? val.toLocaleString('de-DE') : ''} - {mSum > 0 ? mSum.toLocaleString('de-DE') : ''}
{name}{r ? n(r.besucherRF) : ''}{r ? n(r.besucherSF) : ''}{r ? n(r.besucherSonF) : ''}{r ? n(r.besucherPrF) : ''}{r ? n(r.besucherToT) : ''}{r ? n(r.besucherGesamt) : ''}{r ? n(r.tageFuehrungen) : ''}{r ? n(r.tageBeob) : ''}{r ? n(r.tageTD) : ''}{r ? n(r.tageSonst) : ''}{r ? n(r.tageBEOS) : ''}{r ? n(r.tagesToT) : ''}{r ? n(r.tageGesamt) : ''}
Summe{anzahlTotal.reduce((s, v) => s + v, 0) > 0 ? anzahlTotal.reduce((s, v) => s + v, 0).toLocaleString('de-DE') : ''}{t > 0 ? t.toLocaleString('de-DE') : ''}{grandTotal > 0 ? grandTotal.toLocaleString('de-DE') : ''}
Summe{n(col('besucherRF'))}{n(col('besucherSF'))}{n(col('besucherSonF'))}{n(col('besucherPrF'))}{n(col('besucherToT'))}{n(col('besucherGesamt'))}{n(col('tageFuehrungen'))}{n(col('tageBeob'))}{n(col('tageTD'))}{n(col('tageSonst'))}{n(col('tageBEOS'))}{n(col('tagesToT'))}{n(col('tageGesamt'))}
-
+
-
Kumulierte Besucher {year} ({data?.kuppel})
+
Besucher {year}
{data?.cumulative.toLocaleString('de-DE') ?? 0}
-
Führungstage {year} ({data?.kuppel})
+
Führungen {year}
{data?.tage ?? 0}
-
-
Kumulierte Besucher {year} (Sternwarte gesamt)
-
{data?.allCumulative.toLocaleString('de-DE') ?? 0}
-
-
-
Führungstage {year} (Sternwarte gesamt)
-
{data?.allTage ?? 0}
-
);