diff --git a/ANLEITUNG.md b/ANLEITUNG.md index d021e46..1902fcb 100644 --- a/ANLEITUNG.md +++ b/ANLEITUNG.md @@ -78,6 +78,16 @@ Unterhalb der Kuppelauswahl gibt es drei Tabs: **Name / Gruppe** – erscheint nur bei Sonderführung; Name der Gruppe oder Person. +**Spende** – erscheint nur bei Sonderführung, rechts neben Name / Gruppe. Auswahl zwischen `bar`, `überw.`, `kasse` und `keine`; die Angabe ist bei Sonderführungen verpflichtend. Bei `bar` erscheint zusätzlich ein Feld **Betrag (€)**, in das der erhaltene Betrag eingetragen werden muss (Komma oder Punkt als Dezimaltrennzeichen, z. B. `25,50`). + +> **Sonderführungen werden automatisch nachgetragen.** Beim Speichern eines Eintrags der Art +> Sonderführung wird die passende Führung im Sonderführungs-Register über das Datum gesucht und +> dort als *stattgefunden* markiert; Besucherzahl, Spende und Bemerkungen werden mit übernommen. +> Ein Nachtragen im Beo-Formular entfällt damit. Gibt es an dem Tag mehrere Führungen, wird die +> zeitlich nächstgelegene genommen. Wird gar keine gefunden, erscheint über dem Formular eine +> gelbe Meldung — der Logbuch-Eintrag ist trotzdem gespeichert, nur das Register muss dann von +> Hand gepflegt werden. + **BEOs** – beteiligte Beobachter. Der eigene Name ist automatisch vorausgewählt. Weitere BEOs können über das Suchfeld hinzugefügt werden; ein Klick auf × entfernt sie wieder. **Beobachtete Objekte** – nicht sichtbar bei BEOS und TD; bei Sonnenführungen fest auf „Sonne" gesetzt. Für alle anderen Arten: diff --git a/CLAUDE.md b/CLAUDE.md index a7c7060..2ccca8b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,9 @@ Next.js 16 App Router application. All pages are server components; interactive ## Data model -`ArtFuehrung` is stored as abbreviations in the DB (`RF`, `SF`, `PrF`, `BEOS`, `SonF`, `TD`, `Beob`, `ToT`, `Sonst`). Display names are in `ARTEN_MAP` in `types/logbuch.ts`. `BEOS` and `TD` hide the Besucher and Objekte fields. `SonF` pre-selects "Sonne" as the only object. +`ArtFuehrung` is stored as abbreviations in the DB (`RF`, `SF`, `PrF`, `BEOS`, `SonF`, `TD`, `Beob`, `ToT`, `Sonst`). Display names are in `ARTEN_MAP` in `types/logbuch.ts`. `BEOS` and `TD` hide the Besucher and Objekte fields. `SonF` pre-selects "Sonne" as the only object. `SF` (Sonderführung) additionally shows `SonderName` plus the mandatory `Spende` select (`bar`, `ueberw`, `kasse`, `keine` — labels in `SPENDE_MAP`); `SpendeBetrag` is only filled for `bar`. Both columns stay `NULL` for every other Art, enforced client-side in `LogbuchForm` and server-side in `spendeValues()` in `DB4js_all.php`. + +Saving an `SF` entry also writes back into the Sonderführungs register `SoFue2` (`stattgefunden`, `anzahl_echt`, `bezahlt`, `remarks`) via `RepoSoFue::ausLogbuch()` in `DB4js_all.php` — matched by `DATE(wtermin)` against the entry's Beginn, nearest time wins. It runs in the dispatcher **after** the logbuch transaction commits and never throws; the outcome travels back as a `sofue` field in the API response and is shown above the form by `MainClient`. Note `SoFue2` is latin1 while `logbuch` is utf8mb4, and `remarks` holds only 100 chars — `cp1252Safe()` handles both. ## Deployment diff --git a/app/MainClient.tsx b/app/MainClient.tsx index 84cf116..c5d91e5 100644 --- a/app/MainClient.tsx +++ b/app/MainClient.tsx @@ -2,13 +2,31 @@ import { useState } from 'react'; import { KUPPELN } from '@/types/logbuch'; -import type { Kuppel, LogbuchEintrag } from '@/types/logbuch'; +import type { Kuppel, LogbuchEintrag, SofueStatus } from '@/types/logbuch'; import LogbuchForm from '@/components/LogbuchForm'; import LogbuchList from '@/components/LogbuchList'; import Statistik from '@/components/Statistik'; import Fahrkosten from '@/components/Fahrkosten'; import packageJson from '@/package.json'; +function datumDe(iso?: string): string { + if (!iso) return ''; + const [j, m, t] = iso.slice(0, 10).split('-'); + return t && m && j ? `${t}.${m}.${j}` : iso; +} + +/** Rückmeldung zur Rückschreibung einer Sonderführung ins Register SoFue2. */ +function sofueMeldung(s: SofueStatus): string { + switch (s.status) { + case 'ok': + return `Sonderführung vom ${datumDe(s.wtermin ?? s.datum)} im Register als stattgefunden eingetragen.`; + case 'kein_termin': + return `Keine Sonderführung am ${datumDe(s.datum)} gefunden — das Sonderführungs-Register wurde nicht aktualisiert.`; + default: + return 'Das Sonderführungs-Register konnte nicht aktualisiert werden. Bitte den Eintrag dort von Hand nachtragen.'; + } +} + interface Props { kuerzel: string; beoId: number; @@ -22,6 +40,7 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) { const [refreshKey, setRefreshKey] = useState(0); const [editEntry, setEditEntry] = useState(null); const [backupState, setBackupState] = useState<'idle' | 'running' | 'ok' | 'error'>('idle'); + const [sofueHinweis, setSofueHinweis] = useState(null); const grafikSrc = '/api/statistik/grafik'; @@ -32,9 +51,10 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) { const currentUserBeo = { ID: beoId, Kuerzel: kuerzel, Name: beoName }; - function handleSaved() { + function handleSaved(sofue?: SofueStatus | null) { setRefreshKey((k) => k + 1); setEditEntry(null); + setSofueHinweis(sofue ?? null); } function handleEdit(entry: LogbuchEintrag) { @@ -148,6 +168,26 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) { Eintrag bearbeiten (ID {editEntry.ID}) )} + {sofueHinweis && sofueHinweis.status !== 'uebersprungen' && ( +
+ {sofueMeldung(sofueHinweis)} + +
+ )} void; + /** sofue: Ergebnis der Rückschreibung ins Sonderführungs-Register, sofern erfolgt. */ + onSaved: (sofue?: SofueStatus | null) => void; } function toLocalDatetimeValue(isoOrDatetime: string): string { @@ -46,6 +47,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved const [objekte, setObjekte] = useState([]); const [bemerkungen, setBemerkungen] = useState(''); const [sonderName, setSonderName] = useState(''); + const [spende, setSpende] = useState(''); + const [spendeBetragRaw, setSpendeBetragRaw] = useState(''); const [wetter, setWetter] = useState({ temp: 0, feuchte: 0, druck: 0 }); const [tempRaw, setTempRaw] = useState('0'); const [saving, setSaving] = useState(false); @@ -55,12 +58,13 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved const editEntryRef = useRef(editEntry); editEntryRef.current = editEntry; const formRef = useRef(null); + const prevArtRef = useRef(null); function focusNext(current: HTMLElement) { if (!formRef.current) return; const fields = Array.from( formRef.current.querySelectorAll( - 'input:not([disabled]), textarea:not([disabled]), #art-select' + 'input:not([disabled]), textarea:not([disabled]), #art-select, #spende-select' ) ); const idx = fields.indexOf(current); @@ -91,6 +95,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved setBesucher(editEntry.Besucher ?? ''); setBemerkungen(editEntry.Bemerkungen ?? ''); setSonderName(editEntry.SonderName ?? ''); + setSpende(editEntry.Spende ?? ''); + setSpendeBetragRaw(editEntry.SpendeBetrag != null ? String(editEntry.SpendeBetrag) : ''); if (editEntry.WetterTemp !== null) { const t = editEntry.WetterTemp ?? 0; setWetter({ temp: t, feuchte: Math.round(editEntry.WetterFeuchte ?? 0), druck: Math.round(editEntry.WetterDruck ?? 0) }); @@ -105,6 +111,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved setObjekte([]); setBemerkungen(''); setSonderName(''); + setSpende(''); + setSpendeBetragRaw(''); setBesucher(''); } }, [editEntry, currentUserBeo]); @@ -137,12 +145,20 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved } }, [editEntry]); - // Objekte und Besucher zurücksetzen beim Wechsel der Art der Führung + // Objekte, Besucher und Spende zurücksetzen beim Wechsel der Art der Führung useEffect(() => { + const vorherigeArt = prevArtRef.current; + prevArtRef.current = artFuehrung; setObjekte([]); if (NO_OBJEKTE_ARTEN.includes(artFuehrung)) { setBesucher(''); } + // Nicht beim ersten Lauf leeren — sonst überschreibt der Start-Wert 'RF' + // die aus editEntry vorbelegte Spende. + if (vorherigeArt !== null && artFuehrung !== 'SF') { + setSpende(''); + setSpendeBetragRaw(''); + } }, [artFuehrung]); async function handleSubmit(e: React.FormEvent) { @@ -162,6 +178,15 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved setError('Start- und Endzeit sind identisch.'); return; } + const spendeBetrag = parseFloat(spendeBetragRaw.replace(',', '.')); + if (artFuehrung === 'SF' && spende === '') { + setError('Bitte Spende auswählen.'); + return; + } + if (artFuehrung === 'SF' && spende === 'bar' && !(spendeBetrag > 0)) { + setError('Bitte Spendenbetrag eingeben.'); + return; + } setSaving(true); @@ -169,6 +194,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved Kuppel: kuppel, ArtFuehrung: artFuehrung, SonderName: artFuehrung === 'SF' ? sonderName : null, + Spende: artFuehrung === 'SF' && spende !== '' ? spende : null, + SpendeBetrag: artFuehrung === 'SF' && spende === 'bar' ? spendeBetrag : null, Beginn: beginn, Ende: ende, Besucher: besucher === '' ? 0 : besucher, @@ -188,6 +215,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved body: JSON.stringify(body), }); if (!res.ok) throw new Error(await res.text()); + const daten: { sofue?: SofueStatus } = await res.json().catch(() => ({})); setSuccess(true); setTimeout(() => setSuccess(false), 5000); if (!editEntry) { @@ -197,8 +225,13 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved setBeos([currentUserBeo]); setObjekte([]); setBemerkungen(''); + setSonderName(''); + setSpende(''); + setSpendeBetragRaw(''); } - onSaved(); + // Die Meldung zur Sonderführung gehört nach oben: beim Bearbeiten wird dieses + // Formular durch onSaved() neu gemountet und verlöre eigenen State sofort. + onSaved(daten.sofue ?? null); } catch { setError('Fehler beim Speichern. Bitte erneut versuchen.'); } finally { @@ -280,18 +313,41 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved )} - {/* Name/Gruppe bei Sonderführung */} + {/* Name/Gruppe und Spende bei Sonderführung */} {artFuehrung === 'SF' && ( -
- - setSonderName(e.target.value.slice(0, 200))} - maxLength={200} - placeholder="Name oder Gruppe der Sonderführung" - className="w-full px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none" - /> +
+
+ + setSonderName(e.target.value.slice(0, 200))} + maxLength={200} + placeholder="Name oder Gruppe der Sonderführung" + className="w-full px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none" + /> +
+
+ + ({ value: s, label: spendeLabel(s) }))} + onChange={(v) => setSpende(v as SpendeArt)} + /> +
+ {spende === 'bar' && ( +
+ + setSpendeBetragRaw(e.target.value)} + className="w-24 px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none" + /> +
+ )}
)} @@ -388,7 +444,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved {editEntry && (