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:
+2
-2
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user