v1.7.6: Bearbeiten/Löschen auf BEO-Mitglieder beschränkt

Ändern und Löschen eines Eintrags ist nur noch für angemeldete BEOs des Eintrags möglich (Admins dürfen immer). Serverseitige Prüfung via logbuch_beos-Tabelle; clientseitig werden die Aktions-Buttons nur eingeblendet, wenn der User in der BEO-Liste steht.

Außerdem: setState-in-Effect-Linterfehler in LogbuchList behoben (abgeleiteter Loading-State, abgeleitetes Page-Reset via filterKey).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 19:17:00 +02:00
parent d56ebb229d
commit 10c6554276
4 changed files with 49 additions and 25 deletions
+36 -14
View File
@@ -7,6 +7,8 @@ interface Props {
kuppel: Kuppel;
refreshKey: number;
onEdit: (entry: LogbuchEintrag) => void;
currentUserKuerzel: string;
isAdmin?: boolean;
limit?: number;
compact?: boolean;
}
@@ -50,35 +52,51 @@ function formatTime(dt: string): string {
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, compact = false }: Props) {
export default function LogbuchList({ kuppel, refreshKey, onEdit, currentUserKuerzel, isAdmin = false, limit = 10, compact = false }: Props) {
const [entries, setEntries] = useState<LogbuchEintrag[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(0);
const [month, setMonth] = useState(compact ? '' : currentMonth());
const [loading, setLoading] = useState(true);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [error, setError] = useState('');
const [deleteErr, setDeleteErr] = useState('');
const [printEntries, setPrintEntries] = useState<LogbuchEintrag[] | null>(null);
const [search, setSearch] = useState('');
const [activeSearch, setActiveSearch] = useState('');
const printPending = useRef(false);
// Derived page: auto-resets to 0 when filter deps change — no separate setState-in-effect needed
const filterKey = `${kuppel}|${refreshKey}|${month}|${activeSearch}`;
const [pageState, setPageState] = useState({ page: 0, key: filterKey });
const page = pageState.key === filterKey ? pageState.page : 0;
// Fetch result tracked by paramsKey — loading/error are derived, not set synchronously in effect
const paramsKey = `${filterKey}|${limit}|${page}`;
const [fetchResult, setFetchResult] = useState<{ ok: boolean; forParams: string } | null>(null);
const fetchError = fetchResult?.forParams === paramsKey && !fetchResult.ok ? 'Fehler beim Laden.' : '';
const loading = !fetchError && fetchResult?.forParams !== paramsKey;
const error = fetchError || deleteErr;
useEffect(() => {
const t = setTimeout(() => setActiveSearch(search.trim()), 300);
return () => clearTimeout(t);
}, [search]);
useEffect(() => { setPage(0); }, [kuppel, refreshKey, month, activeSearch]);
useEffect(() => {
setLoading(true);
let cancelled = false;
const offset = page * limit;
const url = `/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=${limit}&offset=${offset}` +
(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); });
.then((data) => {
if (!cancelled) {
setEntries(data.entries);
setTotal(data.total);
setFetchResult({ ok: true, forParams: paramsKey });
setDeleteErr('');
}
})
.catch(() => { if (!cancelled) setFetchResult({ ok: false, forParams: paramsKey }); });
return () => { cancelled = true; };
}, [kuppel, refreshKey, limit, page, month, activeSearch]);
useEffect(() => {
@@ -109,7 +127,7 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
setEntries((prev) => prev.filter((e) => e.ID !== id));
setTotal((t) => t - 1);
} catch {
setError('Fehler beim Löschen.');
setDeleteErr('Fehler beim Löschen.');
} finally {
setDeleteId(null);
}
@@ -270,8 +288,12 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
</td>
)}
<td className={`${cell} text-center whitespace-nowrap print:hidden`}>
<button onClick={() => onEdit(e)} className="text-blue-600 hover:text-blue-800 mr-2 font-medium"></button>
<button onClick={() => setDeleteId(e.ID)} className="text-red-600 hover:text-red-800 font-medium"></button>
{(isAdmin || e.BEOs?.split(', ').includes(currentUserKuerzel)) && (
<>
<button onClick={() => onEdit(e)} className="text-blue-600 hover:text-blue-800 mr-2 font-medium"></button>
<button onClick={() => setDeleteId(e.ID)} className="text-red-600 hover:text-red-800 font-medium"></button>
</>
)}
</td>
</tr>
))}
@@ -282,13 +304,13 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
{!compact && total > limit && (
<div className="flex items-center justify-center gap-3 mt-3 print:hidden">
<button
onClick={() => setPage((p) => p - 1)}
onClick={() => setPageState({ page: page - 1, key: filterKey })}
disabled={page === 0}
className="px-3 py-1.5 text-sm rounded-lg bg-gray-200 hover:bg-gray-300 disabled:opacity-40 disabled:cursor-not-allowed"
> Zurück</button>
<span className="text-sm text-gray-600">Seite {page + 1} von {Math.ceil(total / limit)}</span>
<button
onClick={() => setPage((p) => p + 1)}
onClick={() => setPageState({ page: page + 1, key: filterKey })}
disabled={(page + 1) * limit >= total}
className="px-3 py-1.5 text-sm rounded-lg bg-gray-200 hover:bg-gray-300 disabled:opacity-40 disabled:cursor-not-allowed"
>Weiter </button>