// Account — Profil, paramètres, connecteurs, contenus personnels.
// Chaque ligne est pilotée par `/api/v1/account` : le serveur décide de ce qui
// est actif et, quand il refuse, il fournit la raison. Aucun état n'est inventé
// ici, faute de quoi l'écran promettrait des fonctions qui n'existent pas.
const AC_VT = window.VT || {};
const AC_INK = AC_VT.ink || '#0a0a0a';
const AC_SUB = AC_VT.sub || '#6b7280';
const AC_BG = AC_VT.paper || '#f6f5f1';
const AC_SURF = AC_VT.surface || '#ffffff';
const AC_HAIR = AC_VT.hair || 'rgba(10,10,10,0.06)';
const AC_HAIR2 = AC_VT.hair2 || 'rgba(10,10,10,0.10)';
const AC_OK = AC_VT.success || '#059669';
const AC_MED = AC_VT.medBlue || '#3d6b8f';
const AC_DANGER = AC_VT.danger || '#b91c1c';
const AC_MED_TINT = AC_VT.medBlueTint || 'rgba(61,107,143,0.10)';
const AC_OK_TINT = AC_VT.successTint || 'rgba(5,150,105,0.10)';
// Libellés alignés mot pour mot sur l'application native
// (`mobile/src/features/account/account-screen.tsx`) : deux formulations
// différentes pour la même raison serveur finiraient par diverger.
const AC_REASONS = {
organization_admin_required: 'Réservé à un futur administrateur de l’organisation.',
identity_provider_managed: 'Cette action est gérée par le fournisseur d’identité.',
simulated_identity: 'Indisponible avec une identité de démonstration.',
step_up_unavailable: 'Une vérification d’identité renforcée est indisponible.',
provider_unavailable: 'Aucun fournisseur réel n’est configuré.',
source_unavailable: 'Aucune source approuvée n’alimente cette information.',
single_option_supported: 'Une seule option est prise en charge dans cette version.',
feature_not_in_j6a: 'Cette action ne fait pas partie du périmètre J6a.',
secure_export_storage_unavailable: 'Aucun stockage chiffré qualifié n’est disponible pour l’export.',
retention_policy_unapproved: 'La politique de rétention et de fermeture n’est pas approuvée.',
feature_not_in_j6b1: 'Cette action ne fait pas partie du périmètre J6b1.',
role_not_supported: 'Votre accès ne permet pas cette action.',
};
function acReason(capability) {
if (!capability || capability.enabled) return null;
return AC_REASONS[capability.reason_code] || 'Action indisponible.';
}
const AC_ZONES = [
'Europe/Paris', 'Europe/Brussels', 'Europe/Zurich', 'Europe/London',
'America/Montreal', 'America/Guadeloupe', 'Indian/Reunion',
];
const AC_VERIFICATION = { verified: 'vérifié', synthetic: 'synthétique', unverified: 'non vérifié' };
const AC_SALUTATIONS = { doctor: 'Dr.', professor: 'Pr.', none: '' };
const AC_DOC_TYPES = ['CRO', 'CRC', 'CRH', 'NOTE', 'LETTRE'];
function acPlural(count, singular, plural) {
return `${count} ${count > 1 ? (plural || `${singular}s`) : singular}`;
}
function acDate(value) {
if (!value) return '—';
return new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' })
.format(new Date(value));
}
// ── Section row ──
function SettingRow({ icon, label, sub, right, onClick, danger = false, reason = null, role, checked }) {
const actionable = Boolean(onClick) && !reason;
const body = (
<>
{label}
{sub &&
{sub}
}
{reason && (
{reason}
)}
{right || (actionable ? (
) : null)}
>
);
const shared = {
width: '100%', display: 'flex', alignItems: 'center', gap: 14,
padding: '14px 16px', background: 'none', border: 'none',
fontFamily: 'inherit', textAlign: 'left',
};
if (!actionable) {
return (
{body}
);
}
return (
);
}
// ── Section group ──
function SettingGroup({ title, children }) {
return (
{title}
{React.Children.map(children, (child, i) => (
{i > 0 && }
{child}
))}
);
}
// ── Interrupteur purement visuel ──
// Il vit à l'intérieur du bouton de la ligne : en faire un `