fix: Führungen über Mitternacht richtig datieren — Version 1.16.0
Das Formular hat Beginn und Ende bisher stumpf dasselbe Datum gegeben. Wer einen Eintrag nach Mitternacht erfasste, bekam damit einen Beginn, der nach dem Ende lag: Datum 9.8., Ende 00:30, Startzeit 21:00 ergab den 9.8. 21:00 als Beginn. Beginn und Ende werden jetzt auseinander abgeleitet. Anker ist das Ende — der Zeitpunkt, zu dem das Logbuch ausgefüllt wird und der von der Vorbelegung her verlässlich ist. Ist die Startzeit später als die Endzeit, liegt der Beginn am Vortag; wird die Endzeit unter die Startzeit gesetzt, wandert das Ende auf den Folgetag. Das Datumsfeld bleibt das Startdatum und springt sichtbar zurück, erklärt durch eine Zeile unter den Feldern. Über 12 Stunden Dauer erscheint ein Hinweis, der das Speichern nicht blockiert. migrate_zeiten.sql datiert Altbestände mit Beginn > Ende um einen Tag zurück. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,46 @@ function nowRounded5(): string {
|
||||
return `${pad(Math.floor(rounded / 60) % 24)}:${pad(rounded % 60)}`;
|
||||
}
|
||||
|
||||
function tagVerschieben(datum: string, tage: number): string {
|
||||
const d = new Date(datum + 'T12:00:00Z'); // Mittag — unempfindlich gegen Zeitzonen
|
||||
d.setUTCDate(d.getUTCDate() + tage);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Beginn aus dem Ende ableiten: Ist die Startzeit später als die Endzeit, ging die
|
||||
* Führung über Mitternacht und hat am Vortag begonnen. Anker ist das Ende — der
|
||||
* Zeitpunkt, zu dem das Logbuch ausgefüllt wird, und damit der verlässliche.
|
||||
*/
|
||||
function beginnAusEnde(ende: string, startZeit: string): string {
|
||||
const datum = startZeit > ende.slice(11, 16)
|
||||
? tagVerschieben(ende.slice(0, 10), -1)
|
||||
: ende.slice(0, 10);
|
||||
return `${datum}T${startZeit}`;
|
||||
}
|
||||
|
||||
/** Ende aus dem Beginn ableiten: Endzeit vor der Startzeit ⇒ Folgetag. */
|
||||
function endeAusBeginn(beginn: string, endZeit: string): string {
|
||||
const startZeit = beginn.slice(11, 16);
|
||||
const datum = startZeit && endZeit < startZeit
|
||||
? tagVerschieben(beginn.slice(0, 10), 1)
|
||||
: beginn.slice(0, 10);
|
||||
return `${datum}T${endZeit}`;
|
||||
}
|
||||
|
||||
/** "2026-08-09T00:30" → "09.08." */
|
||||
function datumKurz(zeitpunkt: string): string {
|
||||
const [j, m, t] = zeitpunkt.slice(0, 10).split('-');
|
||||
return t && m && j ? `${t}.${m}.` : zeitpunkt.slice(0, 10);
|
||||
}
|
||||
|
||||
/** Dauer in Minuten; null, solange keine Startzeit eingetragen ist. */
|
||||
function dauerMinuten(beginn: string, ende: string): number | null {
|
||||
if (!beginn.slice(11, 16) || !ende.slice(11, 16)) return null;
|
||||
const d = (Date.parse(ende + ':00Z') - Date.parse(beginn + ':00Z')) / 60000;
|
||||
return Number.isFinite(d) ? d : null;
|
||||
}
|
||||
|
||||
const NO_OBJEKTE_ARTEN: ArtFuehrung[] = ['BEOS', 'TD'];
|
||||
const SONNE_ART: ArtFuehrung = 'SonF';
|
||||
|
||||
@@ -73,6 +113,13 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
if (idx >= 0) fields[(idx + 1) % fields.length]?.focus();
|
||||
}
|
||||
|
||||
const dauer = dauerMinuten(beginn, ende);
|
||||
const ueberMitternacht = beginn.slice(11, 16) !== '' && beginn.slice(0, 10) !== ende.slice(0, 10);
|
||||
const dauerZuLang = dauer !== null && dauer > 12 * 60;
|
||||
const dauerText = dauer === null
|
||||
? ''
|
||||
: `${Math.floor(dauer / 60)}:${String(Math.round(dauer % 60)).padStart(2, '0')}`;
|
||||
|
||||
const showObjekte = !NO_OBJEKTE_ARTEN.includes(artFuehrung);
|
||||
const showBesucher = !NO_OBJEKTE_ARTEN.includes(artFuehrung);
|
||||
const isSonne = artFuehrung === SONNE_ART;
|
||||
@@ -315,8 +362,10 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
value={beginn.slice(0, 10)}
|
||||
onChange={(e) => {
|
||||
if (!e.target.value) return;
|
||||
setBeginn(e.target.value + 'T' + (beginn.slice(11, 16) || '00:00'));
|
||||
setEnde(e.target.value + 'T' + (ende.slice(11, 16) || '00:00'));
|
||||
// Das Feld bezeichnet den Beginn; das Ende folgt daraus.
|
||||
const neuerBeginn = e.target.value + 'T' + (beginn.slice(11, 16) || '00:00');
|
||||
setBeginn(neuerBeginn);
|
||||
setEnde(endeAusBeginn(neuerBeginn, ende.slice(11, 16) || '00:00'));
|
||||
}}
|
||||
required
|
||||
className="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"
|
||||
@@ -326,7 +375,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
<label className={labelCls}>Startzeit</label>
|
||||
<TimeInput
|
||||
value={beginn.slice(11, 16)}
|
||||
onChange={(t) => setBeginn(beginn.slice(0, 10) + 'T' + t)}
|
||||
onChange={(t) => setBeginn(beginnAusEnde(ende, t))}
|
||||
autoFocus
|
||||
className="w-24"
|
||||
/>
|
||||
@@ -335,7 +384,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
<label className={labelCls}>Endzeit</label>
|
||||
<TimeInput
|
||||
value={ende.slice(11, 16)}
|
||||
onChange={(t) => setEnde(ende.slice(0, 10) + 'T' + t)}
|
||||
onChange={(t) => setEnde(endeAusBeginn(beginn, t))}
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
@@ -354,6 +403,18 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Erklärt den Sprung des Datumsfeldes bei Führungen über Mitternacht */}
|
||||
{ueberMitternacht && (
|
||||
<div className="text-xs text-gray-500">
|
||||
Führung über Mitternacht — endet am {datumKurz(ende)} um {ende.slice(11, 16)} Uhr.
|
||||
</div>
|
||||
)}
|
||||
{dauerZuLang && (
|
||||
<div className="text-xs text-amber-700">
|
||||
Dauer {dauerText} Std. — bitte Start- und Endzeit prüfen.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Name/Gruppe und Spende bei Sonderführung */}
|
||||
{artFuehrung === 'SF' && (
|
||||
<div className="flex flex-wrap gap-3 items-end">
|
||||
|
||||
Reference in New Issue
Block a user