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:
@@ -139,6 +139,8 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
|
|||||||
kuppel={activeKuppel}
|
kuppel={activeKuppel}
|
||||||
refreshKey={refreshKey}
|
refreshKey={refreshKey}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
|
currentUserKuerzel={kuerzel}
|
||||||
|
isAdmin={role?.includes('admin') ?? false}
|
||||||
limit={5}
|
limit={5}
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
@@ -158,6 +160,8 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
|
|||||||
kuppel={activeKuppel}
|
kuppel={activeKuppel}
|
||||||
refreshKey={refreshKey}
|
refreshKey={refreshKey}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
|
currentUserKuerzel={kuerzel}
|
||||||
|
isAdmin={role?.includes('admin') ?? false}
|
||||||
limit={15}
|
limit={15}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,17 +11,16 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|||||||
const logbuchId = parseInt(id);
|
const logbuchId = parseInt(id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Zugriffskontrolle: Nur Ersteller oder Admin dürfen ändern
|
const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[];
|
||||||
const existingRows = await query('SELECT created_by FROM logbuch WHERE ID = ?', [logbuchId]) as { created_by: number }[];
|
|
||||||
if (existingRows.length === 0) {
|
if (existingRows.length === 0) {
|
||||||
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = session.role?.includes('admin');
|
const isAdmin = session.role?.includes('admin');
|
||||||
const createdBy = existingRows[0].created_by;
|
const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[];
|
||||||
const isCreator = createdBy === null || createdBy === session.beoId;
|
const isBeo = (beoRows[0]?.cnt ?? 0) > 0;
|
||||||
|
|
||||||
if (!isAdmin && !isCreator) {
|
if (!isAdmin && !isBeo) {
|
||||||
return NextResponse.json({ error: 'Keine Berechtigung zum Ändern dieses Eintrags' }, { status: 403 });
|
return NextResponse.json({ error: 'Keine Berechtigung zum Ändern dieses Eintrags' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,17 +83,16 @@ export async function DELETE(_request: NextRequest, { params }: { params: Promis
|
|||||||
const logbuchId = parseInt(id);
|
const logbuchId = parseInt(id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Zugriffskontrolle: Nur Ersteller oder Admin dürfen löschen
|
const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[];
|
||||||
const existingRows = await query('SELECT created_by FROM logbuch WHERE ID = ?', [logbuchId]) as { created_by: number }[];
|
|
||||||
if (existingRows.length === 0) {
|
if (existingRows.length === 0) {
|
||||||
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = session.role?.includes('admin');
|
const isAdmin = session.role?.includes('admin');
|
||||||
const createdBy = existingRows[0].created_by;
|
const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[];
|
||||||
const isCreator = createdBy === null || createdBy === session.beoId;
|
const isBeo = (beoRows[0]?.cnt ?? 0) > 0;
|
||||||
|
|
||||||
if (!isAdmin && !isCreator) {
|
if (!isAdmin && !isBeo) {
|
||||||
return NextResponse.json({ error: 'Keine Berechtigung zum Löschen dieses Eintrags' }, { status: 403 });
|
return NextResponse.json({ error: 'Keine Berechtigung zum Löschen dieses Eintrags' }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-14
@@ -7,6 +7,8 @@ interface Props {
|
|||||||
kuppel: Kuppel;
|
kuppel: Kuppel;
|
||||||
refreshKey: number;
|
refreshKey: number;
|
||||||
onEdit: (entry: LogbuchEintrag) => void;
|
onEdit: (entry: LogbuchEintrag) => void;
|
||||||
|
currentUserKuerzel: string;
|
||||||
|
isAdmin?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
@@ -50,35 +52,51 @@ function formatTime(dt: string): string {
|
|||||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
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 [entries, setEntries] = useState<LogbuchEintrag[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [month, setMonth] = useState(compact ? '' : currentMonth());
|
const [month, setMonth] = useState(compact ? '' : currentMonth());
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
const [error, setError] = useState('');
|
const [deleteErr, setDeleteErr] = useState('');
|
||||||
const [printEntries, setPrintEntries] = useState<LogbuchEintrag[] | null>(null);
|
const [printEntries, setPrintEntries] = useState<LogbuchEintrag[] | null>(null);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [activeSearch, setActiveSearch] = useState('');
|
const [activeSearch, setActiveSearch] = useState('');
|
||||||
const printPending = useRef(false);
|
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(() => {
|
useEffect(() => {
|
||||||
const t = setTimeout(() => setActiveSearch(search.trim()), 300);
|
const t = setTimeout(() => setActiveSearch(search.trim()), 300);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
useEffect(() => { setPage(0); }, [kuppel, refreshKey, month, activeSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
let cancelled = false;
|
||||||
const offset = page * limit;
|
const offset = page * limit;
|
||||||
const url = `/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=${limit}&offset=${offset}` +
|
const url = `/api/logbuch?kuppel=${encodeURIComponent(kuppel)}&limit=${limit}&offset=${offset}` +
|
||||||
(activeSearch ? `&search=${encodeURIComponent(activeSearch)}` : (month ? `&month=${encodeURIComponent(month)}` : ''));
|
(activeSearch ? `&search=${encodeURIComponent(activeSearch)}` : (month ? `&month=${encodeURIComponent(month)}` : ''));
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
.then((r) => { if (!r.ok) throw new Error(); return r.json(); })
|
||||||
.then((data) => { setEntries(data.entries); setTotal(data.total); setLoading(false); })
|
.then((data) => {
|
||||||
.catch(() => { setError('Fehler beim Laden.'); setLoading(false); });
|
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]);
|
}, [kuppel, refreshKey, limit, page, month, activeSearch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -109,7 +127,7 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
|||||||
setEntries((prev) => prev.filter((e) => e.ID !== id));
|
setEntries((prev) => prev.filter((e) => e.ID !== id));
|
||||||
setTotal((t) => t - 1);
|
setTotal((t) => t - 1);
|
||||||
} catch {
|
} catch {
|
||||||
setError('Fehler beim Löschen.');
|
setDeleteErr('Fehler beim Löschen.');
|
||||||
} finally {
|
} finally {
|
||||||
setDeleteId(null);
|
setDeleteId(null);
|
||||||
}
|
}
|
||||||
@@ -270,8 +288,12 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
|||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td className={`${cell} text-center whitespace-nowrap print:hidden`}>
|
<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>
|
{(isAdmin || e.BEOs?.split(', ').includes(currentUserKuerzel)) && (
|
||||||
<button onClick={() => setDeleteId(e.ID)} className="text-red-600 hover:text-red-800 font-medium">✕</button>
|
<>
|
||||||
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -282,13 +304,13 @@ export default function LogbuchList({ kuppel, refreshKey, onEdit, limit = 10, co
|
|||||||
{!compact && total > limit && (
|
{!compact && total > limit && (
|
||||||
<div className="flex items-center justify-center gap-3 mt-3 print:hidden">
|
<div className="flex items-center justify-center gap-3 mt-3 print:hidden">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => p - 1)}
|
onClick={() => setPageState({ page: page - 1, key: filterKey })}
|
||||||
disabled={page === 0}
|
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"
|
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>
|
>← Zurück</button>
|
||||||
<span className="text-sm text-gray-600">Seite {page + 1} von {Math.ceil(total / limit)}</span>
|
<span className="text-sm text-gray-600">Seite {page + 1} von {Math.ceil(total / limit)}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => p + 1)}
|
onClick={() => setPageState({ page: page + 1, key: filterKey })}
|
||||||
disabled={(page + 1) * limit >= total}
|
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"
|
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>
|
>Weiter →</button>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "logbuch",
|
"name": "logbuch",
|
||||||
"version": "1.7.5",
|
"version": "1.7.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user