feat(PROJ-50): Admin-Tab "DSGVO-Anfragen" implementiert
Neuer Tab im Admin-Bereich: Antrags-Liste, Formular (Adresse + Zeitraum), Detail-Dialog mit Mail-Tabelle, PDF-Export und Löschen mit Bestätigung. Rollen-Gating: admin/auditor sehen den Tab, nur admin kann anlegen/löschen. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
0552ce49e2
commit
261a95437c
@@ -35,11 +35,20 @@ import { RetentionTab } from "@/components/admin/tabs/RetentionTab";
|
||||
import { ArchivingRulesTab } from "@/components/admin/tabs/ArchivingRulesTab";
|
||||
import { QuotaTab } from "@/components/admin/tabs/QuotaTab";
|
||||
import { SMTPOutTab } from "@/components/admin/tabs/SMTPOutTab";
|
||||
import { DSGVOTab } from "@/components/admin/tabs/DSGVOTab";
|
||||
import { ResetPasswordDialog, DeleteUserDialog } from "@/components/admin/UserDialogs";
|
||||
|
||||
export default function AdminPage() {
|
||||
const { user, loading: authLoading } = useAuth("domain_admin", "/admin/login");
|
||||
const isSuperAdmin = user?.role === "superadmin";
|
||||
const canManageDSGVO =
|
||||
user?.role === "superadmin" ||
|
||||
user?.role === "domain_admin" ||
|
||||
user?.role === "admin";
|
||||
const canViewDSGVO =
|
||||
canManageDSGVO ||
|
||||
user?.role === "auditor" ||
|
||||
user?.role === "domain_auditor";
|
||||
|
||||
const dashboard = useAdminDashboard(!!user);
|
||||
const usersState = useAdminUsers();
|
||||
@@ -144,6 +153,7 @@ export default function AdminPage() {
|
||||
{isSuperAdmin && <TabsTrigger value="services">Dienste</TabsTrigger>}
|
||||
<TabsTrigger value="users">Benutzer</TabsTrigger>
|
||||
<TabsTrigger value="audit">Audit-Log</TabsTrigger>
|
||||
{canViewDSGVO && <TabsTrigger value="dsgvo">DSGVO-Anfragen</TabsTrigger>}
|
||||
<TabsTrigger value="import">Import</TabsTrigger>
|
||||
{isSuperAdmin && <TabsTrigger value="ldap" onClick={loadLDAP}>LDAP (Global)</TabsTrigger>}
|
||||
{!isSuperAdmin && user?.role === "domain_admin" && (
|
||||
@@ -234,6 +244,12 @@ export default function AdminPage() {
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{canViewDSGVO && (
|
||||
<TabsContent value="dsgvo">
|
||||
<DSGVOTab canManage={canManageDSGVO} />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="import">
|
||||
<ImportTab
|
||||
uploadDragging={upload.uploadDragging}
|
||||
|
||||
@@ -0,0 +1,532 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
getDSGVORequests,
|
||||
getDSGVORequest,
|
||||
createDSGVORequest,
|
||||
deleteDSGVODeletableMails,
|
||||
exportDSGVORequestPDF,
|
||||
type DSGVORequest,
|
||||
type DSGVOStatus,
|
||||
type DSGVOMailStatus,
|
||||
type DSGVOMailResult,
|
||||
} from "@/lib/api";
|
||||
|
||||
const STATUS_META: Record<
|
||||
DSGVOStatus,
|
||||
{ label: string; variant: "default" | "secondary" | "destructive" | "outline" }
|
||||
> = {
|
||||
open: { label: "Offen", variant: "secondary" },
|
||||
partial: { label: "Teilweise abgelehnt", variant: "outline" },
|
||||
completed: { label: "Abgeschlossen", variant: "default" },
|
||||
failed: { label: "Fehlgeschlagen", variant: "destructive" },
|
||||
};
|
||||
|
||||
const MAIL_STATUS_META: Record<
|
||||
DSGVOMailStatus,
|
||||
{ label: string; variant: "default" | "secondary" | "destructive" | "outline" }
|
||||
> = {
|
||||
rejected: { label: "Abgelehnt – Aufbewahrungspflicht", variant: "destructive" },
|
||||
deletable: { label: "Löschbar", variant: "outline" },
|
||||
deleted: { label: "Gelöscht", variant: "secondary" },
|
||||
};
|
||||
|
||||
function StatusBadge({ status }: { status: DSGVOStatus }) {
|
||||
const m = STATUS_META[status] ?? { label: status, variant: "secondary" as const };
|
||||
return <Badge variant={m.variant}>{m.label}</Badge>;
|
||||
}
|
||||
|
||||
function formatDate(iso: string | null): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function formatDay(iso: string | null): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleDateString("de-DE", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
interface DSGVOTabProps {
|
||||
/** true für admin/superadmin/domain_admin (Anlegen/Löschen), false für auditor (read-only). */
|
||||
canManage: boolean;
|
||||
}
|
||||
|
||||
export function DSGVOTab({ canManage }: DSGVOTabProps) {
|
||||
const [requests, setRequests] = useState<DSGVORequest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Neuer Antrag
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [address, setAddress] = useState("");
|
||||
const [dateFrom, setDateFrom] = useState("");
|
||||
const [dateTo, setDateTo] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [createError, setCreateError] = useState("");
|
||||
|
||||
// Detail
|
||||
const [detail, setDetail] = useState<DSGVORequest | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailError, setDetailError] = useState("");
|
||||
|
||||
// Löschen
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// Export
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
getDSGVORequests()
|
||||
.then(setRequests)
|
||||
.catch(() => setError("DSGVO-Anträge konnten nicht geladen werden"))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const openCreate = () => {
|
||||
setAddress("");
|
||||
setDateFrom("");
|
||||
setDateTo("");
|
||||
setCreateError("");
|
||||
setCreateOpen(true);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
const addr = address.trim();
|
||||
if (!addr) {
|
||||
setCreateError("E-Mail-Adresse darf nicht leer sein");
|
||||
return;
|
||||
}
|
||||
setCreating(true);
|
||||
setCreateError("");
|
||||
try {
|
||||
const created = await createDSGVORequest({
|
||||
address: addr,
|
||||
date_from: dateFrom.trim() || undefined,
|
||||
date_to: dateTo.trim() || undefined,
|
||||
});
|
||||
setCreateOpen(false);
|
||||
load();
|
||||
// Ergebnis direkt anzeigen
|
||||
setDetail(created);
|
||||
setDetailError("");
|
||||
} catch (e: unknown) {
|
||||
setCreateError(
|
||||
e instanceof Error ? e.message : "Antrag konnte nicht erstellt werden"
|
||||
);
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openDetail = async (id: number) => {
|
||||
setDetailLoading(true);
|
||||
setDetailError("");
|
||||
setDetail(null);
|
||||
try {
|
||||
const d = await getDSGVORequest(id);
|
||||
setDetail(d);
|
||||
} catch {
|
||||
setDetailError("Antrags-Detail konnte nicht geladen werden");
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async (id: number) => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const { blob, filename } = await exportDSGVORequestPDF(id);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e: unknown) {
|
||||
setDetailError(e instanceof Error ? e.message : "Export fehlgeschlagen");
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!detail) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
const updated = await deleteDSGVODeletableMails(detail.id);
|
||||
setDetail(updated);
|
||||
setDeleteConfirm(false);
|
||||
load();
|
||||
} catch (e: unknown) {
|
||||
setDetailError(e instanceof Error ? e.message : "Löschen fehlgeschlagen");
|
||||
setDeleteConfirm(false);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const detailSummary = detail?.result_summary ?? null;
|
||||
const deletableCount = detailSummary?.deletable ?? 0;
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-2">
|
||||
<div>
|
||||
<CardTitle>DSGVO-Löschanträge</CardTitle>
|
||||
<CardDescription>
|
||||
Erfasst und dokumentiert Löschersuchen (Art. 17 DSGVO) zu Mail-Inhalten.
|
||||
Mails mit aktiver gesetzlicher Aufbewahrungspflicht (GoBD) werden mit
|
||||
Begründung abgelehnt; nur Mails ohne Aufbewahrungspflicht sind löschbar.
|
||||
</CardDescription>
|
||||
</div>
|
||||
{canManage && (
|
||||
<Button size="sm" onClick={openCreate}>
|
||||
Neuer Antrag
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{error && <p className="mb-3 text-sm text-destructive">{error}</p>}
|
||||
{loading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : requests.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-muted-foreground">
|
||||
Noch keine DSGVO-Anträge erfasst.
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Datum</TableHead>
|
||||
<TableHead>E-Mail-Adresse</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Treffer gesamt</TableHead>
|
||||
<TableHead className="w-28"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{requests.map((r) => (
|
||||
<TableRow key={r.id}>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{formatDate(r.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="break-all">
|
||||
{r.requested_address}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={r.status} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{r.result_summary?.total ?? 0}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => openDetail(r.id)}
|
||||
>
|
||||
Details
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Neuer Antrag */}
|
||||
<Dialog open={createOpen} onOpenChange={(o) => { if (!o) setCreateOpen(false); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Neuer DSGVO-Löschantrag</DialogTitle>
|
||||
<DialogDescription>
|
||||
Das System durchsucht das Archiv (Mandanten-Scope) nach der Adresse als
|
||||
Absender, Empfänger oder CC und prüft pro Mail die Aufbewahrungspflicht.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dsgvo-address">E-Mail-Adresse</Label>
|
||||
<Input
|
||||
id="dsgvo-address"
|
||||
type="email"
|
||||
placeholder="betroffene.person@example.com"
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dsgvo-from">Datum von (optional)</Label>
|
||||
<Input
|
||||
id="dsgvo-from"
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dsgvo-to">Datum bis (optional)</Label>
|
||||
<Input
|
||||
id="dsgvo-to"
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Eingrenzung des Zeitraums empfohlen, wenn die Adresse in sehr vielen Mails
|
||||
vorkommt (max. 10.000 Treffer pro Antrag).
|
||||
</p>
|
||||
{createError && (
|
||||
<p className="text-sm text-destructive">{createError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateOpen(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button disabled={creating} onClick={handleCreate}>
|
||||
{creating ? "Wird verarbeitet..." : "Antrag stellen"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Detail */}
|
||||
<Dialog
|
||||
open={!!detail || detailLoading}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) {
|
||||
setDetail(null);
|
||||
setDetailError("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-h-[90vh] max-w-3xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>DSGVO-Antrag</DialogTitle>
|
||||
{detail && (
|
||||
<DialogDescription>
|
||||
{detail.requested_address} · {formatDate(detail.created_at)} ·
|
||||
bearbeitet von {detail.requested_by}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
{detailLoading ? (
|
||||
<div className="space-y-2 py-2">
|
||||
<Skeleton className="h-6 w-48" />
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</div>
|
||||
) : detail ? (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<StatusBadge status={detail.status} />
|
||||
{detailSummary && (
|
||||
<div className="flex flex-wrap gap-2 text-sm">
|
||||
<Badge variant="secondary">
|
||||
Treffer gesamt: {detailSummary.total}
|
||||
</Badge>
|
||||
<Badge variant="destructive">
|
||||
Abgelehnt: {detailSummary.rejected}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
Löschbar: {detailSummary.deletable}
|
||||
</Badge>
|
||||
{detailSummary.deleted > 0 && (
|
||||
<Badge>Gelöscht: {detailSummary.deleted}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{detailSummary?.truncated && (
|
||||
<p className="rounded-md bg-amber-50 p-2 text-xs text-amber-700 dark:bg-amber-950 dark:text-amber-400">
|
||||
Es wurden mehr als 10.000 Treffer gefunden – das Ergebnis ist
|
||||
gekürzt. Bitte den Zeitraum eingrenzen, um vollständige Ergebnisse
|
||||
zu erhalten.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{detailError && (
|
||||
<p className="text-sm text-destructive">{detailError}</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={exporting}
|
||||
onClick={() => handleExport(detail.id)}
|
||||
>
|
||||
{exporting ? "Erstelle PDF..." : "Als PDF herunterladen"}
|
||||
</Button>
|
||||
{canManage && deletableCount > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => setDeleteConfirm(true)}
|
||||
>
|
||||
Löschbare Mails jetzt löschen ({deletableCount})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{detailSummary && detailSummary.mails.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Betreff</TableHead>
|
||||
<TableHead>Datum</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Aufbewahrung bis</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{detailSummary.mails.map((m: DSGVOMailResult) => {
|
||||
const sm =
|
||||
MAIL_STATUS_META[m.status] ?? {
|
||||
label: m.status,
|
||||
variant: "secondary" as const,
|
||||
};
|
||||
return (
|
||||
<TableRow key={m.id}>
|
||||
<TableCell className="max-w-xs break-words">
|
||||
{m.subject || (
|
||||
<span className="text-muted-foreground">
|
||||
(kein Betreff)
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{formatDay(m.received_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={sm.variant} title={m.reason}>
|
||||
{sm.label}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
{formatDay(m.retain_until)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||
Keine betroffenen E-Mails – die Adresse kommt in keiner archivierten
|
||||
Mail vor. Der Vorgang ist dennoch dokumentiert.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
detailError && <p className="text-sm text-destructive">{detailError}</p>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDetail(null);
|
||||
setDetailError("");
|
||||
}}
|
||||
>
|
||||
Schließen
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Lösch-Bestätigung */}
|
||||
<Dialog open={deleteConfirm} onOpenChange={(o) => { if (!o) setDeleteConfirm(false); }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Löschbare Mails endgültig löschen?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Es werden {deletableCount} Mail(s) ohne aktive Aufbewahrungspflicht
|
||||
unwiderruflich aus dem Archiv und Index entfernt. Mails mit gesetzlicher
|
||||
Aufbewahrungspflicht bleiben unberührt. Jede Löschung wird im Audit-Log
|
||||
protokolliert. Dieser Schritt kann nicht rückgängig gemacht werden.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDeleteConfirm(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button variant="destructive" disabled={deleting} onClick={handleDelete}>
|
||||
{deleting ? "Lösche..." : "Endgültig löschen"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { API_BASE, request } from "./core";
|
||||
|
||||
// PROJ-50: DSGVO-Löschersuchen für Mail-Inhalte (GoBD-Vorrang).
|
||||
// Endpoints unter /api/admin/dsgvo. Zugriff: admin/superadmin/domain_admin (RW),
|
||||
// auditor/domain_auditor (read-only).
|
||||
|
||||
export type DSGVOStatus = "open" | "partial" | "completed" | "failed";
|
||||
|
||||
export type DSGVOMailStatus = "rejected" | "deletable" | "deleted";
|
||||
|
||||
export interface DSGVOMailResult {
|
||||
id: string;
|
||||
subject: string;
|
||||
received_at: string;
|
||||
status: DSGVOMailStatus;
|
||||
reason: string;
|
||||
retain_until: string | null;
|
||||
}
|
||||
|
||||
export interface DSGVOResultSummary {
|
||||
total: number;
|
||||
rejected: number;
|
||||
deletable: number;
|
||||
deleted: number;
|
||||
truncated: boolean;
|
||||
mails: DSGVOMailResult[];
|
||||
}
|
||||
|
||||
export interface DSGVORequest {
|
||||
id: number;
|
||||
tenant_id: number | null;
|
||||
requested_address: string;
|
||||
requested_by: string;
|
||||
created_at: string;
|
||||
status: DSGVOStatus;
|
||||
result_summary: DSGVOResultSummary | null;
|
||||
}
|
||||
|
||||
export interface CreateDSGVORequestInput {
|
||||
address: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}
|
||||
|
||||
export async function getDSGVORequests(): Promise<DSGVORequest[]> {
|
||||
const data = await request<DSGVORequest[] | null>("/api/admin/dsgvo");
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
export async function getDSGVORequest(id: number): Promise<DSGVORequest> {
|
||||
return request<DSGVORequest>(`/api/admin/dsgvo/${id}`);
|
||||
}
|
||||
|
||||
export async function createDSGVORequest(
|
||||
input: CreateDSGVORequestInput
|
||||
): Promise<DSGVORequest> {
|
||||
return request<DSGVORequest>("/api/admin/dsgvo", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteDSGVODeletableMails(
|
||||
id: number
|
||||
): Promise<DSGVORequest> {
|
||||
return request<DSGVORequest>(`/api/admin/dsgvo/${id}/delete`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export async function exportDSGVORequestPDF(
|
||||
id: number
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const res = await fetch(`${API_BASE}/api/admin/dsgvo/${id}/export`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error("PDF-Export fehlgeschlagen");
|
||||
const blob = await res.blob();
|
||||
const cd = res.headers.get("Content-Disposition") || "";
|
||||
const filename =
|
||||
cd.match(/filename="([^"]+)"/)?.[1] || `dsgvo-antrag-${id}.pdf`;
|
||||
return { blob, filename };
|
||||
}
|
||||
@@ -184,3 +184,19 @@ export {
|
||||
createSavedSearch,
|
||||
deleteSavedSearch,
|
||||
} from "./saved_searches";
|
||||
|
||||
export type {
|
||||
DSGVOStatus,
|
||||
DSGVOMailStatus,
|
||||
DSGVOMailResult,
|
||||
DSGVOResultSummary,
|
||||
DSGVORequest,
|
||||
CreateDSGVORequestInput,
|
||||
} from "./dsgvo";
|
||||
export {
|
||||
getDSGVORequests,
|
||||
getDSGVORequest,
|
||||
createDSGVORequest,
|
||||
deleteDSGVODeletableMails,
|
||||
exportDSGVORequestPDF,
|
||||
} from "./dsgvo";
|
||||
|
||||
Reference in New Issue
Block a user