v1.7.4: Suche in Listenansicht (Bemerkungen, Objekte, BEOs)

Suchfeld in der Toolbar der Listenansicht: Suche über alle Einträge
der Kuppel in Bemerkungen, Objekte und BEOs. Monatsauswahl, Suchfeld
und Drucken-Button in einer Zeile; Monatsauswahl wird bei aktiver
Suche unsichtbar aber platzhaltend ausgeblendet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 21:45:09 +02:00
parent d5ceff74be
commit 9e2f430d4a
5 changed files with 264 additions and 48 deletions
+65 -45
View File
@@ -59,20 +59,27 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
const [deleteId, setDeleteId] = useState<number | null>(null);
const [error, setError] = useState('');
const [printEntries, setPrintEntries] = useState<LogbuchEintrag[] | null>(null);
const [search, setSearch] = useState('');
const [activeSearch, setActiveSearch] = useState('');
const printPending = useRef(false);
useEffect(() => { setPage(0); }, [kuppel, refreshKey, month]);
useEffect(() => {
const t = setTimeout(() => setActiveSearch(search.trim()), 300);
return () => clearTimeout(t);
}, [search]);
useEffect(() => { setPage(0); }, [kuppel, refreshKey, month, activeSearch]);
useEffect(() => {
setLoading(true);
const offset = page * limit;
const url = `/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=${limit}&offset=${offset}` +
(month ? `&month=${encodeURIComponent(month)}` : '');
(activeSearch ? `&search=${encodeURIComponent(activeSearch)}` : (month ? `&month=${encodeURIComponent(month)}` : ''));
fetch(url)
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
.then((data) => { setEntries(data.entries); setTotal(data.total); setLoading(false); })
.catch(() => { setError('Fehler beim Laden.'); setLoading(false); });
}, [kuppel, refreshKey, limit, page, month]);
}, [kuppel, refreshKey, limit, page, month, activeSearch]);
useEffect(() => {
function onAfterPrint() { setPrintEntries(null); }
@@ -108,32 +115,57 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
}
}
const monthNav = !compact && (
const toolbar = !compact && (
<div className="flex items-center gap-2 mb-3 print:hidden">
<button
onClick={() => setMonth((m) => prevMonth(m))}
className="px-2 py-1 text-sm rounded-lg bg-gray-200 hover:bg-gray-300"
></button>
<input
type="month"
value={month}
max={currentMonth()}
onChange={(e) => setMonth(e.target.value > currentMonth() ? currentMonth() : e.target.value)}
className="border border-gray-300 rounded-lg px-2 py-1 text-sm"
/>
<button
onClick={() => setMonth((m) => nextMonth(m))}
disabled={month >= currentMonth()}
className="px-2 py-1 text-sm rounded-lg bg-gray-200 hover:bg-gray-300 disabled:opacity-40 disabled:cursor-not-allowed"
></button>
{month !== currentMonth() && (
<div
className="flex items-center gap-1 shrink-0"
style={{ visibility: activeSearch ? 'hidden' : 'visible' }}
>
<button
onClick={() => setMonth(currentMonth())}
className="text-sm text-blue-600 hover:underline"
>
Aktueller Monat
</button>
)}
onClick={() => setMonth((m) => prevMonth(m))}
className="px-2 py-1 text-sm rounded-lg bg-gray-200 hover:bg-gray-300"
></button>
<input
type="month"
value={month}
max={currentMonth()}
onChange={(e) => setMonth(e.target.value > currentMonth() ? currentMonth() : e.target.value)}
className="border border-gray-300 rounded-lg px-2 py-1 text-sm"
/>
<button
onClick={() => setMonth((m) => nextMonth(m))}
disabled={month >= currentMonth()}
className="px-2 py-1 text-sm rounded-lg bg-gray-200 hover:bg-gray-300 disabled:opacity-40 disabled:cursor-not-allowed"
></button>
{month !== currentMonth() && (
<button
onClick={() => setMonth(currentMonth())}
className="text-sm text-blue-600 hover:underline ml-1"
>Aktueller Monat</button>
)}
</div>
<div className="relative flex-1 min-w-0 mx-3">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Suche in Bemerkungen, Objekte, BEOs…"
className="w-full px-3 py-1.5 pr-8 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-500"
/>
{search ? (
<button
onClick={() => setSearch('')}
aria-label="Suche löschen"
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 text-sm leading-none"
></button>
) : (
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none text-sm">🔍</span>
)}
</div>
<button
onClick={handlePrint}
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg shrink-0"
>🖨 Drucken</button>
</div>
);
@@ -150,27 +182,15 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
? 'px-1.5 py-1 border border-gray-300 text-xs font-semibold'
: 'px-3 py-2 border border-gray-300';
if (loading) return <>{monthNav}<div className="text-gray-500 text-sm py-4">Lade Einträge...</div></>;
if (error) return <>{monthNav}<div className="text-red-600 text-sm py-4">{error}</div></>;
const displayEntries = printEntries ?? entries;
return (
<div>
{!compact && (
<div className="flex justify-between items-center mb-2 print:hidden">
<span className="text-sm font-semibold text-gray-600">Einträge {kuppel}-Kuppel</span>
<button
onClick={handlePrint}
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg"
>
🖨 Drucken
</button>
</div>
)}
{monthNav}
{toolbar}
{printHeader}
<div className="overflow-x-auto">
{loading && <div className="text-gray-500 text-sm py-4">Lade Einträge...</div>}
{error && <div className="text-red-600 text-sm py-4">{error}</div>}
{!loading && !error && <div className="overflow-x-auto">
<table className="w-full border-collapse" style={{ fontSize: compact ? '0.75rem' : '0.875rem' }}>
<thead>
<tr className="bg-gray-100 text-left">
@@ -196,7 +216,7 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
{displayEntries.length === 0 ? (
<tr>
<td colSpan={compact ? 7 : 10} className="px-3 py-4 text-gray-500 text-sm text-center">
Keine Einträge für {monthLabel(month)}.
{activeSearch ? `Keine Einträge für „${activeSearch}" gefunden.` : `Keine Einträge für ${monthLabel(month)}.`}
</td>
</tr>
) : displayEntries.map((e) => (
@@ -257,7 +277,7 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
))}
</tbody>
</table>
</div>
</div>}
{!compact && total > limit && (
<div className="flex items-center justify-center gap-3 mt-3 print:hidden">