chore: feat/spende → main (Version 1.13.0)
This commit is contained in:
@@ -78,6 +78,16 @@ Unterhalb der Kuppelauswahl gibt es drei Tabs:
|
|||||||
|
|
||||||
**Name / Gruppe** – erscheint nur bei Sonderführung; Name der Gruppe oder Person.
|
**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.
|
**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:
|
**Beobachtete Objekte** – nicht sichtbar bei BEOS und TD; bei Sonnenführungen fest auf „Sonne" gesetzt. Für alle anderen Arten:
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ Next.js 16 App Router application. All pages are server components; interactive
|
|||||||
|
|
||||||
## Data model
|
## 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
|
## Deployment
|
||||||
|
|
||||||
|
|||||||
+42
-2
@@ -2,13 +2,31 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { KUPPELN } from '@/types/logbuch';
|
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 LogbuchForm from '@/components/LogbuchForm';
|
||||||
import LogbuchList from '@/components/LogbuchList';
|
import LogbuchList from '@/components/LogbuchList';
|
||||||
import Statistik from '@/components/Statistik';
|
import Statistik from '@/components/Statistik';
|
||||||
import Fahrkosten from '@/components/Fahrkosten';
|
import Fahrkosten from '@/components/Fahrkosten';
|
||||||
import packageJson from '@/package.json';
|
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 {
|
interface Props {
|
||||||
kuerzel: string;
|
kuerzel: string;
|
||||||
beoId: number;
|
beoId: number;
|
||||||
@@ -22,6 +40,7 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
|
|||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const [editEntry, setEditEntry] = useState<LogbuchEintrag | null>(null);
|
const [editEntry, setEditEntry] = useState<LogbuchEintrag | null>(null);
|
||||||
const [backupState, setBackupState] = useState<'idle' | 'running' | 'ok' | 'error'>('idle');
|
const [backupState, setBackupState] = useState<'idle' | 'running' | 'ok' | 'error'>('idle');
|
||||||
|
const [sofueHinweis, setSofueHinweis] = useState<SofueStatus | null>(null);
|
||||||
|
|
||||||
const grafikSrc = '/api/statistik/grafik';
|
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 };
|
const currentUserBeo = { ID: beoId, Kuerzel: kuerzel, Name: beoName };
|
||||||
|
|
||||||
function handleSaved() {
|
function handleSaved(sofue?: SofueStatus | null) {
|
||||||
setRefreshKey((k) => k + 1);
|
setRefreshKey((k) => k + 1);
|
||||||
setEditEntry(null);
|
setEditEntry(null);
|
||||||
|
setSofueHinweis(sofue ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(entry: LogbuchEintrag) {
|
function handleEdit(entry: LogbuchEintrag) {
|
||||||
@@ -148,6 +168,26 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
|
|||||||
Eintrag bearbeiten (ID {editEntry.ID})
|
Eintrag bearbeiten (ID {editEntry.ID})
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{sofueHinweis && sofueHinweis.status !== 'uebersprungen' && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mb-3 text-sm rounded-lg px-3 py-2 border flex justify-between items-start gap-3 ' +
|
||||||
|
(sofueHinweis.status === 'ok'
|
||||||
|
? 'text-green-700 bg-green-50 border-green-300'
|
||||||
|
: 'text-amber-800 bg-amber-50 border-amber-400')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{sofueMeldung(sofueHinweis)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSofueHinweis(null)}
|
||||||
|
className="shrink-0 font-medium hover:opacity-70"
|
||||||
|
aria-label="Meldung schließen"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<LogbuchForm
|
<LogbuchForm
|
||||||
key={`${activeKuppel}-${editEntry?.ID ?? 'new'}`}
|
key={`${activeKuppel}-${editEntry?.ID ?? 'new'}`}
|
||||||
kuppel={activeKuppel}
|
kuppel={activeKuppel}
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
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 ?? '', {
|
const result = await phpdb.updateLogbuch(logbuchId, session.beoId, session.role ?? '', {
|
||||||
Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
|
Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
|
||||||
|
Spende: Spende ?? null,
|
||||||
|
SpendeBetrag: SpendeBetrag ?? null,
|
||||||
Besucher: Besucher ?? 0,
|
Besucher: Besucher ?? 0,
|
||||||
beoIds: beoIds ?? [],
|
beoIds: beoIds ?? [],
|
||||||
objekte: objekte ?? [],
|
objekte: objekte ?? [],
|
||||||
@@ -24,7 +26,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
triggerBackup();
|
triggerBackup();
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true, sofue: result?.sofue });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
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({
|
const result = await phpdb.createLogbuch({
|
||||||
Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
|
Kuppel, ArtFuehrung, SonderName, Beginn, Ende,
|
||||||
|
Spende: Spende ?? null,
|
||||||
|
SpendeBetrag: SpendeBetrag ?? null,
|
||||||
Besucher: Besucher ?? 0,
|
Besucher: Besucher ?? 0,
|
||||||
beoIds: beoIds ?? [],
|
beoIds: beoIds ?? [],
|
||||||
objekte: objekte ?? [],
|
objekte: objekte ?? [],
|
||||||
@@ -43,6 +45,7 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
triggerBackup();
|
triggerBackup();
|
||||||
|
// result enthält id und — bei Sonderführungen — den sofue-Status der Rückschreibung
|
||||||
return NextResponse.json(result, { status: 201 });
|
return NextResponse.json(result, { status: 201 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('POST /api/logbuch:', error);
|
console.error('POST /api/logbuch:', error);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import type { Kuppel, ArtFuehrung, BeoOption, SelectedObjekt, Wetter, LogbuchEintrag } from '@/types/logbuch';
|
import type { Kuppel, ArtFuehrung, SpendeArt, BeoOption, SelectedObjekt, SofueStatus, Wetter, LogbuchEintrag } from '@/types/logbuch';
|
||||||
import { ARTEN, ARTEN_MAP, artLabel } from '@/types/logbuch';
|
import { ARTEN, ARTEN_MAP, artLabel, SPENDE_ARTEN, spendeLabel } from '@/types/logbuch';
|
||||||
import BeoSelector from './BeoSelector';
|
import BeoSelector from './BeoSelector';
|
||||||
import ObjektSelector from './ObjektSelector';
|
import ObjektSelector from './ObjektSelector';
|
||||||
import CustomSelect from './CustomSelect';
|
import CustomSelect from './CustomSelect';
|
||||||
@@ -12,7 +12,8 @@ interface Props {
|
|||||||
kuppel: Kuppel;
|
kuppel: Kuppel;
|
||||||
currentUserBeo: BeoOption;
|
currentUserBeo: BeoOption;
|
||||||
editEntry?: LogbuchEintrag | null;
|
editEntry?: LogbuchEintrag | null;
|
||||||
onSaved: () => void;
|
/** sofue: Ergebnis der Rückschreibung ins Sonderführungs-Register, sofern erfolgt. */
|
||||||
|
onSaved: (sofue?: SofueStatus | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toLocalDatetimeValue(isoOrDatetime: string): string {
|
function toLocalDatetimeValue(isoOrDatetime: string): string {
|
||||||
@@ -46,6 +47,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
const [objekte, setObjekte] = useState<SelectedObjekt[]>([]);
|
const [objekte, setObjekte] = useState<SelectedObjekt[]>([]);
|
||||||
const [bemerkungen, setBemerkungen] = useState('');
|
const [bemerkungen, setBemerkungen] = useState('');
|
||||||
const [sonderName, setSonderName] = useState('');
|
const [sonderName, setSonderName] = useState('');
|
||||||
|
const [spende, setSpende] = useState<SpendeArt | ''>('');
|
||||||
|
const [spendeBetragRaw, setSpendeBetragRaw] = useState('');
|
||||||
const [wetter, setWetter] = useState<Wetter>({ temp: 0, feuchte: 0, druck: 0 });
|
const [wetter, setWetter] = useState<Wetter>({ temp: 0, feuchte: 0, druck: 0 });
|
||||||
const [tempRaw, setTempRaw] = useState('0');
|
const [tempRaw, setTempRaw] = useState('0');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -55,12 +58,13 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
const editEntryRef = useRef(editEntry);
|
const editEntryRef = useRef(editEntry);
|
||||||
editEntryRef.current = editEntry;
|
editEntryRef.current = editEntry;
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
const prevArtRef = useRef<ArtFuehrung | null>(null);
|
||||||
|
|
||||||
function focusNext(current: HTMLElement) {
|
function focusNext(current: HTMLElement) {
|
||||||
if (!formRef.current) return;
|
if (!formRef.current) return;
|
||||||
const fields = Array.from(
|
const fields = Array.from(
|
||||||
formRef.current.querySelectorAll<HTMLElement>(
|
formRef.current.querySelectorAll<HTMLElement>(
|
||||||
'input:not([disabled]), textarea:not([disabled]), #art-select'
|
'input:not([disabled]), textarea:not([disabled]), #art-select, #spende-select'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const idx = fields.indexOf(current);
|
const idx = fields.indexOf(current);
|
||||||
@@ -91,6 +95,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
setBesucher(editEntry.Besucher ?? '');
|
setBesucher(editEntry.Besucher ?? '');
|
||||||
setBemerkungen(editEntry.Bemerkungen ?? '');
|
setBemerkungen(editEntry.Bemerkungen ?? '');
|
||||||
setSonderName(editEntry.SonderName ?? '');
|
setSonderName(editEntry.SonderName ?? '');
|
||||||
|
setSpende(editEntry.Spende ?? '');
|
||||||
|
setSpendeBetragRaw(editEntry.SpendeBetrag != null ? String(editEntry.SpendeBetrag) : '');
|
||||||
if (editEntry.WetterTemp !== null) {
|
if (editEntry.WetterTemp !== null) {
|
||||||
const t = editEntry.WetterTemp ?? 0;
|
const t = editEntry.WetterTemp ?? 0;
|
||||||
setWetter({ temp: t, feuchte: Math.round(editEntry.WetterFeuchte ?? 0), druck: Math.round(editEntry.WetterDruck ?? 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([]);
|
setObjekte([]);
|
||||||
setBemerkungen('');
|
setBemerkungen('');
|
||||||
setSonderName('');
|
setSonderName('');
|
||||||
|
setSpende('');
|
||||||
|
setSpendeBetragRaw('');
|
||||||
setBesucher('');
|
setBesucher('');
|
||||||
}
|
}
|
||||||
}, [editEntry, currentUserBeo]);
|
}, [editEntry, currentUserBeo]);
|
||||||
@@ -137,12 +145,20 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
}
|
}
|
||||||
}, [editEntry]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
|
const vorherigeArt = prevArtRef.current;
|
||||||
|
prevArtRef.current = artFuehrung;
|
||||||
setObjekte([]);
|
setObjekte([]);
|
||||||
if (NO_OBJEKTE_ARTEN.includes(artFuehrung)) {
|
if (NO_OBJEKTE_ARTEN.includes(artFuehrung)) {
|
||||||
setBesucher('');
|
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]);
|
}, [artFuehrung]);
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
@@ -162,6 +178,15 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
setError('Start- und Endzeit sind identisch.');
|
setError('Start- und Endzeit sind identisch.');
|
||||||
return;
|
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);
|
setSaving(true);
|
||||||
|
|
||||||
@@ -169,6 +194,8 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
Kuppel: kuppel,
|
Kuppel: kuppel,
|
||||||
ArtFuehrung: artFuehrung,
|
ArtFuehrung: artFuehrung,
|
||||||
SonderName: artFuehrung === 'SF' ? sonderName : null,
|
SonderName: artFuehrung === 'SF' ? sonderName : null,
|
||||||
|
Spende: artFuehrung === 'SF' && spende !== '' ? spende : null,
|
||||||
|
SpendeBetrag: artFuehrung === 'SF' && spende === 'bar' ? spendeBetrag : null,
|
||||||
Beginn: beginn,
|
Beginn: beginn,
|
||||||
Ende: ende,
|
Ende: ende,
|
||||||
Besucher: besucher === '' ? 0 : besucher,
|
Besucher: besucher === '' ? 0 : besucher,
|
||||||
@@ -188,6 +215,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
const daten: { sofue?: SofueStatus } = await res.json().catch(() => ({}));
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
setTimeout(() => setSuccess(false), 5000);
|
setTimeout(() => setSuccess(false), 5000);
|
||||||
if (!editEntry) {
|
if (!editEntry) {
|
||||||
@@ -197,8 +225,13 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
setBeos([currentUserBeo]);
|
setBeos([currentUserBeo]);
|
||||||
setObjekte([]);
|
setObjekte([]);
|
||||||
setBemerkungen('');
|
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 {
|
} catch {
|
||||||
setError('Fehler beim Speichern. Bitte erneut versuchen.');
|
setError('Fehler beim Speichern. Bitte erneut versuchen.');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -280,9 +313,10 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Name/Gruppe bei Sonderführung */}
|
{/* Name/Gruppe und Spende bei Sonderführung */}
|
||||||
{artFuehrung === 'SF' && (
|
{artFuehrung === 'SF' && (
|
||||||
<div>
|
<div className="flex flex-wrap gap-3 items-end">
|
||||||
|
<div className="flex-1 min-w-[160px]">
|
||||||
<label className={labelCls}>Name / Gruppe</label>
|
<label className={labelCls}>Name / Gruppe</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -293,6 +327,28 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="shrink-0 w-32">
|
||||||
|
<label className={labelCls}>Spende</label>
|
||||||
|
<CustomSelect
|
||||||
|
id="spende-select"
|
||||||
|
placeholder={spende === '' ? 'bitte wählen' : spendeLabel(spende)}
|
||||||
|
options={SPENDE_ARTEN.map((s) => ({ value: s, label: spendeLabel(s) }))}
|
||||||
|
onChange={(v) => setSpende(v as SpendeArt)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{spende === 'bar' && (
|
||||||
|
<div className="shrink-0">
|
||||||
|
<label className={labelCls}>Betrag (€)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={spendeBetragRaw}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* BEOs */}
|
{/* BEOs */}
|
||||||
@@ -388,7 +444,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
|||||||
{editEntry && (
|
{editEntry && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSaved}
|
onClick={() => onSaved()}
|
||||||
className="w-full sm:w-auto px-6 py-1 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium rounded-lg transition-colors text-sm"
|
className="w-full sm:w-auto px-6 py-1 bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium rounded-lg transition-colors text-sm"
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import type { Kuppel, LogbuchEintrag } from '@/types/logbuch';
|
import type { Kuppel, LogbuchEintrag } from '@/types/logbuch';
|
||||||
|
import { spendeLabel } from '@/types/logbuch';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
kuppel: Kuppel;
|
kuppel: Kuppel;
|
||||||
@@ -255,6 +256,13 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, currentUserKue
|
|||||||
<td className={cell}>
|
<td className={cell}>
|
||||||
<div>{e.ArtFuehrung}</div>
|
<div>{e.ArtFuehrung}</div>
|
||||||
{e.SonderName && <div className="text-xs text-gray-500">{e.SonderName}</div>}
|
{e.SonderName && <div className="text-xs text-gray-500">{e.SonderName}</div>}
|
||||||
|
{e.Spende && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Spende: {spendeLabel(e.Spende)}
|
||||||
|
{e.Spende === 'bar' && e.SpendeBetrag != null &&
|
||||||
|
' ' + Number(e.SpendeBetrag).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className={`${cell} text-center`}>{e.Besucher || ''}</td>
|
<td className={`${cell} text-center`}>{e.Besucher || ''}</td>
|
||||||
<td className={cell}>
|
<td className={cell}>
|
||||||
|
|||||||
+6
-4
@@ -1,4 +1,4 @@
|
|||||||
import type { BeoOption, LogbuchEintrag, ObjektOption, SelectedObjekt, Wetter } from '@/types/logbuch';
|
import type { BeoOption, LogbuchEintrag, ObjektOption, SelectedObjekt, SofueStatus, Wetter } from '@/types/logbuch';
|
||||||
|
|
||||||
const PHP_DB_URL = process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php';
|
const PHP_DB_URL = process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php';
|
||||||
|
|
||||||
@@ -85,6 +85,8 @@ export interface CreateLogbuchData {
|
|||||||
Kuppel: string;
|
Kuppel: string;
|
||||||
ArtFuehrung: string;
|
ArtFuehrung: string;
|
||||||
SonderName?: string | null;
|
SonderName?: string | null;
|
||||||
|
Spende?: string | null;
|
||||||
|
SpendeBetrag?: number | null;
|
||||||
Beginn: string;
|
Beginn: string;
|
||||||
Ende: string;
|
Ende: string;
|
||||||
Besucher?: number;
|
Besucher?: number;
|
||||||
@@ -95,7 +97,7 @@ export interface CreateLogbuchData {
|
|||||||
created_by: number;
|
created_by: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createLogbuch(data: CreateLogbuchData): Promise<{ id: number }> {
|
export async function createLogbuch(data: CreateLogbuchData): Promise<{ id: number; sofue?: SofueStatus }> {
|
||||||
return call('LB_CREATE_LOGBUCH', data);
|
return call('LB_CREATE_LOGBUCH', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +106,8 @@ export async function updateLogbuch(
|
|||||||
userId: number,
|
userId: number,
|
||||||
userRole: string,
|
userRole: string,
|
||||||
data: Omit<CreateLogbuchData, 'created_by'>
|
data: Omit<CreateLogbuchData, 'created_by'>
|
||||||
): Promise<void> {
|
): Promise<{ sofue?: SofueStatus }> {
|
||||||
await call('LB_UPDATE_LOGBUCH', { id, user_id: userId, user_role: userRole, ...data });
|
return call('LB_UPDATE_LOGBUCH', { id, user_id: userId, user_role: userRole, ...data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteLogbuch(
|
export async function deleteLogbuch(
|
||||||
|
|||||||
@@ -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;
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "logbuch",
|
"name": "logbuch",
|
||||||
"version": "1.11.0",
|
"version": "1.13.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -240,6 +240,10 @@
|
|||||||
|
|
||||||
<p><strong>Name / Gruppe</strong> – erscheint nur bei Sonderführung; Name der Gruppe oder Person.</p>
|
<p><strong>Name / Gruppe</strong> – erscheint nur bei Sonderführung; Name der Gruppe oder Person.</p>
|
||||||
|
|
||||||
|
<p><strong>Spende</strong> – erscheint nur bei Sonderführung, rechts neben Name / Gruppe. Auswahl zwischen <code>bar</code>, <code>überw.</code>, <code>kasse</code> und <code>keine</code>; die Angabe ist bei Sonderführungen verpflichtend. Bei <code>bar</code> erscheint zusätzlich ein Feld <strong>Betrag (€)</strong>, in das der erhaltene Betrag eingetragen werden muss (Komma oder Punkt als Dezimaltrennzeichen, z. B. <code>25,50</code>).</p>
|
||||||
|
|
||||||
|
<p><strong>Sonderführungen werden automatisch nachgetragen.</strong> Beim Speichern eines Eintrags der Art Sonderführung wird die passende Führung im Sonderführungs-Register über das Datum gesucht und dort als <em>stattgefunden</em> 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.</p>
|
||||||
|
|
||||||
<p><strong>BEOs</strong> – 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.</p>
|
<p><strong>BEOs</strong> – 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.</p>
|
||||||
|
|
||||||
<p><strong>Beobachtete Objekte</strong> – nicht sichtbar bei BEOS und TD; bei Sonnenführungen fest auf „Sonne" gesetzt. Für alle anderen Arten:</p>
|
<p><strong>Beobachtete Objekte</strong> – nicht sichtbar bei BEOS und TD; bei Sonnenführungen fest auf „Sonne" gesetzt. Für alle anderen Arten:</p>
|
||||||
|
|||||||
@@ -21,6 +21,29 @@ export function artLabel(a: ArtFuehrung): string {
|
|||||||
return ARTEN_MAP[a] || a;
|
return ARTEN_MAP[a] || a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SpendeArt = 'bar' | 'ueberw' | 'kasse' | 'keine';
|
||||||
|
|
||||||
|
export const SPENDE_MAP: Record<SpendeArt, string> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ergebnis der Rückschreibung einer Sonderführung ins Register SoFue2. */
|
||||||
|
export interface SofueStatus {
|
||||||
|
status: 'ok' | 'kein_termin' | 'fehler' | 'uebersprungen';
|
||||||
|
id?: number;
|
||||||
|
wtermin?: string;
|
||||||
|
datum?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BeoOption {
|
export interface BeoOption {
|
||||||
ID: number;
|
ID: number;
|
||||||
Kuerzel: string;
|
Kuerzel: string;
|
||||||
@@ -48,6 +71,8 @@ export interface LogbuchEintrag {
|
|||||||
Kuppel: Kuppel;
|
Kuppel: Kuppel;
|
||||||
ArtFuehrung: ArtFuehrung;
|
ArtFuehrung: ArtFuehrung;
|
||||||
SonderName: string | null;
|
SonderName: string | null;
|
||||||
|
Spende: SpendeArt | null;
|
||||||
|
SpendeBetrag: number | null;
|
||||||
Beginn: string;
|
Beginn: string;
|
||||||
Ende: string;
|
Ende: string;
|
||||||
Besucher: number;
|
Besucher: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user