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>
This commit is contained in:
@@ -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<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')
|
||||
@@ -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) {
|
||||
|
||||
<div className="flex gap-2">
|
||||
{available.length > 0 && (
|
||||
<div className="flex-1">
|
||||
<CustomSelect
|
||||
placeholder="+ Objekte hinzufügen"
|
||||
options={available.map((o) => ({ value: String(o.ID), label: o.Name }))}
|
||||
onChange={add}
|
||||
keepOpen
|
||||
<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-base text-gray-700 hover:bg-gray-50 whitespace-nowrap"
|
||||
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>
|
||||
@@ -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
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addNew}
|
||||
className="px-4 py-2 bg-green-600 text-white text-base rounded-lg hover:bg-green-700"
|
||||
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-base rounded-lg hover:bg-gray-300"
|
||||
className="px-4 py-2 bg-gray-200 text-gray-700 text-sm rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user