+ {/* Mentions obligatoires manquantes ------------------------------- */}
+ {missing.length > 0 ? (
+
+ {t('identity.incompleteBody')}
+
+ {missing.map(field => (
+ {t(`identity.fields.${field}`)}
+ ))}
+
+ {t('identity.where')}
+
+ ) : (
+
+ {t('identity.completeBody', { name: CONTROLLER.tradingName })}
+
+ )}
+
+ {/* Conservation --------------------------------------------------- */}
+
+
+
+
{t('retention.title')}
+
{t('retention.subtitle')}
+
+ 0 ? 'default' : 'outline'}
+ disabled={expiredTotal === 0 || purge.isPending}
+ onClick={askPurge}
+ >
+
+ {t('purge.action')}
+
+
+
+ {/* La purge automatique supprime definitivement des lignes :
+ son etat doit se lire sans avoir a ouvrir la configuration. */}
+
+ {preview.data.enabled ? t('retention.autoOnBody') : t('retention.autoOffBody')}
+
+
+
+
+
+
+ {t('retention.table')}
+ {t('retention.duration')}
+ {t('retention.onErasure')}
+ {t('retention.expired')}
+ {t('retention.basis')}
+
+
+
+ {rules.data.rules.map((rule: RetentionRule) => {
+ const line = preview.data.lines.find(item => item.table === rule.table);
+ return (
+
+ {rule.table}
+
+ {rule.months === null
+ ? t('retention.accountLifetime')
+ : t('retention.months', { count: rule.months })}
+
+ {t(`retention.actions.${rule.onErasure}`)}
+
+ {line ? (
+ 0 ? 'font-semibold text-amber-600' : 'text-neutral-400'
+ }
+ >
+ {line.expired}
+
+ ) : (
+ —
+ )}
+
+
+ {rule.basis}
+
+
+ );
+ })}
+
+
+
+
+
+
+ {/* Registre des sous-traitants ------------------------------------ */}
+
+
+ {t('processors.title')}
+ {t('processors.subtitle')}
+
+
+
+
+
+
+ {t('processors.name')}
+ {t('processors.role')}
+ {t('processors.location')}
+ {t('processors.basis')}
+
+
+
+ {SUBPROCESSORS.map(processor => (
+
+
+ {processor.name}
+
+
+ {tProcessor(`${processor.key}.role`)}
+
+
+ {tProcessor(`${processor.key}.location`)}
+
+
+ {tBasis(processor.transferBasis)}
+
+
+ ))}
+
+
+
+
+
+
+ {/* Demandes de droits --------------------------------------------- */}
+
+
+ {t('requests.title')}
+ {t('requests.subtitle')}
+
+
+ {requests.data && requests.data.requests.length > 0 ? (
+
+
+
+
+ {t('requests.date')}
+ {t('requests.action')}
+ {t('requests.subject')}
+
+
+
+ {requests.data.requests.map((entry: RightsRequest, index: number) => (
+
+
+ {dateTime(entry.timestamp)}
+
+
+
+ {entry.action === 'gdpr_erasure_executed' ? (
+
+ ) : (
+
+ )}
+ {t(`requests.actions.${entry.action}`)}
+
+
+
+ {entry.user_email}
+
+
+ ))}
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {t('disclaimer')}
+
+
+ >
+ );
+}
diff --git a/apps/frontend/src/__tests__/components/nav-config.test.ts b/apps/frontend/src/__tests__/components/nav-config.test.ts
index 298749d..6bac4d5 100644
--- a/apps/frontend/src/__tests__/components/nav-config.test.ts
+++ b/apps/frontend/src/__tests__/components/nav-config.test.ts
@@ -66,7 +66,7 @@ describe('buildAppNav', () => {
});
describe('buildAdminNav', () => {
- it('expose les huit destinations d administration', () => {
+ it('expose les neuf destinations d administration', () => {
const groups = buildAdminNav({ t, tShell });
const keys = groups.flatMap(g => g.items).map(i => i.key);
expect(keys).toEqual([
@@ -77,6 +77,7 @@ describe('buildAdminNav', () => {
'csvRates',
'blog',
'assistant',
+ 'compliance',
'logs',
]);
});
diff --git a/apps/frontend/src/components/shell/nav-config.ts b/apps/frontend/src/components/shell/nav-config.ts
index 17fa150..c8a3a79 100644
--- a/apps/frontend/src/components/shell/nav-config.ts
+++ b/apps/frontend/src/components/shell/nav-config.ts
@@ -9,6 +9,7 @@ import {
Package,
ScrollText,
Search,
+ ShieldCheck,
Users,
type LucideIcon,
} from 'lucide-react';
@@ -177,6 +178,12 @@ export function buildAdminNav({ t, tShell }: AdminNavParams): NavGroup[] {
label: tShell('groups.system'),
items: [
{ key: 'assistant', label: t('items.assistant'), href: '/admin/assistant', icon: Bot },
+ {
+ key: 'compliance',
+ label: t('items.compliance'),
+ href: '/admin/compliance',
+ icon: ShieldCheck,
+ },
{ key: 'logs', label: t('items.logs'), href: '/admin/logs', icon: ScrollText },
],
},
diff --git a/apps/frontend/src/lib/api/compliance.ts b/apps/frontend/src/lib/api/compliance.ts
new file mode 100644
index 0000000..2e20df4
--- /dev/null
+++ b/apps/frontend/src/lib/api/compliance.ts
@@ -0,0 +1,60 @@
+/**
+ * Conformité — vue d'administration.
+ *
+ * Les durées de conservation viennent du serveur, pas d'une copie côté client :
+ * ce sont celles que le code applique réellement, et c'est précisément ce
+ * qu'une console de conformité doit montrer.
+ */
+
+import { get, post } from './client';
+
+export interface RetentionRule {
+ table: string;
+ onErasure: 'delete' | 'anonymise' | 'keep';
+ months: number | null;
+ timestampColumn: string | null;
+ keepWhere: string | null;
+ basis: string;
+}
+
+export interface RetentionLine {
+ table: string;
+ months: number;
+ /** Lignes ayant dépassé la durée de conservation. */
+ expired: number;
+ oldest: string | null;
+}
+
+export interface RetentionReport {
+ /** La purge automatique est-elle activée sur cet environnement ? */
+ enabled: boolean;
+ runAt: string;
+ lines: RetentionLine[];
+}
+
+export interface RightsRequest {
+ action: string;
+ status: string;
+ user_id: string;
+ user_email: string;
+ organization_id: string;
+ metadata: Record