M24: implement privacy governance
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { useCallback, useEffect, useState, type FormEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { api } from "../api/client";
|
||||
import type { CustomerAnonymizeResult, PrivacyRetention } from "../api/types";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
|
||||
export function Privacy() {
|
||||
const { t } = useTranslation("privacy");
|
||||
const { user } = useAuth();
|
||||
const [retention, setRetention] = useState<PrivacyRetention | null>(null);
|
||||
const [customerRef, setCustomerRef] = useState("");
|
||||
const [confirmation, setConfirmation] = useState("");
|
||||
const [reason, setReason] = useState("");
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
const [working, setWorking] = useState(false);
|
||||
|
||||
const loadRetention = useCallback(() => {
|
||||
setError(false);
|
||||
api.get<PrivacyRetention>("/api/v1/privacy/retention").then(setRetention).catch(() => setError(true));
|
||||
}, []);
|
||||
|
||||
useEffect(loadRetention, [loadRetention]);
|
||||
|
||||
async function anonymize(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
setWorking(true);
|
||||
setMessage(null);
|
||||
setError(false);
|
||||
try {
|
||||
const result = await api.post<CustomerAnonymizeResult>(
|
||||
`/api/v1/privacy/customers/${encodeURIComponent(customerRef)}/anonymize`,
|
||||
{ confirmation, reason },
|
||||
);
|
||||
setMessage(t(`anonymize.${result.status}`, { ref: result.public_ref }));
|
||||
setConfirmation("");
|
||||
setReason("");
|
||||
loadRetention();
|
||||
} catch {
|
||||
setError(true);
|
||||
} finally {
|
||||
setWorking(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (user?.role !== "operations_manager") return <div className="page">
|
||||
<PageHeader eyebrow={t("eyebrow")} title={t("title")} description={t("managerOnly")} />
|
||||
</div>;
|
||||
return <div className="page privacy-page">
|
||||
<PageHeader eyebrow={t("eyebrow")} title={t("title")} description={t("description")} />
|
||||
{error && <ErrorState message={t("error")} />}
|
||||
{!retention && !error && <LoadingState label={t("loading")} />}
|
||||
{retention && <>
|
||||
<section className="privacy-metrics" aria-label={t("policyTitle")}>
|
||||
<article><span>{t("totalCustomers")}</span><strong>{retention.customers_total}</strong></article>
|
||||
<article><span>{t("eligibleCustomers")}</span><strong>{retention.customers_eligible}</strong></article>
|
||||
<article><span>{t("anonymizedCustomers")}</span><strong>{retention.customers_anonymized}</strong></article>
|
||||
<article><span>{t("bookingRetention")}</span><strong>{t("days", { count: retention.minimum_booking_retention_days })}</strong></article>
|
||||
<article><span>{t("auditRetention")}</span><strong>{t("days", { count: retention.audit_retention_days })}</strong></article>
|
||||
</section>
|
||||
<div className="privacy-workspaces">
|
||||
<section className="panel privacy-panel">
|
||||
<h2>{t("exports.title")}</h2><p>{t("exports.detail")}</p>
|
||||
<label>{t("customerRef")}<input value={customerRef} onChange={(event) => setCustomerRef(event.target.value.toUpperCase())} placeholder="CUS-0001" /></label>
|
||||
<div className="form-actions-inline">
|
||||
<a className={`button ${customerRef ? "" : "is-disabled"}`} aria-disabled={!customerRef} href={customerRef ? `/api/v1/privacy/customers/${encodeURIComponent(customerRef)}/export` : undefined}>{t("exports.customer")}</a>
|
||||
<a className="button" href="/api/v1/audit/export.csv">{t("exports.audit")}</a>
|
||||
</div>
|
||||
</section>
|
||||
<form className="panel privacy-panel" onSubmit={anonymize}>
|
||||
<h2>{t("anonymize.title")}</h2><p>{t("anonymize.detail", { days: retention.minimum_booking_retention_days })}</p>
|
||||
<label>{t("customerRef")}<input value={customerRef} onChange={(event) => setCustomerRef(event.target.value.toUpperCase())} placeholder="CUS-0001" required /></label>
|
||||
<label>{t("anonymize.reason")}<textarea value={reason} onChange={(event) => setReason(event.target.value)} minLength={8} required /></label>
|
||||
<label>{t("anonymize.confirmation", { ref: customerRef || "CUS-…" })}<input value={confirmation} onChange={(event) => setConfirmation(event.target.value.toUpperCase())} required /></label>
|
||||
<button className="button button-danger" disabled={working || confirmation !== customerRef || !customerRef}>{working ? t("anonymize.working") : t("anonymize.submit")}</button>
|
||||
</form>
|
||||
</div>
|
||||
{message && <p className="success" role="status">{message}</p>}
|
||||
</>}
|
||||
</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user