diff --git a/ANLEITUNG.md b/ANLEITUNG.md index d021e46..f6c451f 100644 --- a/ANLEITUNG.md +++ b/ANLEITUNG.md @@ -78,6 +78,8 @@ 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`). + **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..825f374 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ 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`. ## Deployment diff --git a/app/api/logbuch/[id]/route.ts b/app/api/logbuch/[id]/route.ts index 1d8842c..87376ca 100644 --- a/app/api/logbuch/[id]/route.ts +++ b/app/api/logbuch/[id]/route.ts @@ -12,10 +12,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ try { const body = await request.json(); - const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body; + const { Kuppel, ArtFuehrung, SonderName, Spende, SpendeBetrag, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body; await phpdb.updateLogbuch(logbuchId, session.beoId, session.role ?? '', { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, + Spende: Spende ?? null, + SpendeBetrag: SpendeBetrag ?? null, Besucher: Besucher ?? 0, beoIds: beoIds ?? [], objekte: objekte ?? [], diff --git a/app/api/logbuch/route.ts b/app/api/logbuch/route.ts index b11ace6..79e67e5 100644 --- a/app/api/logbuch/route.ts +++ b/app/api/logbuch/route.ts @@ -30,10 +30,12 @@ export async function POST(request: NextRequest) { try { const body = await request.json(); - const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body; + const { Kuppel, ArtFuehrung, SonderName, Spende, SpendeBetrag, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body; const result = await phpdb.createLogbuch({ Kuppel, ArtFuehrung, SonderName, Beginn, Ende, + Spende: Spende ?? null, + SpendeBetrag: SpendeBetrag ?? null, Besucher: Besucher ?? 0, beoIds: beoIds ?? [], objekte: objekte ?? [], diff --git a/components/LogbuchForm.tsx b/components/LogbuchForm.tsx index 1ba8d4e..a7867fb 100644 --- a/components/LogbuchForm.tsx +++ b/components/LogbuchForm.tsx @@ -1,8 +1,8 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; -import type { Kuppel, ArtFuehrung, BeoOption, SelectedObjekt, Wetter, LogbuchEintrag } from '@/types/logbuch'; -import { ARTEN, ARTEN_MAP, artLabel } from '@/types/logbuch'; +import type { Kuppel, ArtFuehrung, SpendeArt, BeoOption, SelectedObjekt, Wetter, LogbuchEintrag } from '@/types/logbuch'; +import { ARTEN, ARTEN_MAP, artLabel, SPENDE_ARTEN, spendeLabel } from '@/types/logbuch'; import BeoSelector from './BeoSelector'; import ObjektSelector from './ObjektSelector'; import CustomSelect from './CustomSelect'; @@ -46,6 +46,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 +57,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 +94,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 +110,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved setObjekte([]); setBemerkungen(''); setSonderName(''); + setSpende(''); + setSpendeBetragRaw(''); setBesucher(''); } }, [editEntry, currentUserBeo]); @@ -137,12 +144,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 +177,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 +193,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, @@ -197,6 +223,9 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved setBeos([currentUserBeo]); setObjekte([]); setBemerkungen(''); + setSonderName(''); + setSpende(''); + setSpendeBetragRaw(''); } onSaved(); } catch { @@ -280,18 +309,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" + /> +
+ )}
)} diff --git a/components/LogbuchList.tsx b/components/LogbuchList.tsx index a6bb8d3..e1cb714 100644 --- a/components/LogbuchList.tsx +++ b/components/LogbuchList.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import type { Kuppel, LogbuchEintrag } from '@/types/logbuch'; +import { spendeLabel } from '@/types/logbuch'; interface Props { kuppel: Kuppel; @@ -255,6 +256,13 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, currentUserKue
{e.ArtFuehrung}
{e.SonderName &&
{e.SonderName}
} + {e.Spende && ( +
+ Spende: {spendeLabel(e.Spende)} + {e.Spende === 'bar' && e.SpendeBetrag != null && + ' ' + Number(e.SpendeBetrag).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })} +
+ )} {e.Besucher || ''} diff --git a/lib/phpdb.ts b/lib/phpdb.ts index 8616c69..7be529d 100644 --- a/lib/phpdb.ts +++ b/lib/phpdb.ts @@ -85,6 +85,8 @@ export interface CreateLogbuchData { Kuppel: string; ArtFuehrung: string; SonderName?: string | null; + Spende?: string | null; + SpendeBetrag?: number | null; Beginn: string; Ende: string; Besucher?: number; diff --git a/migrate_spende.sql b/migrate_spende.sql new file mode 100644 index 0000000..a10abec --- /dev/null +++ b/migrate_spende.sql @@ -0,0 +1,7 @@ +-- Migration: Spende-Feld für Sonderführungen (Version 1.12.0) +-- Spende wird nur bei ArtFuehrung = 'SF' gesetzt, sonst NULL. +-- SpendeBetrag ist nur bei Spende = 'bar' gefüllt. +-- Hinweis: In der DB steht der ASCII-Schlüssel 'ueberw', angezeigt wird "überw.". +ALTER TABLE logbuch + ADD COLUMN Spende ENUM('bar','ueberw','kasse','keine') DEFAULT NULL, + ADD COLUMN SpendeBetrag DECIMAL(8,2) DEFAULT NULL; diff --git a/package.json b/package.json index 3147f38..ffc18e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "logbuch", - "version": "1.11.0", + "version": "1.12.0", "private": true, "scripts": { "dev": "next dev", diff --git a/public/anleitung.html b/public/anleitung.html index 912902f..cfb09a0 100644 --- a/public/anleitung.html +++ b/public/anleitung.html @@ -240,6 +240,8 @@

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).

+

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/types/logbuch.ts b/types/logbuch.ts index b2bbe08..57795d8 100644 --- a/types/logbuch.ts +++ b/types/logbuch.ts @@ -21,6 +21,21 @@ export function artLabel(a: ArtFuehrung): string { return ARTEN_MAP[a] || a; } +export type SpendeArt = 'bar' | 'ueberw' | 'kasse' | 'keine'; + +export const SPENDE_MAP: Record = { + bar: 'bar', + ueberw: 'überw.', + kasse: 'kasse', + keine: 'keine', +}; + +export const SPENDE_ARTEN = Object.keys(SPENDE_MAP) as SpendeArt[]; + +export function spendeLabel(s: SpendeArt): string { + return SPENDE_MAP[s] || s; +} + export interface BeoOption { ID: number; Kuerzel: string; @@ -48,6 +63,8 @@ export interface LogbuchEintrag { Kuppel: Kuppel; ArtFuehrung: ArtFuehrung; SonderName: string | null; + Spende: SpendeArt | null; + SpendeBetrag: number | null; Beginn: string; Ende: string; Besucher: number;