'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useLocale, useTranslations } from 'next-intl'; import { AlertTriangle, CheckCircle2, Eraser, RefreshCw, ShieldCheck } from 'lucide-react'; import { getRetentionPreview, getRetentionRules, getRightsRequests, runRetentionPurge, type RetentionLine, type RetentionRule, type RightsRequest, } from '@/lib/api/compliance'; import { CONTROLLER, SUBPROCESSORS, missingControllerFields, } from '@/lib/legal/processing'; import { Button } from '@/components/ui/button'; import { Callout } from '@/components/ui/callout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PageHeader } from '@/components/ui/PageHeader'; import { EmptyState, ErrorState, PageSpinner } from '@/components/ui/states'; import { useConfirm } from '@/components/ui/use-confirm'; import { useToast } from '@/components/ui/toast'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; /** * Console de conformite. * * Elle repond a trois questions qu'on ne pouvait poser nulle part : que * declare-t-on, que conserve-t-on encore au-dela de la duree annoncee, et * a-t-on trace des demandes de droits. Le premier bloc n'est pas decoratif : * une politique de confidentialite sans identite du responsable est * incomplete au sens de l'article 13, et c'est ici qu'on le voit. */ export default function CompliancePage() { const t = useTranslations('admin.compliance'); const tProcessor = useTranslations('marketing.privacy.processors'); const tBasis = useTranslations('marketing.privacy.recipientsTable.bases'); const locale = useLocale(); const dateLocale = locale === 'fr' ? 'fr-FR' : 'en-US'; const confirm = useConfirm(); const { toast } = useToast(); const queryClient = useQueryClient(); const rules = useQuery({ queryKey: ['retention-rules'], queryFn: getRetentionRules }); const preview = useQuery({ queryKey: ['retention-preview'], queryFn: getRetentionPreview }); const requests = useQuery({ queryKey: ['rights-requests'], queryFn: getRightsRequests }); const purge = useMutation({ mutationFn: runRetentionPurge, onSuccess: report => { const removed = report.lines.reduce((sum, line) => sum + line.expired, 0); toast.success(t('purge.done', { count: removed })); queryClient.invalidateQueries({ queryKey: ['retention-preview'] }); queryClient.invalidateQueries({ queryKey: ['rights-requests'] }); }, onError: () => toast.error(t('purge.failed')), }); const missing = missingControllerFields(); const dateTime = (value: string) => new Date(value).toLocaleString(dateLocale, { dateStyle: 'medium', timeStyle: 'short' }); const expiredTotal = preview.data?.lines.reduce((sum: number, line: RetentionLine) => sum + line.expired, 0) ?? 0; const askPurge = async () => { const ok = await confirm({ title: t('purge.confirmTitle'), description: t('purge.confirmBody', { count: expiredTotal }), confirmLabel: t('purge.action'), destructive: true, }); if (ok) purge.mutate(); }; if (rules.isPending || preview.isPending) return ; if (rules.isError || preview.isError) { return ( { rules.refetch(); preview.refetch(); }} /> ); } return ( <> preview.refetch()}> {t('refresh')} } />
{/* 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')}

{/* 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' ? ( {entry.user_email} ))}
) : ( )}

); }