feat: add DSAR administration
This commit is contained in:
+159
-1
@@ -1,5 +1,5 @@
|
||||
import type { ApiSettings, DeltaDeletedItem, PrivacyRetentionPolicy } from "@govoplan/core-webui";
|
||||
import { apiFetch, apiPath, apiQuery } from "@govoplan/core-webui";
|
||||
import { apiDownload, apiFetch, apiPath, apiQuery } from "@govoplan/core-webui";
|
||||
export { fetchAdminOverview, fetchPermissionCatalog, fetchTenants } from "@govoplan/core-webui";
|
||||
export type {
|
||||
AdminOverview,
|
||||
@@ -565,6 +565,102 @@ export type GovernanceTemplateItem = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type DataSubjectSelector = {
|
||||
account_id?: string | null;
|
||||
identity_id?: string | null;
|
||||
membership_id?: string | null;
|
||||
email?: string | null;
|
||||
external_references?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type DataSubjectRequestSummary = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
reference: string;
|
||||
request_kind: "access" | "erasure" | "access_and_erasure";
|
||||
status: string;
|
||||
subject: DataSubjectSelector;
|
||||
purpose: string;
|
||||
legal_basis?: string | null;
|
||||
due_at?: string | null;
|
||||
requested_by_account_id: string;
|
||||
record_count: number;
|
||||
executable_action_count: number;
|
||||
coverage: {
|
||||
covered_modules?: string[];
|
||||
modules_without_provider?: string[];
|
||||
provider_discovery_failures?: Array<Record<string, unknown>>;
|
||||
};
|
||||
evidence_sha256?: string | null;
|
||||
resource_revision: number;
|
||||
etag: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
completed_at?: string | null;
|
||||
notes?: string | null;
|
||||
};
|
||||
|
||||
export type DataSubjectRecord = {
|
||||
provider_id: string;
|
||||
module_id: string;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
category: string;
|
||||
title: string;
|
||||
data: Record<string, unknown>;
|
||||
observed_at?: string | null;
|
||||
immutable_evidence: boolean;
|
||||
retention_reason?: string | null;
|
||||
source_path?: string | null;
|
||||
};
|
||||
|
||||
export type DataSubjectErasureAction = {
|
||||
action_id: string;
|
||||
provider_id: string;
|
||||
module_id: string;
|
||||
kind: string;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
title: string;
|
||||
rationale: string;
|
||||
executable: boolean;
|
||||
irreversible: boolean;
|
||||
metadata: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DataSubjectRequestDetail = {
|
||||
request: DataSubjectRequestSummary;
|
||||
search: {
|
||||
records?: DataSubjectRecord[];
|
||||
provider_runs?: Array<Record<string, unknown>>;
|
||||
searched_at?: string;
|
||||
record_count?: number;
|
||||
};
|
||||
erasure_plan: {
|
||||
actions?: DataSubjectErasureAction[];
|
||||
provider_runs?: Array<Record<string, unknown>>;
|
||||
planned_at?: string;
|
||||
executable_count?: number;
|
||||
retained_count?: number;
|
||||
};
|
||||
execution: {
|
||||
results?: Array<{ action_id: string; status: string; summary: string; evidence?: Record<string, unknown> }>;
|
||||
executed_at?: string;
|
||||
successful_count?: number;
|
||||
failed_count?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type DataSubjectRequestCreatePayload = {
|
||||
reference: string;
|
||||
request_kind: DataSubjectRequestSummary["request_kind"];
|
||||
subject: DataSubjectSelector;
|
||||
purpose: string;
|
||||
legal_basis?: string | null;
|
||||
due_at?: string | null;
|
||||
notes?: string | null;
|
||||
};
|
||||
|
||||
export function fetchSystemSettings(settings: ApiSettings): Promise<SystemSettingsItem> {
|
||||
return apiFetch(settings, "/api/v1/admin/system/settings");
|
||||
}
|
||||
@@ -713,6 +809,68 @@ export function deleteGovernanceTemplate(settings: ApiSettings, templateId: stri
|
||||
return apiFetch(settings, apiPath(`/api/v1/admin/system/governance-templates/${templateId}`, { change_request_id: changeRequestId }), { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function fetchDataSubjectRequests(settings: ApiSettings): Promise<DataSubjectRequestSummary[]> {
|
||||
const response = await apiFetch<{ items: DataSubjectRequestSummary[] }>(settings, "/api/v1/admin/privacy/data-subject-requests");
|
||||
return response.items;
|
||||
}
|
||||
|
||||
export function fetchDataSubjectRequest(settings: ApiSettings, requestId: string): Promise<DataSubjectRequestDetail> {
|
||||
return apiFetch(settings, `/api/v1/admin/privacy/data-subject-requests/${encodeURIComponent(requestId)}`);
|
||||
}
|
||||
|
||||
export function createDataSubjectRequest(settings: ApiSettings, payload: DataSubjectRequestCreatePayload): Promise<DataSubjectRequestDetail> {
|
||||
return apiFetch(settings, "/api/v1/admin/privacy/data-subject-requests", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function searchDataSubjectRequest(settings: ApiSettings, request: DataSubjectRequestSummary): Promise<DataSubjectRequestDetail> {
|
||||
return mutateDataSubjectRequest(settings, request, "search", {});
|
||||
}
|
||||
|
||||
export function planDataSubjectErasure(settings: ApiSettings, request: DataSubjectRequestSummary): Promise<DataSubjectRequestDetail> {
|
||||
return mutateDataSubjectRequest(settings, request, "erasure-plan", {});
|
||||
}
|
||||
|
||||
export function executeDataSubjectErasure(
|
||||
settings: ApiSettings,
|
||||
request: DataSubjectRequestSummary,
|
||||
actionIds: string[],
|
||||
confirmation: string
|
||||
): Promise<DataSubjectRequestDetail> {
|
||||
return mutateDataSubjectRequest(settings, request, "execute", {
|
||||
action_ids: actionIds,
|
||||
confirmation
|
||||
});
|
||||
}
|
||||
|
||||
export function exportDataSubjectRequest(settings: ApiSettings, request: DataSubjectRequestSummary): Promise<void> {
|
||||
const safeReference = request.reference.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
return apiDownload(
|
||||
settings,
|
||||
`/api/v1/admin/privacy/data-subject-requests/${encodeURIComponent(request.id)}/export`,
|
||||
`dsar-${safeReference || request.id}.json`
|
||||
);
|
||||
}
|
||||
|
||||
function mutateDataSubjectRequest(
|
||||
settings: ApiSettings,
|
||||
request: DataSubjectRequestSummary,
|
||||
operation: string,
|
||||
payload: Record<string, unknown>
|
||||
): Promise<DataSubjectRequestDetail> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/admin/privacy/data-subject-requests/${encodeURIComponent(request.id)}/${operation}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "If-Match": request.etag },
|
||||
body: JSON.stringify({ base_revision: request.resource_revision, ...payload })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchConfigurationChanges(settings: ApiSettings): Promise<{ requests: ConfigurationChangeRequest[]; history: ConfigurationChangeRecord[] }> {
|
||||
return apiFetch(settings, "/api/v1/admin/configuration-changes");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Download, Eye, Play, Plus, RefreshCw, Search, ShieldCheck } from "lucide-react";
|
||||
import type { ApiSettings, DataGridColumn } from "@govoplan/core-webui";
|
||||
import {
|
||||
AdminIconButton,
|
||||
AdminPageLayout,
|
||||
Button,
|
||||
Card,
|
||||
DataGrid,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
MetricCard,
|
||||
StatusBadge,
|
||||
TableActionGroup,
|
||||
adminErrorMessage,
|
||||
formatDateTime
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
createDataSubjectRequest,
|
||||
executeDataSubjectErasure,
|
||||
exportDataSubjectRequest,
|
||||
fetchDataSubjectRequest,
|
||||
fetchDataSubjectRequests,
|
||||
planDataSubjectErasure,
|
||||
searchDataSubjectRequest,
|
||||
type DataSubjectErasureAction,
|
||||
type DataSubjectRecord,
|
||||
type DataSubjectRequestCreatePayload,
|
||||
type DataSubjectRequestDetail,
|
||||
type DataSubjectRequestSummary
|
||||
} from "../../api/admin";
|
||||
|
||||
const I18N = {
|
||||
title: "i18n:govoplan-admin.data_subject_requests.ds001",
|
||||
description: "i18n:govoplan-admin.data_subject_requests.ds002",
|
||||
create: "i18n:govoplan-admin.data_subject_requests.ds003",
|
||||
reload: "i18n:govoplan-admin.data_subject_requests.ds004",
|
||||
reference: "i18n:govoplan-admin.data_subject_requests.ds005",
|
||||
kindLabel: "i18n:govoplan-admin.data_subject_requests.ds006",
|
||||
statusLabel: "i18n:govoplan-admin.data_subject_requests.ds007",
|
||||
subject: "i18n:govoplan-admin.data_subject_requests.ds008",
|
||||
due: "i18n:govoplan-admin.data_subject_requests.ds009",
|
||||
actions: "i18n:govoplan-admin.data_subject_requests.ds010",
|
||||
inspect: "i18n:govoplan-admin.data_subject_requests.ds011",
|
||||
noRequests: "i18n:govoplan-admin.data_subject_requests.ds012",
|
||||
records: "i18n:govoplan-admin.data_subject_requests.ds013",
|
||||
executable: "i18n:govoplan-admin.data_subject_requests.ds014",
|
||||
uncovered: "i18n:govoplan-admin.data_subject_requests.ds015",
|
||||
runSearch: "i18n:govoplan-admin.data_subject_requests.ds016",
|
||||
plan: "i18n:govoplan-admin.data_subject_requests.ds017",
|
||||
export: "i18n:govoplan-admin.data_subject_requests.ds018",
|
||||
execute: "i18n:govoplan-admin.data_subject_requests.ds019",
|
||||
collectedRecords: "i18n:govoplan-admin.data_subject_requests.ds020",
|
||||
erasurePlan: "i18n:govoplan-admin.data_subject_requests.ds021",
|
||||
coverage: "i18n:govoplan-admin.data_subject_requests.ds022",
|
||||
noSelection: "i18n:govoplan-admin.data_subject_requests.ds023",
|
||||
provider: "i18n:govoplan-admin.data_subject_requests.ds024",
|
||||
category: "i18n:govoplan-admin.data_subject_requests.ds025",
|
||||
evidence: "i18n:govoplan-admin.data_subject_requests.ds026",
|
||||
decision: "i18n:govoplan-admin.data_subject_requests.ds027",
|
||||
rationale: "i18n:govoplan-admin.data_subject_requests.ds028",
|
||||
select: "i18n:govoplan-admin.data_subject_requests.ds029",
|
||||
createTitle: "i18n:govoplan-admin.data_subject_requests.ds030",
|
||||
purpose: "i18n:govoplan-admin.data_subject_requests.ds031",
|
||||
legalBasis: "i18n:govoplan-admin.data_subject_requests.ds032",
|
||||
email: "i18n:govoplan-admin.data_subject_requests.ds033",
|
||||
accountIdLabel: "i18n:govoplan-admin.data_subject_requests.ds034",
|
||||
membershipIdLabel: "i18n:govoplan-admin.data_subject_requests.ds035",
|
||||
identityIdLabel: "i18n:govoplan-admin.data_subject_requests.ds036",
|
||||
notes: "i18n:govoplan-admin.data_subject_requests.ds037",
|
||||
save: "i18n:govoplan-admin.data_subject_requests.ds038",
|
||||
cancel: "i18n:govoplan-admin.data_subject_requests.ds039",
|
||||
executeTitle: "i18n:govoplan-admin.data_subject_requests.ds040",
|
||||
executeWarning: "i18n:govoplan-admin.data_subject_requests.ds041",
|
||||
confirmation: "i18n:govoplan-admin.data_subject_requests.ds042",
|
||||
confirmationHelp: "i18n:govoplan-admin.data_subject_requests.ds043",
|
||||
missingSelector: "i18n:govoplan-admin.data_subject_requests.ds044",
|
||||
providerGap: "i18n:govoplan-admin.data_subject_requests.ds045",
|
||||
created: "i18n:govoplan-admin.data_subject_requests.ds046",
|
||||
searchComplete: "i18n:govoplan-admin.data_subject_requests.ds047",
|
||||
planComplete: "i18n:govoplan-admin.data_subject_requests.ds048",
|
||||
executionComplete: "i18n:govoplan-admin.data_subject_requests.ds049",
|
||||
exportComplete: "i18n:govoplan-admin.data_subject_requests.ds050",
|
||||
accessRequest: "i18n:govoplan-admin.data_subject_requests.ds051",
|
||||
erasureRequest: "i18n:govoplan-admin.data_subject_requests.ds052",
|
||||
combinedRequest: "i18n:govoplan-admin.data_subject_requests.ds053",
|
||||
mutable: "i18n:govoplan-admin.data_subject_requests.ds054",
|
||||
retainedEvidence: "i18n:govoplan-admin.data_subject_requests.ds055",
|
||||
irreversible: "i18n:govoplan-admin.data_subject_requests.ds056",
|
||||
executableState: "i18n:govoplan-admin.data_subject_requests.ds057",
|
||||
retainedReview: "i18n:govoplan-admin.data_subject_requests.ds058",
|
||||
externalReferences: "i18n:govoplan-admin.data_subject_requests.ds059",
|
||||
externalReferencesHelp: "i18n:govoplan-admin.data_subject_requests.ds060",
|
||||
invalidExternalReferences: "i18n:govoplan-admin.data_subject_requests.ds061"
|
||||
} as const;
|
||||
|
||||
type Draft = {
|
||||
reference: string;
|
||||
requestKind: DataSubjectRequestSummary["request_kind"];
|
||||
email: string;
|
||||
accountId: string;
|
||||
membershipId: string;
|
||||
identityId: string;
|
||||
externalReferences: string;
|
||||
purpose: string;
|
||||
legalBasis: string;
|
||||
dueDate: string;
|
||||
notes: string;
|
||||
};
|
||||
|
||||
const emptyDraft: Draft = {
|
||||
reference: "",
|
||||
requestKind: "access_and_erasure",
|
||||
email: "",
|
||||
accountId: "",
|
||||
membershipId: "",
|
||||
identityId: "",
|
||||
externalReferences: "",
|
||||
purpose: "",
|
||||
legalBasis: "",
|
||||
dueDate: "",
|
||||
notes: ""
|
||||
};
|
||||
|
||||
export default function DataSubjectRequestsPanel({
|
||||
settings,
|
||||
canManage,
|
||||
canExport,
|
||||
canErase
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
canManage: boolean;
|
||||
canExport: boolean;
|
||||
canErase: boolean;
|
||||
}) {
|
||||
const [items, setItems] = useState<DataSubjectRequestSummary[]>([]);
|
||||
const [selected, setSelected] = useState<DataSubjectRequestDetail | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [draft, setDraft] = useState<Draft>(emptyDraft);
|
||||
const [selectedActions, setSelectedActions] = useState<string[]>([]);
|
||||
const [confirmation, setConfirmation] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
async function load(preferredId?: string) {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const next = await fetchDataSubjectRequests(settings);
|
||||
setItems(next);
|
||||
const requestId = preferredId ?? selected?.request.id;
|
||||
if (requestId && next.some((item) => item.id === requestId)) {
|
||||
setSelected(await fetchDataSubjectRequest(settings, requestId));
|
||||
} else if (selected && !next.some((item) => item.id === selected.request.id)) {
|
||||
setSelected(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [settings.accessToken, settings.apiBaseUrl]);
|
||||
|
||||
async function inspect(item: DataSubjectRequestSummary) {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const detail = await fetchDataSubjectRequest(settings, item.id);
|
||||
setSelected(detail);
|
||||
setSelectedActions(defaultActionSelection(detail));
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function create() {
|
||||
if (![draft.email, draft.accountId, draft.membershipId, draft.identityId, draft.externalReferences].some((value) => value.trim())) {
|
||||
setError(I18N.missingSelector);
|
||||
return;
|
||||
}
|
||||
if (!validExternalReferenceLines(draft.externalReferences)) {
|
||||
setError(I18N.invalidExternalReferences);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const detail = await createDataSubjectRequest(settings, createPayload(draft));
|
||||
setCreating(false);
|
||||
setDraft(emptyDraft);
|
||||
setSelected(detail);
|
||||
setSuccess(I18N.created);
|
||||
await load(detail.request.id);
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function mutate(
|
||||
operation: (request: DataSubjectRequestSummary) => Promise<DataSubjectRequestDetail>,
|
||||
message: string
|
||||
): Promise<boolean> {
|
||||
if (!selected) return false;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
const detail = await operation(selected.request);
|
||||
setSelected(detail);
|
||||
setSelectedActions(defaultActionSelection(detail));
|
||||
setSuccess(message);
|
||||
await load(detail.request.id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function download() {
|
||||
if (!selected) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await exportDataSubjectRequest(settings, selected.request);
|
||||
setSuccess(I18N.exportComplete);
|
||||
} catch (err) {
|
||||
setError(adminErrorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function execute() {
|
||||
if (!selected) return;
|
||||
const succeeded = await mutate(
|
||||
(request) => executeDataSubjectErasure(settings, request, selectedActions, confirmation),
|
||||
I18N.executionComplete
|
||||
);
|
||||
if (succeeded) {
|
||||
setExecuting(false);
|
||||
setConfirmation("");
|
||||
}
|
||||
}
|
||||
|
||||
const requestColumns = useMemo<DataGridColumn<DataSubjectRequestSummary>[]>(() => [
|
||||
{
|
||||
id: "reference",
|
||||
header: I18N.reference,
|
||||
width: "minmax(180px, 1fr)",
|
||||
minWidth: 160,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
value: (row) => `${row.reference} ${subjectLabel(row)}`,
|
||||
render: (row) => <div><strong>{row.reference}</strong><span className="muted block">{subjectLabel(row)}</span></div>
|
||||
},
|
||||
{ id: "kind", header: I18N.kindLabel, width: 170, sortable: true, filterable: true, value: (row) => row.request_kind },
|
||||
{ id: "status", header: I18N.statusLabel, width: 150, sortable: true, filterable: true, value: (row) => row.status, render: (row) => <StatusBadge status={statusTone(row.status)} label={row.status} /> },
|
||||
{ id: "due", header: I18N.due, width: 170, sortable: true, value: (row) => row.due_at ?? "", render: (row) => row.due_at ? formatDateTime(row.due_at) : "-" },
|
||||
{
|
||||
id: "actions",
|
||||
header: I18N.actions,
|
||||
width: 72,
|
||||
sticky: "end",
|
||||
align: "right",
|
||||
render: (row) => <TableActionGroup actions={[{
|
||||
id: "inspect",
|
||||
label: I18N.inspect,
|
||||
icon: <Eye size={16} />,
|
||||
disabled: busy,
|
||||
onClick: () => void inspect(row)
|
||||
}]} />
|
||||
}
|
||||
], [busy]);
|
||||
|
||||
const recordColumns = useMemo<DataGridColumn<DataSubjectRecord>[]>(() => [
|
||||
{ id: "title", header: I18N.records, width: "minmax(220px, 1.3fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (row) => `${row.title} ${row.resource_type}`, render: (row) => <div><strong>{row.title}</strong><span className="muted block">{row.resource_type}</span></div> },
|
||||
{ id: "provider", header: I18N.provider, width: 140, sortable: true, filterable: true, value: (row) => row.module_id },
|
||||
{ id: "category", header: I18N.category, width: 170, sortable: true, filterable: true, value: (row) => row.category },
|
||||
{ id: "evidence", header: I18N.evidence, width: 190, filterable: true, value: (row) => row.immutable_evidence ? row.retention_reason || "retained" : "mutable", render: (row) => row.immutable_evidence ? <StatusBadge status="warning" label={row.retention_reason || I18N.retainedEvidence} /> : <StatusBadge status="active" label={I18N.mutable} /> }
|
||||
], []);
|
||||
|
||||
const actionColumns = useMemo<DataGridColumn<DataSubjectErasureAction>[]>(() => [
|
||||
{
|
||||
id: "select",
|
||||
header: I18N.select,
|
||||
width: 72,
|
||||
render: (row) => <input
|
||||
type="checkbox"
|
||||
checked={selectedActions.includes(row.action_id)}
|
||||
disabled={!row.executable || busy}
|
||||
aria-label={`${I18N.select}: ${row.title}`}
|
||||
onChange={(event) => setSelectedActions((current) => event.target.checked ? [...current, row.action_id] : current.filter((id) => id !== row.action_id))}
|
||||
/>
|
||||
},
|
||||
{ id: "action", header: I18N.decision, width: "minmax(220px, 1fr)", minWidth: 190, resizable: true, sortable: true, filterable: true, value: (row) => `${row.title} ${row.kind}`, render: (row) => <div><strong>{row.title}</strong><span className="muted block">{row.kind}</span></div> },
|
||||
{ id: "provider", header: I18N.provider, width: 140, sortable: true, filterable: true, value: (row) => row.module_id },
|
||||
{ id: "rationale", header: I18N.rationale, width: "minmax(260px, 1.4fr)", minWidth: 220, resizable: true, value: (row) => row.rationale },
|
||||
{ id: "state", header: I18N.statusLabel, width: 150, value: (row) => row.executable ? "executable" : row.kind, render: (row) => <StatusBadge status={row.executable ? (row.irreversible ? "warning" : "active") : "inactive"} label={row.executable ? (row.irreversible ? I18N.irreversible : I18N.executableState) : I18N.retainedReview} /> }
|
||||
], [busy, selectedActions]);
|
||||
|
||||
const coverageGaps = selected?.request.coverage.modules_without_provider ?? [];
|
||||
const actions = selected?.erasure_plan.actions ?? [];
|
||||
const records = selected?.search.records ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdminPageLayout
|
||||
title={I18N.title}
|
||||
description={I18N.description}
|
||||
loading={loading}
|
||||
error={error}
|
||||
success={success}
|
||||
actions={<>
|
||||
<Button onClick={() => void load()} disabled={loading || busy}><RefreshCw size={16} /> {I18N.reload}</Button>
|
||||
<AdminIconButton label={I18N.create} icon={<Plus />} variant="primary" onClick={() => setCreating(true)} disabled={!canManage || busy} />
|
||||
</>}
|
||||
>
|
||||
<div className="admin-table-surface">
|
||||
<DataGrid
|
||||
id="admin-data-subject-requests"
|
||||
rows={items}
|
||||
columns={requestColumns}
|
||||
initialFit="container"
|
||||
getRowKey={(row) => row.id}
|
||||
emptyText={I18N.noRequests}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selected ? <>
|
||||
<div className="metric-grid">
|
||||
<MetricCard label={I18N.records} value={selected.request.record_count} tone="info" />
|
||||
<MetricCard label={I18N.executable} value={selected.request.executable_action_count} tone={selected.request.executable_action_count ? "warning" : "neutral"} />
|
||||
<MetricCard label={I18N.uncovered} value={coverageGaps.length} tone={coverageGaps.length ? "warning" : "good"} />
|
||||
</div>
|
||||
|
||||
<Card title={selected.request.reference}>
|
||||
<dl className="admin-details-grid">
|
||||
<div><dt>{I18N.subject}</dt><dd>{subjectLabel(selected.request)}</dd></div>
|
||||
<div><dt>{I18N.kindLabel}</dt><dd>{selected.request.request_kind}</dd></div>
|
||||
<div><dt>{I18N.statusLabel}</dt><dd><StatusBadge status={statusTone(selected.request.status)} label={selected.request.status} /></dd></div>
|
||||
<div><dt>{I18N.due}</dt><dd>{selected.request.due_at ? formatDateTime(selected.request.due_at) : "-"}</dd></div>
|
||||
<div><dt>{I18N.purpose}</dt><dd>{selected.request.purpose}</dd></div>
|
||||
<div><dt>{I18N.legalBasis}</dt><dd>{selected.request.legal_basis || "-"}</dd></div>
|
||||
</dl>
|
||||
<div className="button-row">
|
||||
<Button onClick={() => void mutate((request) => searchDataSubjectRequest(settings, request), I18N.searchComplete)} disabled={!canManage || busy}><Search size={16} /> {I18N.runSearch}</Button>
|
||||
{selected.request.request_kind !== "access" && <Button onClick={() => void mutate((request) => planDataSubjectErasure(settings, request), I18N.planComplete)} disabled={!canManage || busy || !selected.search.searched_at}><ShieldCheck size={16} /> {I18N.plan}</Button>}
|
||||
<Button onClick={() => void download()} disabled={!canExport || busy}><Download size={16} /> {I18N.export}</Button>
|
||||
{actions.some((action) => action.executable) && <Button variant="danger" onClick={() => setExecuting(true)} disabled={!canErase || busy || !selectedActions.length}><Play size={16} /> {I18N.execute}</Button>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{coverageGaps.length > 0 && <DismissibleAlert tone="warning"><strong>{I18N.providerGap}</strong><br />{I18N.coverage}: {coverageGaps.join(", ")}</DismissibleAlert>}
|
||||
|
||||
<Card title={I18N.collectedRecords}>
|
||||
<DataGrid id={`admin-dsar-records-${selected.request.id}`} rows={records} columns={recordColumns} initialFit="container" getRowKey={(row) => `${row.provider_id}:${row.resource_type}:${row.resource_id}`} emptyText={I18N.noSelection} />
|
||||
</Card>
|
||||
|
||||
{actions.length > 0 && <Card title={I18N.erasurePlan}>
|
||||
<DataGrid id={`admin-dsar-actions-${selected.request.id}`} rows={actions} columns={actionColumns} initialFit="container" getRowKey={(row) => row.action_id} emptyText={I18N.noSelection} />
|
||||
</Card>}
|
||||
</> : <DismissibleAlert tone="info">{I18N.noSelection}</DismissibleAlert>}
|
||||
</AdminPageLayout>
|
||||
|
||||
<Dialog
|
||||
open={creating}
|
||||
title={I18N.createTitle}
|
||||
className="admin-dialog admin-dialog-wide"
|
||||
portal
|
||||
closeDisabled={busy}
|
||||
onClose={() => setCreating(false)}
|
||||
footer={<><Button onClick={() => setCreating(false)} disabled={busy}>{I18N.cancel}</Button><Button variant="primary" onClick={() => void create()} disabled={busy || !draft.reference.trim() || !draft.purpose.trim()}>{I18N.save}</Button></>}
|
||||
helpContextId="admin.privacy.data-subject-requests"
|
||||
>
|
||||
<div className="admin-form-grid two-columns">
|
||||
<FormField label={I18N.reference}><input value={draft.reference} onChange={(event) => setDraft({ ...draft, reference: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.kindLabel}><select value={draft.requestKind} onChange={(event) => setDraft({ ...draft, requestKind: event.target.value as Draft["requestKind"] })}><option value="access">{I18N.accessRequest}</option><option value="erasure">{I18N.erasureRequest}</option><option value="access_and_erasure">{I18N.combinedRequest}</option></select></FormField>
|
||||
<FormField label={I18N.email}><input type="email" value={draft.email} onChange={(event) => setDraft({ ...draft, email: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.accountIdLabel}><input value={draft.accountId} onChange={(event) => setDraft({ ...draft, accountId: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.membershipIdLabel}><input value={draft.membershipId} onChange={(event) => setDraft({ ...draft, membershipId: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.identityIdLabel}><input value={draft.identityId} onChange={(event) => setDraft({ ...draft, identityId: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.externalReferences} help={I18N.externalReferencesHelp}><textarea rows={3} value={draft.externalReferences} onChange={(event) => setDraft({ ...draft, externalReferences: event.target.value })} placeholder="source.namespace=reference" /></FormField>
|
||||
<FormField label={I18N.due}><input type="date" value={draft.dueDate} onChange={(event) => setDraft({ ...draft, dueDate: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.legalBasis}><input value={draft.legalBasis} onChange={(event) => setDraft({ ...draft, legalBasis: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.purpose}><textarea rows={3} value={draft.purpose} onChange={(event) => setDraft({ ...draft, purpose: event.target.value })} /></FormField>
|
||||
<FormField label={I18N.notes}><textarea rows={3} value={draft.notes} onChange={(event) => setDraft({ ...draft, notes: event.target.value })} /></FormField>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={executing && Boolean(selected)}
|
||||
title={I18N.executeTitle}
|
||||
className="admin-dialog"
|
||||
portal
|
||||
role="alertdialog"
|
||||
closeDisabled={busy}
|
||||
onClose={() => setExecuting(false)}
|
||||
footer={<><Button onClick={() => setExecuting(false)} disabled={busy}>{I18N.cancel}</Button><Button variant="danger" onClick={() => void execute()} disabled={busy || !selected || confirmation !== `ERASE ${selected.request.id}`}>{I18N.execute}</Button></>}
|
||||
>
|
||||
<DismissibleAlert tone="warning">{I18N.executeWarning}</DismissibleAlert>
|
||||
<p>{selectedActions.length} / {actions.filter((action) => action.executable).length}</p>
|
||||
<FormField label={I18N.confirmation} help={selected ? `${I18N.confirmationHelp}: ERASE ${selected.request.id}` : I18N.confirmationHelp}>
|
||||
<input value={confirmation} onChange={(event) => setConfirmation(event.target.value)} autoComplete="off" />
|
||||
</FormField>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function createPayload(draft: Draft): DataSubjectRequestCreatePayload {
|
||||
return {
|
||||
reference: draft.reference.trim(),
|
||||
request_kind: draft.requestKind,
|
||||
subject: {
|
||||
email: optional(draft.email),
|
||||
account_id: optional(draft.accountId),
|
||||
membership_id: optional(draft.membershipId),
|
||||
identity_id: optional(draft.identityId),
|
||||
external_references: externalReferenceLines(draft.externalReferences)
|
||||
},
|
||||
purpose: draft.purpose.trim(),
|
||||
legal_basis: optional(draft.legalBasis),
|
||||
due_at: draft.dueDate ? new Date(`${draft.dueDate}T23:59:59`).toISOString() : null,
|
||||
notes: optional(draft.notes)
|
||||
};
|
||||
}
|
||||
|
||||
function defaultActionSelection(detail: DataSubjectRequestDetail): string[] {
|
||||
return (detail.erasure_plan.actions ?? []).filter((action) => action.executable).map((action) => action.action_id);
|
||||
}
|
||||
|
||||
function subjectLabel(request: DataSubjectRequestSummary): string {
|
||||
const subject = request.subject;
|
||||
return subject.email || subject.membership_id || subject.identity_id || subject.account_id || Object.values(subject.external_references ?? {})[0] || "-";
|
||||
}
|
||||
|
||||
function statusTone(value: string): string {
|
||||
if (value === "completed") return "success";
|
||||
if (value.includes("partial")) return "warning";
|
||||
if (value === "draft") return "inactive";
|
||||
return "active";
|
||||
}
|
||||
|
||||
function optional(value: string): string | null {
|
||||
const normalized = value.trim();
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
function validExternalReferenceLines(value: string): boolean {
|
||||
return value.split(/\r?\n/).every((line) => {
|
||||
if (!line.trim()) return true;
|
||||
const separator = line.indexOf("=");
|
||||
return separator > 0 && Boolean(line.slice(separator + 1).trim());
|
||||
});
|
||||
}
|
||||
|
||||
function externalReferenceLines(value: string): Record<string, string> {
|
||||
return Object.fromEntries(value.split(/\r?\n/).flatMap((line) => {
|
||||
const separator = line.indexOf("=");
|
||||
if (separator <= 0) return [];
|
||||
const key = line.slice(0, separator).trim();
|
||||
const reference = line.slice(separator + 1).trim();
|
||||
return key && reference ? [[key, reference]] : [];
|
||||
}));
|
||||
}
|
||||
@@ -2,6 +2,67 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-admin.data_subject_requests.ds001": "Data-subject requests",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds002": "Search, export, and govern erasure requests with explicit provider coverage and retained evidence.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds003": "Create request",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds004": "Reload",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds005": "Reference",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds006": "Request type",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds007": "Status",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds008": "Data subject",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds009": "Due",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds010": "Actions",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds011": "Inspect request",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds012": "No data-subject requests have been recorded.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds013": "Records",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds014": "Executable actions",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds015": "Modules without provider",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds016": "Run search",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds017": "Create erasure plan",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds018": "Export JSON",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds019": "Execute selected actions",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds020": "Collected records",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds021": "Erasure plan",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds022": "Provider coverage",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds023": "Select a request to inspect its collected records and decisions.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds024": "Provider",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds025": "Category",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds026": "Evidence handling",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds027": "Decision",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds028": "Rationale",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds029": "Select",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds030": "Create data-subject request",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds031": "Purpose",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds032": "Legal basis",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds033": "Email",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds034": "Account ID",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds035": "Membership ID",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds036": "Identity ID",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds037": "Notes",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds038": "Save",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds039": "Cancel",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds040": "Execute erasure actions",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds041": "These actions may be irreversible. Review retained evidence and provider coverage before continuing.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds042": "Confirmation",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds043": "Enter the exact confirmation phrase",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds044": "Provide at least one subject selector.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds045": "The export is not complete across every enabled module.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds046": "Data-subject request created.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds047": "Provider search completed.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds048": "Erasure plan created.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds049": "Selected erasure actions were processed.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds050": "Data-subject export downloaded.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds051": "Access",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds052": "Erasure",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds053": "Access and erasure",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds054": "Mutable",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds055": "Retained evidence",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds056": "Irreversible",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds057": "Executable",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds058": "Retained / review",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds059": "External references",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds060": "Enter one namespace=reference pair per line for identifiers owned by source systems.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds061": "Each external reference must use namespace=reference on its own line.",
|
||||
"i18n:govoplan-admin.search_module_directory.7c8f1001": "Search module directory",
|
||||
"i18n:govoplan-admin.module_name_package_or_tag.7c8f1002": "Module name, package, or tag",
|
||||
"i18n:govoplan-admin.filter_module_directory.7c8f1003": "Filter module directory",
|
||||
@@ -454,6 +515,67 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.working.049ac820": "Working..."
|
||||
},
|
||||
"de": {
|
||||
"i18n:govoplan-admin.data_subject_requests.ds001": "Betroffenenanfragen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds002": "Auskunfts- und Löschanfragen mit expliziter Anbieterabdeckung und Aufbewahrungsnachweisen suchen, exportieren und steuern.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds003": "Anfrage anlegen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds004": "Neu laden",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds005": "Aktenzeichen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds006": "Anfrageart",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds007": "Status",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds008": "Betroffene Person",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds009": "Fällig",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds010": "Aktionen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds011": "Anfrage prüfen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds012": "Es wurden noch keine Betroffenenanfragen erfasst.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds013": "Datensätze",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds014": "Ausführbare Aktionen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds015": "Module ohne Anbieter",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds016": "Suche ausführen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds017": "Löschplan erstellen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds018": "JSON exportieren",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds019": "Ausgewählte Aktionen ausführen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds020": "Gefundene Datensätze",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds021": "Löschplan",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds022": "Anbieterabdeckung",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds023": "Wählen Sie eine Anfrage aus, um Datensätze und Entscheidungen zu prüfen.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds024": "Anbieter",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds025": "Kategorie",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds026": "Nachweisbehandlung",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds027": "Entscheidung",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds028": "Begründung",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds029": "Auswählen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds030": "Betroffenenanfrage anlegen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds031": "Zweck",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds032": "Rechtsgrundlage",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds033": "E-Mail",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds034": "Konto-ID",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds035": "Mitgliedschafts-ID",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds036": "Identitäts-ID",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds037": "Notizen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds038": "Speichern",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds039": "Abbrechen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds040": "Löschaktionen ausführen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds041": "Diese Aktionen können unumkehrbar sein. Prüfen Sie Aufbewahrungsnachweise und Anbieterabdeckung, bevor Sie fortfahren.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds042": "Bestätigung",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds043": "Geben Sie die Bestätigungsphrase exakt ein",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds044": "Geben Sie mindestens ein Merkmal der betroffenen Person an.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds045": "Der Export deckt nicht alle aktivierten Module vollständig ab.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds046": "Betroffenenanfrage wurde angelegt.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds047": "Anbietersuche wurde abgeschlossen.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds048": "Löschplan wurde erstellt.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds049": "Ausgewählte Löschaktionen wurden verarbeitet.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds050": "Betroffenenauskunft wurde heruntergeladen.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds051": "Auskunft",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds052": "Löschung",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds053": "Auskunft und Löschung",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds054": "Veränderbar",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds055": "Aufzubewahrender Nachweis",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds056": "Nicht umkehrbar",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds057": "Ausführbar",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds058": "Aufbewahren / prüfen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds059": "Externe Referenzen",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds060": "Je Zeile ein Paar Namensraum=Referenz für Kennungen aus Quellsystemen eingeben.",
|
||||
"i18n:govoplan-admin.data_subject_requests.ds061": "Jede externe Referenz muss in einer eigenen Zeile als Namensraum=Referenz angegeben werden.",
|
||||
"i18n:govoplan-admin.search_module_directory.7c8f1001": "Modulverzeichnis durchsuchen",
|
||||
"i18n:govoplan-admin.module_name_package_or_tag.7c8f1002": "Modulname, Paket oder Schlagwort",
|
||||
"i18n:govoplan-admin.filter_module_directory.7c8f1003": "Modulverzeichnis filtern",
|
||||
|
||||
@@ -11,6 +11,7 @@ const GovernanceTemplatesPanel = lazy(() => import("./features/admin/GovernanceT
|
||||
const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel"));
|
||||
const TenantModuleManagementPanel = lazy(() => import("./features/admin/TenantModuleManagementPanel"));
|
||||
const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel"));
|
||||
const DataSubjectRequestsPanel = lazy(() => import("./features/admin/DataSubjectRequestsPanel"));
|
||||
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
@@ -124,6 +125,22 @@ const adminSections: AdminSectionsUiCapability = {
|
||||
canWrite: hasScope(auth, "system:settings:write")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "tenant-data-subject-requests",
|
||||
moduleId: "admin",
|
||||
kind: "management",
|
||||
surfaceId: "admin.section.tenant-data-subject-requests",
|
||||
label: "i18n:govoplan-admin.data_subject_requests.ds001",
|
||||
group: "TENANT",
|
||||
order: 55,
|
||||
allOf: ["access:privacy:read"],
|
||||
render: ({ settings, auth }) => createElement(DataSubjectRequestsPanel, {
|
||||
settings,
|
||||
canManage: hasScope(auth, "access:privacy:manage"),
|
||||
canExport: hasScope(auth, "access:privacy:export"),
|
||||
canErase: hasScope(auth, "access:privacy:erase")
|
||||
})
|
||||
},
|
||||
{
|
||||
id: "tenant-modules",
|
||||
moduleId: "admin",
|
||||
@@ -173,6 +190,7 @@ export const adminModule: PlatformWebModule = {
|
||||
{ id: "admin.section.system-groups", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.central_groups.5c9b5b66", order: 50 },
|
||||
{ id: "admin.section.system-modules", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.modules.04e9462c", order: 85 },
|
||||
{ id: "admin.section.system-tenant-modules", moduleId: "admin", kind: "section", label: "Tenant modules", order: 86 },
|
||||
{ id: "admin.section.tenant-data-subject-requests", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.data_subject_requests.ds001", order: 55 },
|
||||
{ id: "admin.section.tenant-modules", moduleId: "admin", kind: "section", label: "Modules", order: 60 }
|
||||
],
|
||||
uiCapabilities: {
|
||||
|
||||
Reference in New Issue
Block a user