import React, { useEffect, useMemo, useRef, useState } from "react"; import { getAuditLogs, getUsersLite } from "../../api"; /* ----------------------------- kleine Utils ----------------------------- */ function euroDelta(newC, oldC) { if (newC == null || oldC == null) return null; const d = (newC - oldC) / 100; return (d >= 0 ? "+" : "") + d.toFixed(2) + " €"; } function fmtDT(v) { if (!v) return "—"; const d = new Date(v); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString(); } function classNames(...xs) { return xs.filter(Boolean).join(" "); } /* -------------------------- NiceDropdown (inline) ------------------------ */ function NiceDropdown({ value, onChange, options, placeholder = "Auswählen…", className = "", buttonClassName = "", menuClassName = "", minWidth = 160, }) { const [open, setOpen] = useState(false); const btnRef = useRef(null); const popRef = useRef(null); const current = useMemo( () => options?.find((o) => String(o.value) === String(value)) ?? null, [options, value] ); useEffect(() => { function onDoc(e) { if (!open) return; if (popRef.current?.contains(e.target)) return; if (btnRef.current?.contains(e.target)) return; setOpen(false); } document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [open]); return (
{open && (
{options.map(opt => { const active = String(opt.value) === String(value); return ( ); })}
)}
); } /* ------------------------ UserPicker (Such-Combobox) --------------------- */ function UserPicker({ value, onChange, placeholder = "Alle Nutzer" }) { const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const btnRef = useRef(null); const popRef = useRef(null); useEffect(() => { let alive = true; if (!open) return; (async () => { setLoading(true); try { const res = await getUsersLite({ limit: 20, offset: 0, q }); const arr = Array.isArray(res) ? res : (res.items || []); if (alive) setItems(arr); } finally { if (alive) setLoading(false); } })(); return () => { alive = false; }; }, [open, q]); useEffect(() => { function onDoc(e) { if (!open) return; if (popRef.current?.contains(e.target)) return; if (btnRef.current?.contains(e.target)) return; setOpen(false); } document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, [open]); const selected = items.find(u => String(u.id) === String(value)); return (
{open && (
setQ(e.target.value)} />
{loading ? (
Laden…
) : ( <> {items.map(u => ( ))} )}
)}
); } /* --------------------------------- Page ---------------------------------- */ export default function LogsPage() { const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [hasMore, setHasMore] = useState(true); // Filter-Params const [userId, setUserId] = useState(""); const [action, setAction] = useState(""); const [range, setRange] = useState("7d"); // 24h, 7d, 30d, all const [query, setQuery] = useState(""); const [offset, setOffset] = useState(0); const LIMIT = 100; function computeRange(r) { if (r === "all") return {}; const now = new Date(); let from = new Date(now); if (r === "24h") from.setHours(now.getHours() - 24); else if (r === "7d") from.setDate(now.getDate() - 7); else if (r === "30d") from.setDate(now.getDate() - 30); return { date_from: from.toISOString(), date_to: now.toISOString() }; } async function load(reset = false) { setLoading(true); setErr(null); try { const pageOffset = reset ? 0 : offset; const { date_from, date_to } = computeRange(range); const data = await getAuditLogs({ limit: LIMIT, offset: pageOffset, user_id: userId || undefined, action: action || undefined, q: query || undefined, date_from, date_to, }); const arr = Array.isArray(data) ? data : (data.items || []); setRows((prev) => (reset ? arr : [...prev, ...arr])); setHasMore(arr.length === LIMIT); setOffset(pageOffset + arr.length); } catch (e) { setErr(e); } finally { setLoading(false); } } // Initial + bei Filterwechsel neu laden useEffect(() => { load(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId, action, range]); // Clientseitige Fallback-Suche (falls Backend "q" ignoriert) const visible = useMemo(() => { if (!query) return rows; const q = query.toLowerCase(); return rows.filter(r => (r.user?.name || "").toLowerCase().includes(q) || String(r.user_id ?? "").includes(q) || String(r.action ?? "").toLowerCase().includes(q) || String(r.info ?? "").toLowerCase().includes(q) ); }, [rows, query]); return (

Audit-Logs

{/* Toolbar */}
setQuery(e.target.value)} />
{/* Tabelle */}
{visible.map((r) => ( ))} {loading && ( )} {!loading && visible.length === 0 && ( )}
Zeit User Aktion Info Δ Balance
{fmtDT(r.timestamp)} {r.user?.name ?? r.user?.alias ?? r.user_id} {String(r.action ?? "—")} {r.info ?? "—"} {euroDelta(r.new_balance_cents, r.old_balance_cents) ?? "—"}
Laden…
Keine Einträge.
{/* Pagination */}
{rows.length} geladen{hasMore ? " (Mehr verfügbar)" : ""}
); }