d5bd359802
- Statistik gilt jetzt für alle Kuppeln gemeinsam (kein Kuppel-Filter mehr) - Neue Tabellenstruktur: Besucher (RF/SF/SonF/PrF/ToT/Gesamt) | Anzahl (Führ./Beob./TD/Sonst./BEOS/ToT/Gesamt) - Anzahl zählt Einträge (COUNT(*)) statt distinkte Tage - Zusammenfassungskacheln auf 2 reduziert (Besucher + Führungen gesamt) - Fix: Besucher-State wird zurückgesetzt wenn ArtFuehrung auf TD/BEOS wechselt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
7.0 KiB
TypeScript
169 lines
7.0 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
interface MonthRow {
|
|
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;
|
|
}
|
|
|
|
interface StatsData {
|
|
monthly: MonthRow[];
|
|
cumulative: number;
|
|
tage: number;
|
|
year: number;
|
|
}
|
|
|
|
const MONATE = [
|
|
'Januar','Februar','März','April','Mai','Juni',
|
|
'Juli','August','September','Oktober','November','Dezember',
|
|
];
|
|
|
|
function n(v: number) {
|
|
return v > 0 ? v.toLocaleString('de-DE') : '';
|
|
}
|
|
|
|
export default function Statistik() {
|
|
const [year, setYear] = useState(new Date().getFullYear());
|
|
const [data, setData] = useState<StatsData | null>(null);
|
|
const [fetchError, setFetchError] = useState<number | null>(null);
|
|
|
|
const error = fetchError === year ? 'Fehler beim Laden der Statistik.' : '';
|
|
const loading = !error && (!data || data.year !== year);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
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); });
|
|
return () => { cancelled = true; };
|
|
}, [year]);
|
|
|
|
if (loading) return <div className="text-gray-500 text-sm py-4">Lade Statistik...</div>;
|
|
if (error) return <div className="text-red-600 text-sm py-4">{error}</div>;
|
|
|
|
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 (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-3 print:hidden">
|
|
<label className="text-sm font-medium text-gray-700">Jahr</label>
|
|
<input
|
|
type="number"
|
|
value={year}
|
|
onChange={(e) => setYear(parseInt(e.target.value, 10) || new Date().getFullYear())}
|
|
min={2000}
|
|
max={2100}
|
|
className="w-24 px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-sm focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse text-sm">
|
|
<thead>
|
|
<tr>
|
|
<th className={thTop} rowSpan={2}>Monat</th>
|
|
<th className={thTop} colSpan={6}>Besucher</th>
|
|
<th className={`${thTop} border-l-4 border-l-gray-400`} colSpan={7}>Anzahl</th>
|
|
</tr>
|
|
<tr>
|
|
<th className={thSub}>RF</th>
|
|
<th className={thSub}>SF</th>
|
|
<th className={thSub}>SonF</th>
|
|
<th className={thSub}>PrF</th>
|
|
<th className={thSub}>ToT</th>
|
|
<th className={thSub}>Gesamt</th>
|
|
<th className={thDiv}>Führ.</th>
|
|
<th className={thSub}>Beob.</th>
|
|
<th className={thSub}>TD</th>
|
|
<th className={thSub}>Sonst.</th>
|
|
<th className={thSub}>BEOS</th>
|
|
<th className={thSub}>ToT</th>
|
|
<th className={thSub}>Gesamt</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{MONATE.map((name, 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 (
|
|
<tr key={name} className={cls}>
|
|
<td className={tdL}>{name}</td>
|
|
<td className={td}>{r ? n(r.besucherRF) : ''}</td>
|
|
<td className={td}>{r ? n(r.besucherSF) : ''}</td>
|
|
<td className={td}>{r ? n(r.besucherSonF) : ''}</td>
|
|
<td className={td}>{r ? n(r.besucherPrF) : ''}</td>
|
|
<td className={td}>{r ? n(r.besucherToT) : ''}</td>
|
|
<td className={`${td} font-semibold`}>{r ? n(r.besucherGesamt) : ''}</td>
|
|
<td className={`${tdDiv} font-semibold`}>{r ? n(r.tageFuehrungen) : ''}</td>
|
|
<td className={td}>{r ? n(r.tageBeob) : ''}</td>
|
|
<td className={td}>{r ? n(r.tageTD) : ''}</td>
|
|
<td className={td}>{r ? n(r.tageSonst) : ''}</td>
|
|
<td className={td}>{r ? n(r.tageBEOS) : ''}</td>
|
|
<td className={td}>{r ? n(r.tagesToT) : ''}</td>
|
|
<td className={`${td} font-semibold`}>{r ? n(r.tageGesamt) : ''}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
<tr>
|
|
<td className={tdL + ' font-semibold'}>Summe</td>
|
|
<td className={tdSum}>{n(col('besucherRF'))}</td>
|
|
<td className={tdSum}>{n(col('besucherSF'))}</td>
|
|
<td className={tdSum}>{n(col('besucherSonF'))}</td>
|
|
<td className={tdSum}>{n(col('besucherPrF'))}</td>
|
|
<td className={tdSum}>{n(col('besucherToT'))}</td>
|
|
<td className={tdSum}>{n(col('besucherGesamt'))}</td>
|
|
<td className={tdSumDiv}>{n(col('tageFuehrungen'))}</td>
|
|
<td className={tdSum}>{n(col('tageBeob'))}</td>
|
|
<td className={tdSum}>{n(col('tageTD'))}</td>
|
|
<td className={tdSum}>{n(col('tageSonst'))}</td>
|
|
<td className={tdSum}>{n(col('tageBEOS'))}</td>
|
|
<td className={tdSum}>{n(col('tagesToT'))}</td>
|
|
<td className={tdSum}>{n(col('tageGesamt'))}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 w-full max-w-sm">
|
|
<div className="border-2 border-gray-300 rounded-xl p-4 bg-white">
|
|
<div className="text-xs text-gray-500 mb-1">Besucher {year}</div>
|
|
<div className="text-2xl font-bold">{data?.cumulative.toLocaleString('de-DE') ?? 0}</div>
|
|
</div>
|
|
<div className="border-2 border-gray-300 rounded-xl p-4 bg-white">
|
|
<div className="text-xs text-gray-500 mb-1">Führungen {year}</div>
|
|
<div className="text-2xl font-bold">{data?.tage ?? 0}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|