diff --git a/app/MainClient.tsx b/app/MainClient.tsx index 8f4bc69..e7e86ee 100644 --- a/app/MainClient.tsx +++ b/app/MainClient.tsx @@ -43,11 +43,11 @@ export default function MainClient({ kuerzel, beoId, beoName }: Props) { } return ( -
-
+
+
{/* Header */} -
+

Sternwarte-Welzheim   Logbuch für {activeKuppel}-Kuppel @@ -63,7 +63,7 @@ export default function MainClient({ kuerzel, beoId, beoName }: Props) {

{/* Kuppel-Tabs */} -
+
{KUPPELN.map((k) => (
@@ -310,7 +331,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved value={wetter.feuchte} onChange={(e) => setWetter({ ...wetter, feuchte: parseInt(e.target.value) || 0 })} step="1" - className="w-1/3 px-2 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none" + 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" />
@@ -320,7 +341,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved value={wetter.druck} onChange={(e) => setWetter({ ...wetter, druck: parseInt(e.target.value) || 0 })} step="1" - className="w-1/3 px-2 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none" + 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" />
@@ -342,7 +363,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved @@ -350,7 +371,7 @@ export default function LogbuchForm({ kuppel, currentUserBeo, editEntry, onSaved diff --git a/components/LogbuchList.tsx b/components/LogbuchList.tsx index 44f66c0..6e19ebb 100644 --- a/components/LogbuchList.tsx +++ b/components/LogbuchList.tsx @@ -105,7 +105,10 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 20, co
{formatTime(e.Ende)}
)} - {e.ArtFuehrung} + +
{e.ArtFuehrung}
+ {e.SonderName &&
{e.SonderName}
} + {e.Besucher || ''} {e.BEOs || '—'} {e.Objekte || '—'} diff --git a/components/ObjektSelector.tsx b/components/ObjektSelector.tsx index 15637d0..af2fe2e 100644 --- a/components/ObjektSelector.tsx +++ b/components/ObjektSelector.tsx @@ -1,8 +1,7 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { ObjektOption, SelectedObjekt } from '@/types/logbuch'; -import CustomSelect from './CustomSelect'; interface Props { selected: SelectedObjekt[]; @@ -11,8 +10,11 @@ interface Props { export default function ObjektSelector({ selected, onChange }: Props) { const [all, setAll] = useState([]); + const [search, setSearch] = useState(''); + const [dropdownOpen, setDropdownOpen] = useState(false); const [newName, setNewName] = useState(''); const [showNewInput, setShowNewInput] = useState(false); + const wrapperRef = useRef(null); useEffect(() => { fetch('/api/objekte') @@ -21,14 +23,26 @@ export default function ObjektSelector({ selected, onChange }: Props) { .catch(() => {}); }, []); + useEffect(() => { + function handleOutside(e: MouseEvent) { + if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { + setDropdownOpen(false); + } + } + if (dropdownOpen) document.addEventListener('mousedown', handleOutside); + return () => document.removeEventListener('mousedown', handleOutside); + }, [dropdownOpen]); + const selectedNames = new Set(selected.map((o) => o.Name.toLowerCase())); const available = all.filter((o) => !selectedNames.has(o.Name.toLowerCase())); + const filtered = search + ? available.filter((o) => o.Name.toLowerCase().startsWith(search.toLowerCase())) + : available; - function add(value: string) { - const obj = all.find((o) => o.ID === parseInt(value)); - if (obj && !selectedNames.has(obj.Name.toLowerCase())) { - onChange([...selected, { ID: obj.ID, Name: obj.Name }]); - } + function add(obj: ObjektOption) { + onChange([...selected, { ID: obj.ID, Name: obj.Name }]); + setSearch(''); + setDropdownOpen(false); } function addNew() { @@ -66,19 +80,35 @@ export default function ObjektSelector({ selected, onChange }: Props) {
{available.length > 0 && ( -
- ({ value: String(o.ID), label: o.Name }))} - onChange={add} - keepOpen +
+ { setSearch(e.target.value); setDropdownOpen(true); }} + onFocus={() => setDropdownOpen(true)} + placeholder="Objekt suchen..." + className="w-full px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-900 focus:border-blue-500 focus:outline-none" /> + {dropdownOpen && filtered.length > 0 && ( +
+ {filtered.map((o) => ( + + ))} +
+ )}
)} @@ -92,20 +122,20 @@ export default function ObjektSelector({ selected, onChange }: Props) { onChange={(e) => setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addNew(); } }} placeholder="Objektname eingeben" - className="flex-1 px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-base focus:border-blue-500 focus:outline-none" + className="flex-1 px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm focus:border-blue-500 focus:outline-none" autoFocus /> diff --git a/components/TimeInput.tsx b/components/TimeInput.tsx new file mode 100644 index 0000000..91e9408 --- /dev/null +++ b/components/TimeInput.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface Props { + value: string; // "HH:MM" + onChange: (value: string) => void; + className?: string; +} + +function isValid(t: string): boolean { + if (!/^\d{1,2}:\d{2}$/.test(t)) return false; + const [h, m] = t.split(':').map(Number); + return h >= 0 && h <= 23 && m >= 0 && m <= 59; +} + +function normalize(t: string): string { + const [h, m] = t.split(':').map(Number); + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; +} + +export default function TimeInput({ value, onChange, className = '' }: Props) { + const [local, setLocal] = useState(value); + const [error, setError] = useState(false); + + useEffect(() => { + setLocal(value); + setError(false); + }, [value]); + + function handleBlur() { + if (isValid(local)) { + const norm = normalize(local); + setLocal(norm); + setError(false); + onChange(norm); + } else { + setError(true); + } + } + + return ( +
+ { setLocal(e.target.value); setError(false); }} + onBlur={handleBlur} + placeholder="HH:MM" + className={`w-full px-2 py-1 border-2 rounded-lg bg-white text-sm text-gray-900 font-mono text-center focus:outline-none ${ + error ? 'border-red-500 focus:border-red-500' : 'border-gray-400 focus:border-blue-500' + }`} + /> + {error && ( +

+ Ungültig (00:00 – 23:59) +

+ )} +
+ ); +} diff --git a/types/logbuch.ts b/types/logbuch.ts index a68d144..dbef6a8 100644 --- a/types/logbuch.ts +++ b/types/logbuch.ts @@ -47,6 +47,7 @@ export interface LogbuchEintrag { ID: number; Kuppel: Kuppel; ArtFuehrung: ArtFuehrung; + SonderName: string | null; Beginn: string; Ende: string; Besucher: number;