feat: Anleitung-Button im Footer + Statistik-Effect-Fix

- Footer: 3-Spalten-Layout mit 'Anleitung'-Button in der Mitte (Link auf /anleitung.html)
- anleitung.html: 'Zurück zum Logbuch'-Button oben (beim Drucken ausgeblendet)
- Statistik: synchrones setState im Effect durch abgeleiteten loading/error-State ersetzt; Fetch-Abbruch mit cancelled-Flag; as any durch ArtFuehrung-Cast ersetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 16:46:38 +02:00
parent 42a2651f8e
commit d56ebb229d
4 changed files with 43 additions and 11 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
### Zugriff zur Datenbank: ### Zugriff zur Datenbank:
Die Remote-Datenbank auf logbuch.fuerst-.stuttgart.de wird zum lokalen entwickeln über einen SSH-Tunner erreicht: Die Remote-Datenbank auf logbuch.fuerst-.stuttgart.de wird zum lokalen entwickeln über einen SSH-Tunner erreicht:
ssh -L 3336:localhost:3336 rxf@logbuch.fuerst-stuttgart.de -N
~~~ ~~~
ssh -L 3336:localhost:3336 rxf@logbuch.fuerst-stuttgart.de -N ssh -L 3336:localhost:3336 rxf@logbuch.fuerst-stuttgart.de -N
~~~ ~~~
Dieser ist vor dem Starten des Programme einmal einzurichten!! Dieser ist vor dem Starten des Programme einmal einzurichten!!
+9 -1
View File
@@ -179,12 +179,20 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
</div> </div>
)} )}
<footer className="mt-6 flex justify-between items-center text-xs sm:text-sm text-gray-600 px-1 sm:px-4 print:hidden"> <footer className="mt-6 grid grid-cols-3 items-center text-xs sm:text-sm text-gray-600 px-1 sm:px-4 print:hidden">
<div> <div>
<a href="mailto:rxf@gmx.de" className="text-blue-600 hover:underline"> <a href="mailto:rxf@gmx.de" className="text-blue-600 hover:underline">
rxf@gmx.de rxf@gmx.de
</a> </a>
</div> </div>
<div className="flex justify-center">
<a
href="/anleitung.html"
className="px-3 py-1.5 bg-gray-200 hover:bg-gray-300 rounded-lg text-gray-700"
>
Anleitung
</a>
</div>
<div className="text-right"> <div className="text-right">
v{version} {buildDate} v{version} {buildDate}
</div> </div>
+12 -8
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import type { Kuppel } from '@/types/logbuch'; import type { ArtFuehrung, Kuppel } from '@/types/logbuch';
import { artLabel } from '@/types/logbuch'; import { artLabel } from '@/types/logbuch';
interface MonthlyRow { interface MonthlyRow {
@@ -33,16 +33,20 @@ interface Props {
export default function Statistik({ kuppel }: Props) { export default function Statistik({ kuppel }: Props) {
const [year, setYear] = useState(new Date().getFullYear()); const [year, setYear] = useState(new Date().getFullYear());
const [data, setData] = useState<StatsData | null>(null); const [data, setData] = useState<StatsData | null>(null);
const [loading, setLoading] = useState(true); const [fetchError, setFetchError] = useState<{ year: number; kuppel: Kuppel } | null>(null);
const [error, setError] = useState('');
const error = fetchError?.year === year && fetchError?.kuppel === kuppel
? 'Fehler beim Laden der Statistik.'
: '';
const loading = !error && (!data || data.year !== year || data.kuppel !== kuppel);
useEffect(() => { useEffect(() => {
setLoading(true); let cancelled = false;
setError('');
fetch(`/api/statistik?kuppel=${encodeURIComponent(kuppel)}&year=${year}`) fetch(`/api/statistik?kuppel=${encodeURIComponent(kuppel)}&year=${year}`)
.then((r) => { if (!r.ok) throw new Error(); return r.json(); }) .then((r) => { if (!r.ok) throw new Error(); return r.json(); })
.then((d: StatsData) => { setData(d); setLoading(false); }) .then((d: StatsData) => { if (!cancelled) { setData(d); setFetchError(null); } })
.catch(() => { setError('Fehler beim Laden der Statistik.'); setLoading(false); }); .catch(() => { if (!cancelled) setFetchError({ year, kuppel }); });
return () => { cancelled = true; };
}, [kuppel, year]); }, [kuppel, year]);
const { arten, matrix, monatTotal, artTotal, grandTotal, anzahlTotal } = useMemo(() => { const { arten, matrix, monatTotal, artTotal, grandTotal, anzahlTotal } = useMemo(() => {
@@ -111,7 +115,7 @@ export default function Statistik({ kuppel }: Props) {
<th className={headCls}>Monat</th> <th className={headCls}>Monat</th>
<th className={headCls}>Führungen</th> <th className={headCls}>Führungen</th>
{arten.map((art) => ( {arten.map((art) => (
<th key={art} className={headCls}>{artLabel(art as any) || art}</th> <th key={art} className={headCls}>{artLabel(art as ArtFuehrung)}</th>
))} ))}
<th className={headCls}>Gesamt</th> <th className={headCls}>Gesamt</th>
</tr> </tr>
+20
View File
@@ -202,6 +202,23 @@
section { padding: 1.2rem 1rem; } section { padding: 1.2rem 1rem; }
} }
.back-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-bottom: 1.5rem;
padding: 0.5rem 1rem;
background: #fff;
border: 1px solid #d8e0f0;
border-radius: 8px;
color: #2e4e8a;
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
cursor: pointer;
}
.back-btn:hover { background: #eef2fa; }
@media print { @media print {
body { background: #fff; font-size: 12pt; } body { background: #fff; font-size: 12pt; }
.page { max-width: none; padding: 0; } .page { max-width: none; padding: 0; }
@@ -209,12 +226,15 @@
section { border: 1px solid #ccc; break-inside: avoid; } section { border: 1px solid #ccc; break-inside: avoid; }
nav.toc { break-after: page; } nav.toc { break-after: page; }
a { color: inherit; text-decoration: none; } a { color: inherit; text-decoration: none; }
.back-btn { display: none; }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<a href="/" class="back-btn">← Zurück zum Logbuch</a>
<header> <header>
<div class="star"></div> <div class="star"></div>
<div> <div>