bb3f6f05a3
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>
562 lines
21 KiB
TypeScript
562 lines
21 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import type { Kuppel, ArtFuehrung, SpendeArt, BeoOption, SelectedObjekt, SofueStatus, 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';
|
|
import TimeInput from './TimeInput';
|
|
|
|
interface Props {
|
|
kuppel: Kuppel;
|
|
currentUserBeo: BeoOption;
|
|
editEntry?: LogbuchEintrag | null;
|
|
/** sofue: Ergebnis der Rückschreibung ins Sonderführungs-Register, sofern erfolgt. */
|
|
onSaved: (sofue?: SofueStatus | null) => void;
|
|
}
|
|
|
|
function toLocalDatetimeValue(isoOrDatetime: string): string {
|
|
if (!isoOrDatetime) return '';
|
|
return isoOrDatetime.slice(0, 16);
|
|
}
|
|
|
|
function todayDate(): string {
|
|
const now = new Date();
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
}
|
|
|
|
function nowRounded5(): string {
|
|
const now = new Date();
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
const total = now.getHours() * 60 + now.getMinutes();
|
|
const rounded = Math.ceil(total / 5) * 5;
|
|
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';
|
|
|
|
export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved }: Props) {
|
|
const [artFuehrung, setArtFuehrung] = useState<ArtFuehrung>('RF');
|
|
const [beginn, setBeginn] = useState(todayDate());
|
|
const [ende, setEnde] = useState(todayDate() + 'T' + nowRounded5());
|
|
const [besucher, setBesucher] = useState<number | ''>('');
|
|
const [beos, setBeos] = useState<BeoOption[]>([currentUserBeo]);
|
|
const [objekte, setObjekte] = useState<SelectedObjekt[]>([]);
|
|
const [bemerkungen, setBemerkungen] = 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 [tempRaw, setTempRaw] = useState('0');
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState(false);
|
|
const [wetterHinweis, setWetterHinweis] = useState('');
|
|
|
|
const editEntryRef = useRef(editEntry);
|
|
editEntryRef.current = editEntry;
|
|
const formRef = useRef<HTMLFormElement>(null);
|
|
const prevArtRef = useRef<ArtFuehrung | null>(null);
|
|
const prevEndeRef = useRef<string | null>(null);
|
|
|
|
function focusNext(current: HTMLElement) {
|
|
if (!formRef.current) return;
|
|
const fields = Array.from(
|
|
formRef.current.querySelectorAll<HTMLElement>(
|
|
'input:not([disabled]), textarea:not([disabled]), #art-select, #spende-select'
|
|
)
|
|
);
|
|
const idx = fields.indexOf(current);
|
|
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;
|
|
|
|
useEffect(() => {
|
|
fetch('/api/wetter')
|
|
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
|
.then((w: Wetter) => {
|
|
if (!editEntryRef.current) {
|
|
setWetter({ temp: w.temp, feuchte: Math.round(w.feuchte), druck: Math.round(w.druck) });
|
|
setTempRaw(String(w.temp));
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
// Ändert der Benutzer Datum oder Endzeit, wird das Wetter zu diesem Zeitpunkt
|
|
// nachgeladen. Der erste Lauf wird übersprungen: beim Anlegen liefert bereits der
|
|
// Effekt oben den aktuellen Wert, beim Bearbeiten stehen die gespeicherten Werte drin.
|
|
useEffect(() => {
|
|
if (prevEndeRef.current === null) {
|
|
prevEndeRef.current = ende;
|
|
return;
|
|
}
|
|
if (prevEndeRef.current === ende) return;
|
|
prevEndeRef.current = ende;
|
|
|
|
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(ende)) return;
|
|
|
|
let verworfen = false;
|
|
fetch(`/api/wetter?zeit=${encodeURIComponent(ende)}`)
|
|
.then(async (r) => {
|
|
if (r.status === 404) throw new Error('keine');
|
|
if (!r.ok) throw new Error('fehler');
|
|
return r.json() as Promise<Wetter>;
|
|
})
|
|
.then((w) => {
|
|
if (verworfen) return;
|
|
setWetter({ temp: w.temp, feuchte: Math.round(w.feuchte), druck: Math.round(w.druck) });
|
|
setTempRaw(String(w.temp));
|
|
setWetterHinweis('');
|
|
})
|
|
.catch((e: Error) => {
|
|
// Bestehende Werte stehen lassen — ein leeres Formular wäre schlechter als alte Zahlen.
|
|
if (!verworfen) {
|
|
setWetterHinweis(
|
|
e.message === 'keine'
|
|
? 'Keine Wetterdaten zu diesem Zeitpunkt — Werte bitte selbst eintragen.'
|
|
: 'Wetterdaten konnten nicht geladen werden.'
|
|
);
|
|
}
|
|
});
|
|
return () => { verworfen = true; };
|
|
}, [ende]);
|
|
|
|
useEffect(() => {
|
|
if (editEntry) {
|
|
setArtFuehrung(editEntry.ArtFuehrung);
|
|
setBeginn(toLocalDatetimeValue(editEntry.Beginn));
|
|
setEnde(toLocalDatetimeValue(editEntry.Ende));
|
|
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) });
|
|
setTempRaw(String(t));
|
|
}
|
|
} else {
|
|
setArtFuehrung('RF');
|
|
setBeginn(todayDate());
|
|
setEnde(todayDate() + 'T' + nowRounded5());
|
|
setBesucher(0);
|
|
setBeos([currentUserBeo]);
|
|
setObjekte([]);
|
|
setBemerkungen('');
|
|
setSonderName('');
|
|
setSpende('');
|
|
setSpendeBetragRaw('');
|
|
setBesucher('');
|
|
}
|
|
}, [editEntry, currentUserBeo]);
|
|
|
|
useEffect(() => {
|
|
if (editEntry && editEntry.BEOs) {
|
|
fetch('/api/beos')
|
|
.then((r) => r.json())
|
|
.then((all: BeoOption[]) => {
|
|
const kuerzel = editEntry.BEOs.split(', ').map((k) => k.trim());
|
|
setBeos(all.filter((b) => kuerzel.includes(b.Kuerzel)));
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
if (editEntry && editEntry.Objekte) {
|
|
const allNames = editEntry.Objekte.split(', ').map((n) => n.trim());
|
|
const isSonneEntry = editEntry.ArtFuehrung === SONNE_ART;
|
|
const names = isSonneEntry ? allNames.filter((n) => n.toLowerCase() !== 'sonne') : allNames;
|
|
const kat = isSonneEntry ? 'sonne' : 'stern';
|
|
fetch('/api/objekte?kategorie=' + kat)
|
|
.then((r) => r.json())
|
|
.then((all: { ID: number; Name: string }[]) => {
|
|
const result: SelectedObjekt[] = names.map((name) => {
|
|
const found = all.find((o) => o.Name === name);
|
|
return { ID: found?.ID ?? null, Name: name };
|
|
});
|
|
setObjekte(result);
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
}, [editEntry]);
|
|
|
|
// 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) {
|
|
e.preventDefault();
|
|
setError('');
|
|
setSuccess(false);
|
|
|
|
if (showBesucher && artFuehrung !== 'Beob' && besucher === '') {
|
|
setError('Bitte Besucherzahl eingeben.');
|
|
return;
|
|
}
|
|
if (!beginn.slice(11, 16)) {
|
|
setError('Bitte Startzeit eingeben.');
|
|
return;
|
|
}
|
|
if (beginn === ende) {
|
|
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);
|
|
|
|
const body = {
|
|
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,
|
|
beoIds: beos.map((b) => b.ID),
|
|
objekte: showObjekte ? (isSonne ? [{ ID: null, Name: 'Sonne' }, ...objekte] : objekte) : [],
|
|
Bemerkungen: bemerkungen,
|
|
Wetter: { ...wetter, temp: parseFloat(tempRaw) || 0 },
|
|
};
|
|
|
|
const url = editEntry ? `/api/logbuch/${editEntry.ID}` : '/api/logbuch';
|
|
const method = editEntry ? 'PUT' : 'POST';
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
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) {
|
|
setBeginn(todayDate());
|
|
setEnde(todayDate() + 'T' + nowRounded5());
|
|
setBesucher(0);
|
|
setBeos([currentUserBeo]);
|
|
setObjekte([]);
|
|
setBemerkungen('');
|
|
setSonderName('');
|
|
setSpende('');
|
|
setSpendeBetragRaw('');
|
|
}
|
|
// 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 {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
const inputCls = 'w-full px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-blue-500 focus:outline-none';
|
|
const labelCls = 'block text-xs font-medium text-gray-700 mb-0.5';
|
|
|
|
return (
|
|
<form
|
|
ref={formRef}
|
|
onSubmit={handleSubmit}
|
|
onKeyDown={(e) => {
|
|
if (e.key !== 'Enter') return;
|
|
const tag = (e.target as HTMLElement).tagName;
|
|
if (tag === 'BUTTON' || tag === 'TEXTAREA') return;
|
|
e.preventDefault();
|
|
focusNext(e.target as HTMLElement);
|
|
}}
|
|
className="space-y-2 max-w-2xl mx-auto border-2 border-gray-400 rounded-xl p-3 bg-white"
|
|
>
|
|
|
|
{/* Art der Führung / Datum / Startzeit / Endzeit / Besucher — eine Zeile */}
|
|
<div className="flex flex-wrap gap-3 items-end">
|
|
<div className="flex-1 min-w-[160px]">
|
|
<label className={labelCls}>Art der Führung</label>
|
|
<CustomSelect
|
|
id="art-select"
|
|
placeholder={artLabel(artFuehrung)}
|
|
options={ARTEN.map((a) => ({ value: a, label: artLabel(a) }))}
|
|
onChange={(v) => setArtFuehrung(v as ArtFuehrung)}
|
|
/>
|
|
</div>
|
|
<div className="shrink-0">
|
|
<label className={labelCls}>Datum</label>
|
|
<input
|
|
type="date"
|
|
value={beginn.slice(0, 10)}
|
|
onChange={(e) => {
|
|
if (!e.target.value) return;
|
|
// 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"
|
|
/>
|
|
</div>
|
|
<div className="shrink-0">
|
|
<label className={labelCls}>Startzeit</label>
|
|
<TimeInput
|
|
value={beginn.slice(11, 16)}
|
|
onChange={(t) => setBeginn(beginnAusEnde(ende, t))}
|
|
autoFocus
|
|
className="w-24"
|
|
/>
|
|
</div>
|
|
<div className="shrink-0">
|
|
<label className={labelCls}>Endzeit</label>
|
|
<TimeInput
|
|
value={ende.slice(11, 16)}
|
|
onChange={(t) => setEnde(endeAusBeginn(beginn, t))}
|
|
className="w-24"
|
|
/>
|
|
</div>
|
|
{showBesucher && (
|
|
<div className="shrink-0">
|
|
<label className={labelCls}>Besucher</label>
|
|
<input
|
|
type="number"
|
|
value={besucher}
|
|
onChange={(e) => setBesucher(e.target.value === '' ? '' : parseInt(e.target.value) || 0)}
|
|
min={0}
|
|
max={9999}
|
|
className="w-20 px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
)}
|
|
</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">
|
|
<div className="flex-1 min-w-[160px]">
|
|
<label className={labelCls}>Name / Gruppe</label>
|
|
<input
|
|
type="text"
|
|
value={sonderName}
|
|
onChange={(e) => 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"
|
|
/>
|
|
</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 */}
|
|
<div>
|
|
<label className={labelCls}>BEOs</label>
|
|
<BeoSelector selected={beos} onChange={setBeos} />
|
|
</div>
|
|
|
|
{/* Objekte — abhängig von der Art der Führung */}
|
|
{showObjekte && (
|
|
<div>
|
|
<label className={labelCls}>Beobachtete Objekte</label>
|
|
<ObjektSelector
|
|
selected={objekte}
|
|
onChange={setObjekte}
|
|
kategorie={isSonne ? 'sonne' : 'stern'}
|
|
fixedItems={isSonne ? [{ ID: null, Name: 'Sonne' }] : []}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bemerkungen */}
|
|
<div>
|
|
<label className={labelCls}>
|
|
Bemerkungen
|
|
<span className="ml-2 text-gray-400 font-normal text-xs">({bemerkungen.length}/500)</span>
|
|
</label>
|
|
<textarea
|
|
value={bemerkungen}
|
|
onChange={(e) => setBemerkungen(e.target.value.slice(0, 500))}
|
|
rows={2}
|
|
className="w-full px-2 py-1 border-2 border-gray-400 rounded-lg bg-white text-gray-900 text-sm focus:border-blue-500 focus:outline-none resize-y"
|
|
placeholder="Freier Text (max. 500 Zeichen)"
|
|
/>
|
|
</div>
|
|
|
|
{/* Wetter */}
|
|
<div>
|
|
<div className="grid grid-cols-3">
|
|
<div>
|
|
<label className="block text-xs text-gray-500 mb-0.5">Temperatur (°C)</label>
|
|
<input
|
|
type="text"
|
|
inputMode="decimal"
|
|
value={tempRaw}
|
|
onChange={(e) => setTempRaw(e.target.value)}
|
|
className="w-1/3 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 className="flex flex-col items-center">
|
|
<label className="block text-xs text-gray-500 mb-0.5">Feuchte (%)</label>
|
|
<input
|
|
type="number"
|
|
value={wetter.feuchte}
|
|
onChange={(e) => setWetter({ ...wetter, feuchte: parseInt(e.target.value) || 0 })}
|
|
step="1"
|
|
className="w-1/3 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 className="flex flex-col items-end">
|
|
<label className="block text-xs text-gray-500 mb-0.5">Luftdruck (hPa)</label>
|
|
<input
|
|
type="number"
|
|
value={wetter.druck}
|
|
onChange={(e) => setWetter({ ...wetter, druck: parseInt(e.target.value) || 0 })}
|
|
step="1"
|
|
className="w-1/3 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>
|
|
{wetterHinweis && (
|
|
<div className="mt-1 text-xs text-amber-700">{wetterHinweis}</div>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-300 text-red-700 px-3 py-2 rounded-lg text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
{success && !editEntry && (
|
|
<div className="bg-green-50 border border-green-300 text-green-700 px-3 py-2 rounded-lg text-sm">
|
|
Eintrag gespeichert.
|
|
</div>
|
|
)}
|
|
|
|
{/* Buttons — zentriert */}
|
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
|
<button
|
|
type="submit"
|
|
disabled={saving}
|
|
className="w-full sm:w-auto px-6 py-1 bg-[#85B7D7] hover:bg-[#6a9fc5] text-black font-medium rounded-lg transition-colors disabled:opacity-50 text-sm"
|
|
>
|
|
{saving ? 'Speichern...' : editEntry ? 'Änderungen speichern' : 'Eintrag speichern'}
|
|
</button>
|
|
{editEntry && (
|
|
<button
|
|
type="button"
|
|
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"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|