fix(PROJ-50): DSGVO-Tab lädt nicht — API-Contract-Mismatch Frontend/Backend

Backend liefert bei GET /api/admin/dsgvo ein Objekt {"requests":[...]},
Frontend erwartete rohes Array → .map() auf Nicht-Array warf Exception,
angezeigt als "DSGVO-Anträge konnten nicht geladen werden".

Zusätzlich stimmten DSGVOResultSummary/DSGVOMailResult-Feldnamen nicht mit
dem Go-JSON überein (total vs. total_hits; id/status/received_at existieren
im Backend nicht, stattdessen mail_id/deletable/deleted/date). Status wird
jetzt im Frontend aus deletable/deleted abgeleitet (mailStatus()).

Bug bestand seit Feature-Implementierung (2026-06-13), fiel erst jetzt beim
ersten Live-Test auf.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-07-03 23:06:14 +02:00
co-authored by Claude Sonnet 5
parent e91206c2b5
commit 804cd62201
2 changed files with 25 additions and 13 deletions
+13 -6
View File
@@ -60,6 +60,12 @@ const MAIL_STATUS_META: Record<
deleted: { label: "Gelöscht", variant: "secondary" }, deleted: { label: "Gelöscht", variant: "secondary" },
}; };
function mailStatus(m: DSGVOMailResult): DSGVOMailStatus {
if (m.deleted) return "deleted";
if (m.deletable) return "deletable";
return "rejected";
}
function StatusBadge({ status }: { status: DSGVOStatus }) { function StatusBadge({ status }: { status: DSGVOStatus }) {
const m = STATUS_META[status] ?? { label: status, variant: "secondary" as const }; const m = STATUS_META[status] ?? { label: status, variant: "secondary" as const };
return <Badge variant={m.variant}>{m.label}</Badge>; return <Badge variant={m.variant}>{m.label}</Badge>;
@@ -274,7 +280,7 @@ export function DSGVOTab({ canManage }: DSGVOTabProps) {
<StatusBadge status={r.status} /> <StatusBadge status={r.status} />
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{r.result_summary?.total ?? 0} {r.result_summary?.total_hits ?? 0}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <Button
@@ -389,7 +395,7 @@ export function DSGVOTab({ canManage }: DSGVOTabProps) {
{detailSummary && ( {detailSummary && (
<div className="flex flex-wrap gap-2 text-sm"> <div className="flex flex-wrap gap-2 text-sm">
<Badge variant="secondary"> <Badge variant="secondary">
Treffer gesamt: {detailSummary.total} Treffer gesamt: {detailSummary.total_hits}
</Badge> </Badge>
<Badge variant="destructive"> <Badge variant="destructive">
Abgelehnt: {detailSummary.rejected} Abgelehnt: {detailSummary.rejected}
@@ -449,13 +455,14 @@ export function DSGVOTab({ canManage }: DSGVOTabProps) {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{detailSummary.mails.map((m: DSGVOMailResult) => { {detailSummary.mails.map((m: DSGVOMailResult) => {
const status = mailStatus(m);
const sm = const sm =
MAIL_STATUS_META[m.status] ?? { MAIL_STATUS_META[status] ?? {
label: m.status, label: status,
variant: "secondary" as const, variant: "secondary" as const,
}; };
return ( return (
<TableRow key={m.id}> <TableRow key={m.mail_id}>
<TableCell className="max-w-xs break-words"> <TableCell className="max-w-xs break-words">
{m.subject || ( {m.subject || (
<span className="text-muted-foreground"> <span className="text-muted-foreground">
@@ -464,7 +471,7 @@ export function DSGVOTab({ canManage }: DSGVOTabProps) {
)} )}
</TableCell> </TableCell>
<TableCell className="whitespace-nowrap"> <TableCell className="whitespace-nowrap">
{formatDay(m.received_at)} {formatDay(m.date)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant={sm.variant} title={m.reason}> <Badge variant={sm.variant} title={m.reason}>
+12 -7
View File
@@ -8,17 +8,22 @@ export type DSGVOStatus = "open" | "partial" | "completed" | "failed";
export type DSGVOMailStatus = "rejected" | "deletable" | "deleted"; export type DSGVOMailStatus = "rejected" | "deletable" | "deleted";
// Feldnamen 1:1 zu internal/storage/dsgvo_requests.go (DSGVOAffectedMail).
// Es gibt kein "status"-Feld im Backend — der Status wird aus
// deletable/deleted abgeleitet (siehe mailStatus() im Tab).
export interface DSGVOMailResult { export interface DSGVOMailResult {
id: string; mail_id: string;
subject: string; subject: string;
received_at: string; date: string | null;
status: DSGVOMailStatus;
reason: string;
retain_until: string | null; retain_until: string | null;
deletable: boolean;
deleted: boolean;
reason?: string;
} }
// Feldnamen 1:1 zu internal/storage/dsgvo_requests.go (DSGVOResultSummary).
export interface DSGVOResultSummary { export interface DSGVOResultSummary {
total: number; total_hits: number;
rejected: number; rejected: number;
deletable: number; deletable: number;
deleted: number; deleted: number;
@@ -43,8 +48,8 @@ export interface CreateDSGVORequestInput {
} }
export async function getDSGVORequests(): Promise<DSGVORequest[]> { export async function getDSGVORequests(): Promise<DSGVORequest[]> {
const data = await request<DSGVORequest[] | null>("/api/admin/dsgvo"); const data = await request<{ requests: DSGVORequest[] | null }>("/api/admin/dsgvo");
return data ?? []; return data.requests ?? [];
} }
export async function getDSGVORequest(id: number): Promise<DSGVORequest> { export async function getDSGVORequest(id: number): Promise<DSGVORequest> {