Files
logbuch/components/ObjektSelector.tsx
T
admin 8bff795247 v1.4.0: Sonderführung, Zeiterfassung, Enter-Navigation, Objektsuche
- Sonderführung: neues Feld 'Name/Gruppe' (DB-Spalte SonderName), in Liste sichtbar
- Wetter: Race-Condition behoben (API überschreibt DB-Werte beim Bearbeiten nicht mehr)
- Zeiterfassung: TimePicker5 ersetzt durch freie Texteingabe (TimeInput) mit Validierung
- Enter-Taste: navigiert zum nächsten Feld statt die Form abzuschicken; Luftdruck → zurück zu Art; Bemerkungen bleibt normal
- Objektsuche: Freitext-Suche im ObjektSelector, filtert nach Präfix (case-insensitive)
- UI-Anpassungen: kompakteres Layout (space-y-2, kleinere Abstände)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 08:50:51 +02:00

147 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useRef, useState } from 'react';
import type { ObjektOption, SelectedObjekt } from '@/types/logbuch';
interface Props {
selected: SelectedObjekt[];
onChange: (objekte: SelectedObjekt[]) => void;
}
export default function ObjektSelector({ selected, onChange }: Props) {
const [all, setAll] = useState<ObjektOption[]>([]);
const [search, setSearch] = useState('');
const [dropdownOpen, setDropdownOpen] = useState(false);
const [newName, setNewName] = useState('');
const [showNewInput, setShowNewInput] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetch('/api/objekte')
.then((r) => { if (!r.ok) throw new Error('Fehler'); return r.json(); })
.then(setAll)
.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(obj: ObjektOption) {
onChange([...selected, { ID: obj.ID, Name: obj.Name }]);
setSearch('');
setDropdownOpen(false);
}
function addNew() {
const name = newName.trim();
if (!name || selectedNames.has(name.toLowerCase())) return;
onChange([...selected, { ID: null, Name: name }]);
setNewName('');
setShowNewInput(false);
}
function remove(name: string) {
onChange(selected.filter((o) => o.Name !== name));
}
return (
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{selected.map((o) => (
<span
key={o.Name}
className="inline-flex items-center gap-2 bg-green-100 text-green-800 text-base px-3 py-1.5 rounded-full"
>
{o.Name}
<button
type="button"
onClick={() => remove(o.Name)}
className="flex items-center justify-center w-7 h-7 rounded-full text-green-600 hover:bg-red-100 hover:text-red-600 font-bold text-xl leading-none"
aria-label={`${o.Name} entfernen`}
>
×
</button>
</span>
))}
</div>
<div className="flex gap-2">
{available.length > 0 && (
<div ref={wrapperRef} className="relative flex-1">
<input
type="text"
value={search}
onChange={(e) => { 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 && (
<div className="absolute z-50 left-0 right-0 mt-1 bg-white border-2 border-gray-400 rounded-lg shadow-lg max-h-60 overflow-y-auto">
{filtered.map((o) => (
<button
key={o.ID}
type="button"
onClick={() => add(o)}
className="w-full text-left px-4 py-2 text-sm text-gray-900 hover:bg-blue-50 active:bg-blue-100 border-b border-gray-100 last:border-0"
>
{o.Name}
</button>
))}
</div>
)}
</div>
)}
<button
type="button"
onClick={() => setShowNewInput((v) => !v)}
className="px-4 py-2 border-2 border-gray-400 rounded-lg bg-white text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap"
>
+ Neu
</button>
</div>
{showNewInput && (
<div className="flex gap-2">
<input
type="text"
value={newName}
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-sm focus:border-blue-500 focus:outline-none"
autoFocus
/>
<button
type="button"
onClick={addNew}
className="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700"
>
OK
</button>
<button
type="button"
onClick={() => { setShowNewInput(false); setNewName(''); }}
className="px-4 py-2 bg-gray-200 text-gray-700 text-sm rounded-lg hover:bg-gray-300"
>
</button>
</div>
)}
</div>
);
}