diff --git a/ApiCore-Frontend/src/app/(app)/layout.tsx b/ApiCore-Frontend/src/app/(app)/layout.tsx
index f96422a..e24c72b 100644
--- a/ApiCore-Frontend/src/app/(app)/layout.tsx
+++ b/ApiCore-Frontend/src/app/(app)/layout.tsx
@@ -63,6 +63,14 @@ export default function AppLayout({ children }: { children: ReactNode }) {
Benutzer
) : null}
+ {user.role === "ADMIN" ? (
+
+ Audit-Log
+
+ ) : null}
diff --git a/ApiCore-Frontend/src/app/(app)/settings/audit-log/page.spec.tsx b/ApiCore-Frontend/src/app/(app)/settings/audit-log/page.spec.tsx
new file mode 100644
index 0000000..eb52f1c
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/settings/audit-log/page.spec.tsx
@@ -0,0 +1,50 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import type { ReactElement } from "react";
+import AuditLogPage from "./page";
+import { fetchAuditLog } from "@/lib/audit-log/api";
+
+vi.mock("@/lib/audit-log/api", () => ({
+ fetchAuditLog: vi.fn(),
+}));
+
+function renderWithClient(ui: ReactElement) {
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ return render(
{ui});
+}
+
+describe("AuditLogPage", () => {
+ beforeEach(() => {
+ vi.mocked(fetchAuditLog).mockReset();
+ });
+
+ it("renders the fetched audit log entries", async () => {
+ vi.mocked(fetchAuditLog).mockResolvedValueOnce([
+ {
+ id: "1",
+ entityType: "Apiary",
+ entityId: "apiary-1",
+ action: "CREATE",
+ userId: "user-1",
+ timestamp: "2026-01-01T10:00:00.000Z",
+ beforeData: null,
+ afterData: { name: "Stand A" },
+ },
+ ]);
+
+ renderWithClient(
);
+
+ expect(await screen.findByText("Apiary")).toBeInTheDocument();
+ expect(screen.getByText("apiary-1")).toBeInTheDocument();
+ expect(screen.getByText("CREATE")).toBeInTheDocument();
+ });
+
+ it("shows an error message when loading the audit log fails", async () => {
+ vi.mocked(fetchAuditLog).mockRejectedValueOnce(new Error("boom"));
+
+ renderWithClient(
);
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(/nicht geladen/i);
+ });
+});
diff --git a/ApiCore-Frontend/src/app/(app)/settings/audit-log/page.tsx b/ApiCore-Frontend/src/app/(app)/settings/audit-log/page.tsx
new file mode 100644
index 0000000..e422812
--- /dev/null
+++ b/ApiCore-Frontend/src/app/(app)/settings/audit-log/page.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { useState } from "react";
+import { useQuery } from "@tanstack/react-query";
+import { fetchAuditLog } from "@/lib/audit-log/api";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+
+export default function AuditLogPage() {
+ const [entityType, setEntityType] = useState("");
+ const [entityId, setEntityId] = useState("");
+ const [appliedFilter, setAppliedFilter] = useState({ entityType: "", entityId: "" });
+
+ const { data: entries, isLoading, isError } = useQuery({
+ queryKey: ["audit-log", appliedFilter],
+ queryFn: () =>
+ fetchAuditLog({
+ entityType: appliedFilter.entityType || undefined,
+ entityId: appliedFilter.entityId || undefined,
+ }),
+ });
+
+ return (
+
+ Audit-Log
+
+
+
+ {isLoading ? Lädt…
: null}
+ {isError ? (
+
+ Audit-Log konnte nicht geladen werden.
+
+ ) : null}
+
+ {entries ? (
+
+
+
+ Zeitpunkt
+ Entitätstyp
+ Entitäts-ID
+ Aktion
+ Benutzer
+
+
+
+ {entries.map((entry) => (
+
+ {new Date(entry.timestamp).toLocaleString("de-DE")}
+ {entry.entityType}
+ {entry.entityId}
+ {entry.action}
+ {entry.userId ?? "—"}
+
+ ))}
+
+
+ ) : null}
+
+ );
+}
diff --git a/ApiCore-Frontend/src/lib/audit-log/api.ts b/ApiCore-Frontend/src/lib/audit-log/api.ts
new file mode 100644
index 0000000..7a1f9d5
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/audit-log/api.ts
@@ -0,0 +1,10 @@
+import { apiFetch } from "@/lib/auth/api-client";
+import { AuditLogEntry, AuditLogFilter } from "./types";
+
+export function fetchAuditLog(filter: AuditLogFilter = {}): Promise
{
+ const params = new URLSearchParams();
+ if (filter.entityType) params.set("entityType", filter.entityType);
+ if (filter.entityId) params.set("entityId", filter.entityId);
+ const query = params.toString();
+ return apiFetch(`/audit-log${query ? `?${query}` : ""}`);
+}
diff --git a/ApiCore-Frontend/src/lib/audit-log/types.ts b/ApiCore-Frontend/src/lib/audit-log/types.ts
new file mode 100644
index 0000000..6028dca
--- /dev/null
+++ b/ApiCore-Frontend/src/lib/audit-log/types.ts
@@ -0,0 +1,15 @@
+export interface AuditLogEntry {
+ id: string;
+ entityType: string;
+ entityId: string;
+ action: string;
+ userId: string | null;
+ timestamp: string;
+ beforeData: unknown;
+ afterData: unknown;
+}
+
+export interface AuditLogFilter {
+ entityType?: string;
+ entityId?: string;
+}
diff --git a/TASKS.md b/TASKS.md
index 41cbd21..3c8a824 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -140,7 +140,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
- Endpunkte: `GET /api/v1/audit-log?entityType=&entityId=`
- Frontend: `/settings/audit-log` (nur Administrator)
- [x] Backend-Modul fertig, getestet, grün, committet
-- [ ] Frontend-Views fertig, getestet, grün, committet
+- [x] Frontend-Views fertig, getestet, grün, committet
### 20. Backupfähigkeit