// Accueil Compte connecté. La V5 garde les feuilles et écrans métier ; cette
// couche locale ne change que la navigation et la hiérarchie visuelle.
const ACCOUNT_HOME_ENTRIES = Object.freeze([
{ id: 'profile', label: 'Profil', sub: 'Identité et préférences' },
{ id: 'clinic', label: 'Structures', sub: 'Organisations, établissements et services' },
{ id: 'assistant', label: 'Assistant', sub: 'Historique, prompts et modèles' },
{ id: 'connectors', label: 'Connecteurs', sub: 'Services externes et leur statut' },
{ id: 'security', label: 'Sécurité', sub: 'Accès, facturation et données' },
]);
const ACCOUNT_SECTION_TITLES = Object.freeze({
profile: 'Profil', clinic: 'Structures', assistant: 'Assistant',
connectors: 'Connecteurs', security: 'Sécurité',
});
function AccountSectionIcon({ id, size = 19 }) {
const paths = {
profile: <>>,
clinic: <>>,
assistant: <>>,
connectors: <>>,
security: <>>,
};
return ;
}
function AccountHomeEntry({ entry, onOpen }) {
const { InteractiveRow } = window.VascoUI;
return onOpen(entry.id, event.currentTarget)}
data-account-return={entry.id}
style={{ width: '100%', minHeight: 66, padding: '11px 14px', border: 0,
background: 'transparent', fontFamily: 'inherit', textAlign: 'left',
display: 'flex', alignItems: 'center', gap: 12, cursor: 'pointer' }}>
{entry.label}
{entry.sub}
;
}
function AccountSectionShell({ title, headingRef, onBack, refreshing, onTabChange, accent, children }) {
const { IconButton, icons } = window.VascoUI;
return
{title}
{refreshing &&
Actualisation…}
{children}
{typeof BottomNav !== 'undefined' &&
{ if (onTabChange) onTabChange(id); }} accent={accent} />}
;
}
// La feuille historique garde la validation et la sauvegarde déjà éprouvées.
// Seuls ses deux menus natifs sont remplacés au rendu par le dropdown simple du
// design system, sans modifier la référence V5 figée.
const AccountBaseProfileEditorSheet = ProfileEditorSheet;
function accountProfileEditorReplaceSelects(node) {
if (!React.isValidElement(node)) return node;
if (node.type === 'select') {
const optionNodes = React.Children.toArray(node.props.children)
.filter((child) => React.isValidElement(child) && child.type === 'option');
const options = optionNodes.map((option) => [option.props.value, option.props.children]);
const values = options.map(([value]) => value);
const civilite = ['doctor', 'professor', 'none'].every((value) => values.includes(value));
const timeZone = !civilite && values.length > 0
&& values.every((value) => typeof value === 'string' && value.includes('/'));
const DropdownComponent = window.VascoUI?.SimpleSelect;
if ((civilite || timeZone) && DropdownComponent) {
const label = civilite ? 'Civilité' : 'Fuseau horaire';
return node.props.onChange({ target: { value } })} />;
}
}
const children = React.Children.toArray(node.props?.children);
if (!children.length) return node;
return React.cloneElement(
node,
node.props,
children.map((child) => accountProfileEditorReplaceSelects(child)),
);
}
function AccountProfileEditorSheet(props) {
return accountProfileEditorReplaceSelects(AccountBaseProfileEditorSheet(props));
}
function accountFormatPhone(phone) {
const compact = String(phone || '').replace(/\s+/g, '');
const frenchNumber = compact.match(/^(\+33)(\d)(\d{2})(\d{2})(\d{2})(\d{2})$/);
return frenchNumber ? frenchNumber.slice(1).join(' ') : phone;
}
function AccountProfileSection({ overview, capabilities, busy, onEditProfile, onToggleNotifications }) {
const profile = overview.profile;
const displayName = [AC_SALUTATIONS[profile.salutation], profile.given_name, profile.family_name]
.filter(Boolean).join(' ');
const personalSummary = [profile.specialty, accountFormatPhone(profile.phone) || 'Téléphone non renseigné']
.filter(Boolean).join(' · ');
return <>
>}
label={displayName} sub={personalSummary}
reason={acReason(capabilities.edit_profile)} onClick={onEditProfile} />
>}
label={profile.email} sub="Email professionnel" />
}
label={profile.rpps ? `RPPS ${profile.rpps}` : 'RPPS non renseigné'}
sub="Numéro d’identification" />
>}
label="Notifications"
sub={`Rappels de routine ${overview.preferences.routine_in_app_notifications ? 'activés' : 'désactivés'}. Les alertes cliniques restent visibles.`}
reason={acReason(capabilities.routine_notifications)} role="switch"
checked={overview.preferences.routine_in_app_notifications} onClick={onToggleNotifications}
right={} />
>;
}
function AccountClinicEditorGroup({ title, children }) {
return ;
}
function AccountClinicEditorSheet({ clinic, clinicContext, busy, error, onClose, onSave }) {
const [name, setName] = React.useState(clinic.name || '');
const [addressLine, setAddressLine] = React.useState(clinic.address_line || '');
const [postalCode, setPostalCode] = React.useState(clinic.postal_code || '');
const [city, setCity] = React.useState(clinic.city || '');
const [phone, setPhone] = React.useState(clinic.phone || '');
const [email, setEmail] = React.useState(clinic.email || '');
const fields = {
name: name.trim(), address_line: addressLine.trim(), postal_code: postalCode.trim(),
city: city.trim(), phone: phone.trim(), email: email.trim(),
};
const changed = Object.keys(fields).some((key) => fields[key] !== (clinic[key] || ''));
const activeClinic = clinicContext?.clinic?.name || clinic.name || 'l’établissement actif';
const input = (extra = {}) => ({ ...acInputStyle, background: '#fff', ...extra });
const submit = () => { if (changed && !busy) onSave(fields); };
return
Annuler
{busy ? 'Enregistrement…' : 'Enregistrer'}
>}>
{error && {error}
}
setName(event.target.value)}
maxLength={160} style={input()} />
setAddressLine(event.target.value)}
maxLength={200} style={input()} />
setPostalCode(event.target.value)}
maxLength={16} style={input()} />
setCity(event.target.value)}
maxLength={120} style={input()} />
setPhone(event.target.value)}
inputMode="tel" maxLength={24} style={input()} />
setEmail(event.target.value)}
inputMode="email" maxLength={254} style={input({ textOverflow: 'ellipsis' })} />
;
}
const ACCOUNT_DOCUMENT_TYPE_FAMILIES = [
['report', 'Compte rendu'],
['imaging', 'Imagerie'],
['examination', 'Examen'],
['correspondence', 'Courrier et liaison'],
['prescription', 'Ordonnance'],
['correspondence', 'Courrier médical'],
['other', 'Autre document'],
];
function AccountDocumentTypesManager({ open, runtime, onClose }) {
const { Button, Field, FormSheet, SimpleSelect } = window.VascoUI;
const [catalogue, setCatalogue] = React.useState(null);
const [label, setLabel] = React.useState('');
const [family, setFamily] = React.useState('report');
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState(null);
const attemptRef = React.useRef(null);
const load = React.useCallback(async () => {
setBusy(true);
try { setCatalogue(await runtime.documentTypes.list()); setError(null); }
catch (failure) { setError(failure?.message || 'Catalogue indisponible'); }
finally { setBusy(false); }
}, [runtime]);
React.useEffect(() => { if (open) void load(); }, [open, load]);
const normalized = label.trim().replace(/\s+/g, ' ');
const create = async () => {
if (!normalized || busy || !catalogue?.canManage) return;
if (!attemptRef.current) attemptRef.current = runtime.documentTypes.newAttempt();
setBusy(true); setError(null);
try {
await runtime.documentTypes.create(normalized, family, attemptRef.current);
attemptRef.current = null; setLabel(''); setFamily('report');
setCatalogue(await runtime.documentTypes.list());
showToast('Type ajouté au catalogue', 'success');
} catch (failure) {
setError(failure?.message || 'Ajout impossible');
} finally { setBusy(false); }
};
const standard = catalogue?.items?.filter((item) => item.source === 'standard') || [];
const local = catalogue?.items?.filter((item) => item.source === 'organization') || [];
const familyLabel = (value) => ACCOUNT_DOCUMENT_TYPE_FAMILIES.find(
([familyValue]) => familyValue === value,
)?.[1] || 'Autre document';
const typeRows = (items) => items.map((item) =>
{item.label}
{familyLabel(item.family)}
{item.source === 'standard' && {item.code}}
);
const standardCards = standard.map((item) =>
{item.code}
{item.label}
);
return
{error && {error}
}
{!catalogue && !error && Chargement…
}
{catalogue && <>
{!catalogue.canManage &&
Le catalogue est consultable. Seul un administrateur de l’organisation peut ajouter un type.
}
{catalogue.canManage &&
{ setLabel(event.target.value); attemptRef.current = null; }}
style={{ ...acInputStyle, background: '#fff' }} />
{ setFamily(nextFamily); attemptRef.current = null; }} />
}
{local.length ? typeRows(local) : Aucun type local ajouté.
}
{standardCards}
>}
;
}
function AccountClinicSection({ organization, capabilities, clinicOpening, clinicContext, onOpenClinic,
onOpenClinicContext, onOpenClinicInvitation, onManageOwnAssignments,
onManageInvitations, onManagePractitioners, onOpenCorrespondents, onOpenDocumentTypes }) {
const activeOrganization = clinicContext?.organization || organization;
const assignmentCount = clinicContext?.assignments?.length || 0;
const pendingInvitationCount = clinicContext?.managedInvitations?.filter(
(invitation) => invitation.status === 'pending',
).length || 0;
const contextAction = assignmentCount > 1 ? 'Changer de contexte' : 'Voir l’affectation';
const contextRows = clinicContext ? [
>}
label="Établissement" sub={clinicContext.clinic?.name || 'Aucun établissement actif'} />,
>}
label="Service" sub={clinicContext.department?.name || 'Aucun service actif'} />,
>}
label={contextAction}
sub={assignmentCount > 1 ? `${assignmentCount} affectations disponibles` : 'Contexte actif de cette session'}
onClick={onOpenClinicContext} />,
>}
label="Gérer mes affectations" sub="Quitter un service"
onClick={onManageOwnAssignments} />,
...(clinicContext.canInvitePractitioners ? [
>}
label="Inviter un praticien" sub="Choisir son service à partir de son RPPS"
onClick={onOpenClinicInvitation} />,
>}
label="Invitations envoyées"
sub={pendingInvitationCount
? `${pendingInvitationCount} en attente`
: 'Consulter l’historique'}
onClick={onManageInvitations} />,
>}
label="Gérer les praticiens" sub="Retirer une affectation"
onClick={onManagePractitioners} />,
] : []),
] : [
}
label="Établissement et service" sub="Contexte indisponible pour le moment" />,
];
return <>
Ce contexte détermine les données et les accès disponibles. Les écrans concernés s’actualisent après un changement.
>}
label="Organisation" sub={activeOrganization?.name || 'Organisation non renseignée'} />
{contextRows}
>}
label="Coordonnées de l’établissement" sub="Adresse, téléphone et email"
right={clinicOpening ? : undefined} onClick={onOpenClinic} />
{onOpenCorrespondents && >}
label="Correspondants médicaux" sub="Annuaire professionnel" onClick={onOpenCorrespondents} />}
>}
label="Types de documents" sub="Catalogue de l’organisation"
onClick={onOpenDocumentTypes} />
>}
label="Signature électronique" reason={acReason(capabilities.electronic_signature)} />
>;
}
function AccountAssistantSection({ counts, capabilities, onOpenHistory, onOpenPrompts, onOpenTemplates }) {
return
>}
label="Historique des conversations" sub={acPlural(counts.active_conversations, 'conversation active', 'conversations actives')}
reason={acReason(capabilities.conversation_history)} onClick={onOpenHistory} />
}
label="Préférences enregistrées"
sub={acPlural(counts.active_prompts, 'préférence active', 'préférences actives')}
reason={acReason(capabilities.manage_prompts)} onClick={onOpenPrompts} />
>}
label="Modèles de documents" sub={acPlural(counts.active_templates, 'modèle actif', 'modèles actifs')}
reason={acReason(capabilities.manage_templates)} onClick={onOpenTemplates} />
;
}
const ACCOUNT_PROMPT_SCOPE_OPTIONS = Object.freeze([
['ALL', 'Tous mes documents'],
['CRO', 'Mes comptes rendus opératoires'],
['CRC', 'Mes comptes rendus de consultation'],
['CRH', 'Mes comptes rendus d’hospitalisation'],
['LETTRE', 'Mes lettres'],
['STAFF', 'Mes comptes rendus de staff'],
]);
function accountPromptScopeLabel(scope) {
return ACCOUNT_PROMPT_SCOPE_OPTIONS.find(([value]) => value === scope)?.[1]
|| 'Tous mes documents';
}
function AccountPreferenceEditor({ open, item, busy, onClose, onSave }) {
const { Button, Field, FormSheet, SimpleSelect } = window.VascoUI;
const [name, setName] = React.useState('');
const [content, setContent] = React.useState('');
const [scope, setScope] = React.useState('ALL');
React.useEffect(() => {
if (!open) return;
setName(item?.name || 'Préférence rédactionnelle');
setContent(item?.content || '');
setScope(item?.scope || 'ALL');
}, [open, item]);
const normalizedName = name.trim().replace(/\s+/g, ' ');
const valid = normalizedName && content.trim() && name.length <= 120 && content.length <= 8000;
const dirty = !item?.id || normalizedName !== item.name || content !== item.content
|| scope !== item.scope;
const submit = () => {
if (!valid || !dirty || busy) return;
onSave({
name: normalizedName,
description: item?.description || null,
scope,
content,
});
};
return
>}>
setName(event.target.value)} style={acInputStyle} />
;
}
function AccountPreferenceHistory({ open, item, runtime, onClose }) {
const { Button, FormSheet, StatusNotice } = window.VascoUI;
const [versions, setVersions] = React.useState([]);
const [nextVersion, setNextVersion] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
const load = React.useCallback(async (beforeVersion = null, append = false) => {
if (!item?.id || loading) return;
setLoading(true); setError(null);
try {
const response = await runtime.prompts.revisions(item.id, beforeVersion);
setVersions((current) => append ? [...current, ...response.items] : response.items);
setNextVersion(response.next_before_version);
} catch (failure) {
setError(failure?.message || 'Historique indisponible');
} finally { setLoading(false); }
}, [item?.id, loading, runtime]);
React.useEffect(() => {
if (!open || !item?.id) return;
setVersions([]); setNextVersion(null); setError(null);
void load();
// Le chargement est déclenché par l’ouverture et l’identité, pas par son propre état.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, item?.id]);
return
{error && }
{!error && loading && versions.length === 0 && Chargement des versions…
}
{versions.map((version) =>
Version {version.version}
{version.status === 'active' ? 'ACTIVE' : 'INACTIVE'}
{accountPromptScopeLabel(version.scope)}
{acDate(version.updated_at)}
{version.content}
)}
{nextVersion !== null && }
;
}
function AccountPreferenceManager({ accent, runtime, onBack, onChanged }) {
const { Button, ChoiceChip, ScreenHeader, StatusNotice } = window.VascoUI;
const [contentStatus, setContentStatus] = React.useState('active');
const [items, setItems] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState(null);
const [editing, setEditing] = React.useState(null);
const [editorOpen, setEditorOpen] = React.useState(false);
const [historyItem, setHistoryItem] = React.useState(null);
const resource = runtime.prompts;
const load = React.useCallback(async () => {
try {
const response = await resource.list(contentStatus);
setItems(response.items); setError(null);
} catch (failure) {
setItems([]); setError(failure?.message || 'Chargement impossible');
}
}, [contentStatus, resource]);
React.useEffect(() => { setItems(null); void load(); }, [load]);
const openExisting = async (item) => {
if (busy) return;
setBusy(true);
try {
setEditing(await resource.get(item.id)); setEditorOpen(true);
} catch (failure) {
showToast(failure?.message || 'Ouverture impossible', 'error');
} finally { setBusy(false); }
};
const save = async (payload) => {
setBusy(true);
try {
if (editing?.id) await resource.update(editing.id, editing.version, payload);
else await resource.create(payload);
setEditorOpen(false); setEditing(null); await load();
if (onChanged) onChanged();
} catch (failure) {
showToast(failure?.message || 'Enregistrement impossible', 'error');
if (failure?.status === 409) { setEditorOpen(false); setEditing(null); await load(); }
} finally { setBusy(false); }
};
const toggleState = async (item) => {
if (busy) return;
setBusy(true);
try {
const state = item.status === 'active' ? 'archived' : 'active';
await resource.setState(item.id, item.version, state);
await load();
if (onChanged) onChanged();
} catch (failure) {
showToast(failure?.message || 'Action impossible', 'error');
if (failure?.status === 409) await load();
} finally { setBusy(false); }
};
return
{
setEditing(null); setEditorOpen(true);
}}>Ajouter} />
setContentStatus('active')}>Actives
setContentStatus('archived')}>Désactivées
{error &&
}
{items === null &&
}
{items !== null && items.length === 0 && !error &&
{contentStatus === 'active'
? 'Aucune préférence active. L’assistant peut en proposer pendant une conversation.'
: 'Aucune préférence désactivée.'}
}
{(items || []).map((item) =>
{item.name}
{accountPromptScopeLabel(item.scope)}
Version {item.version} · {item.content_length} caractères · {acDate(item.updated_at)}
{item.status === 'archived' &&
DÉSACTIVÉE}
)}
setEditorOpen(false)} onSave={(payload) => void save(payload)} />
setHistoryItem(null)} />
;
}
function AccountConnectorsSection({ connectors }) {
return <>
Chaque service conserve le statut réellement fourni par le serveur.
{connectors.map((connector) => >}
label={connector.display_name} sub="Connecteur indisponible"
reason={AC_REASONS[connector.reason_code] || 'Action indisponible.'} />)}
>;
}
function AccountSecuritySection({ capabilities, busy, onLogoutAll, onLogout }) {
const { Button } = window.VascoUI;
return <>
>}
label="Mot de passe" reason={acReason(capabilities.change_password)} />
}
label="Authentification 2FA" reason={acReason(capabilities.manage_mfa)} />
}
label="Abonnement" reason={acReason(capabilities.subscription)} />
>}
label="Facturation" reason={acReason(capabilities.billing)} />
>}
label="Exporter mes données" reason={acReason(capabilities.data_export)} />
}
label="Fermer mon compte" danger reason={acReason(capabilities.close_account)} />
>}
label="Déconnecter tous les appareils" sub="Révoque toutes les sessions, y compris celle-ci"
danger reason={acReason(capabilities.logout_all)} onClick={onLogoutAll} />
>}
label="Version de Vasco"
sub={`v${window.VASCO_VERSION || '0.0.0'} · Build ${window.VASCO_BUILD || '–'}`} />
>;
}
function AccountScreenConnected({ accent, onTabChange, startOnStats, onStatsClosed,
startOnReminders, onRemindersClosed,
runtime = null, onLoggedOut, clinicContext = null, onOpenClinicContext = null,
onOpenClinicInvitation = null, onManageOwnAssignments = null,
onManageInvitations = null, onManagePractitioners = null,
onOpenCorrespondents = null, initialSection = null }) {
const { Button } = window.VascoUI;
const [overview, setOverview] = React.useState(() => window.VASCO_RUNTIME?.account || null);
const [error, setError] = React.useState(null);
const [refreshing, setRefreshing] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const [section, setSection] = React.useState(initialSection);
const [showStats, setShowStats] = React.useState(!!startOnStats);
// La page Rappels vit à côté des Statistiques, jamais en même temps : les
// deux occupent la même surface pleine au-dessus du Compte.
const [showReminders, setShowReminders] = React.useState(!!startOnReminders);
const [contentScreen, setContentScreen] = React.useState(null);
const [navigationDirection, setNavigationDirection] = React.useState('none');
const [profileOpen, setProfileOpen] = React.useState(false);
const [logoutAllOpen, setLogoutAllOpen] = React.useState(false);
const [clinic, setClinic] = React.useState(null);
const [clinicOpening, setClinicOpening] = React.useState(false);
const [clinicError, setClinicError] = React.useState(null);
const [documentTypesOpen, setDocumentTypesOpen] = React.useState(false);
const returnFocusRef = React.useRef(null);
const secondaryHeadingRef = React.useRef(null);
React.useEffect(() => {
if (startOnStats) { setNavigationDirection('forward'); setShowStats(true); }
}, [startOnStats]);
React.useEffect(() => {
if (startOnReminders) { setNavigationDirection('forward'); setShowReminders(true); }
}, [startOnReminders]);
React.useEffect(() => { if (section) secondaryHeadingRef.current?.focus(); }, [section]);
const load = React.useCallback(async (force) => {
if (!runtime) return;
if (force) setRefreshing(true);
try { setOverview(force ? await runtime.reload() : await runtime.load()); setError(null); }
catch (requestError) { setError(requestError?.message || 'Compte indisponible'); }
finally { setRefreshing(false); }
}, [runtime]);
React.useEffect(() => { void load(false); }, [load]);
React.useEffect(() => {
const onContextChanged = () => {
setClinic(null);
setClinicError(null);
void load(true);
};
window.addEventListener('vasco:context-changed', onContextChanged);
return () => window.removeEventListener('vasco:context-changed', onContextChanged);
}, [load]);
const restoreAccountFocus = (saved = returnFocusRef.current) => {
window.setTimeout(() => {
const target = saved?.node?.isConnected
? saved.node : document.querySelector(`[data-account-return="${saved?.key || ''}"]`);
target?.focus();
}, window.VascoNavigationContract.DURATION_MS + 24);
};
const navigateAccount = (destination, direction, trigger = null) => {
if (trigger) returnFocusRef.current = {
node: trigger,
key: destination === 'stats' ? 'activity' : destination.replace(/^section:/, ''),
};
setNavigationDirection(direction);
if (destination === 'home') {
setContentScreen(null); setShowStats(false); setShowReminders(false); setSection(null); return;
}
if (destination === 'stats') {
setContentScreen(null); setSection(null); setShowReminders(false); setShowStats(true); return;
}
if (destination === 'reminders') {
setContentScreen(null); setSection(null); setShowStats(false); setShowReminders(true); return;
}
if (destination.startsWith('section:')) {
setContentScreen(null); setShowStats(false); setShowReminders(false);
setSection(destination.slice(8)); return;
}
if (destination.startsWith('content:')) setContentScreen(destination.slice(8));
};
const openSection = (next, trigger) => navigateAccount(`section:${next}`, 'forward', trigger);
const closeSection = () => { navigateAccount('home', 'back'); restoreAccountFocus(); };
const openStats = (trigger) => navigateAccount('stats', 'forward', trigger);
const openReminders = (trigger) => navigateAccount('reminders', 'forward', trigger);
const closeReminders = () => {
navigateAccount('home', 'back');
if (onRemindersClosed) onRemindersClosed();
restoreAccountFocus();
};
// Les rappels ne sont pas une donnée du Compte : ils vivent dans le tableau
// de bord, et la page les lit à leur source plutôt que de s'en faire servir
// une copie par un écran qui n'en a pas l'usage.
// La recherche de dossiers est servie par le tableau de bord, à côté des
// rappels : la page en a besoin pour rattacher un rappel à un patient.
const remindersRuntime = React.useMemo(() => {
const dashboard = typeof window !== 'undefined' ? window.VascoApi?.dashboard : null;
if (!dashboard?.reminders) return null;
return { ...dashboard.reminders, patientOptions: dashboard.patientOptions };
}, []);
const closeStats = () => {
navigateAccount('home', 'back');
if (onStatsClosed) onStatsClosed();
restoreAccountFocus();
};
const saveProfile = async (changes) => {
setBusy(true);
try { await runtime.updateProfile(overview.profile.version, changes); setProfileOpen(false); await load(true); }
catch (requestError) { showToast(requestError?.message || 'Enregistrement impossible', 'error');
if (requestError?.status === 409) { setProfileOpen(false); await load(true); } }
finally { setBusy(false); }
};
const openClinic = async () => {
if (!runtime || clinicOpening || busy) return;
setClinicOpening(true);
try { setClinic(await runtime.clinic.load()); setClinicError(null); }
catch (requestError) { showToast(requestError?.message || 'Fiche du cabinet indisponible', 'error'); }
finally { setClinicOpening(false); }
};
const saveClinic = async (fields) => {
setBusy(true);
try { await runtime.clinic.save(clinic.version, fields); setClinic(null); setClinicError(null); }
catch (requestError) {
const message = requestError?.message || 'Enregistrement impossible';
if (requestError?.status === 409) {
try { setClinic(await runtime.clinic.load()); setClinicError(`${message} La fiche affichée vient d’être rechargée depuis le serveur.`); }
catch (reloadError) { setClinic(null); showToast(reloadError?.message || message, 'error'); }
} else setClinicError(message);
} finally { setBusy(false); }
};
const toggleNotifications = async () => {
if (busy) return;
setBusy(true);
try { await runtime.updatePreferences(overview.preferences.version,
!overview.preferences.routine_in_app_notifications); await load(true); }
catch (requestError) { showToast(requestError?.message || 'Modification impossible', 'error');
if (requestError?.status === 409) await load(true); }
finally { setBusy(false); }
};
const confirmLogoutAll = async () => {
setBusy(true);
try { await runtime.logoutAll(); setLogoutAllOpen(false); if (onLoggedOut) onLoggedOut(); }
catch (requestError) { showToast(requestError?.message || 'Révocation impossible', 'error'); }
finally { setBusy(false); }
};
const doLogout = async () => {
if (busy) return;
setBusy(true);
try { await window.VascoApi.logout(); }
catch { window.VascoApi.clearLocalSession(); }
finally { setBusy(false); if (onLoggedOut) onLoggedOut(); }
};
if (!overview) return
{!error &&
}
{error ? 'Compte indisponible' : 'Chargement de votre compte…'}
{error && <>
{error}
>}
;
const profile = overview.profile;
const capabilities = overview.capabilities;
const initials = `${profile.given_name[0] || ''}${profile.family_name[0] || ''}`.toUpperCase();
const activeOrganizationLabel = clinicContext?.department
? clinicContext.organization?.name || null
: null;
const activePracticeLabel = clinicContext?.department
? [clinicContext.clinic?.name, clinicContext.department.name].filter(Boolean).join(' · ')
: null;
let sectionContent = null;
if (section === 'profile') sectionContent = setProfileOpen(true)} onToggleNotifications={toggleNotifications} />;
if (section === 'clinic') sectionContent = void openClinic()}
onOpenClinicContext={onOpenClinicContext} onOpenClinicInvitation={onOpenClinicInvitation}
onManageOwnAssignments={onManageOwnAssignments}
onManageInvitations={onManageInvitations}
onManagePractitioners={onManagePractitioners}
onOpenCorrespondents={onOpenCorrespondents}
onOpenDocumentTypes={() => setDocumentTypesOpen(true)} />;
if (section === 'assistant') sectionContent = onTabChange?.('IA', { openHistory: true })}
onOpenPrompts={() => navigateAccount('content:prompts', 'forward')}
onOpenTemplates={() => navigateAccount('content:templates', 'forward')} />;
if (section === 'connectors') sectionContent = ;
if (section === 'security') sectionContent = setLogoutAllOpen(true)} onLogout={doLogout} />;
const accountScreenKey = contentScreen ? `content:${contentScreen}`
: showStats ? 'stats' : showReminders ? 'reminders'
: section ? `section:${section}` : 'home';
let accountScreen = null;
if (contentScreen) accountScreen = contentScreen === 'prompts' ? {
navigateAccount('section:assistant', 'back');
window.setTimeout(() => secondaryHeadingRef.current?.focus(),
window.VascoNavigationContract.DURATION_MS + 24);
}} onChanged={() => void load(true)} /> : {
navigateAccount('section:assistant', 'back');
window.setTimeout(() => secondaryHeadingRef.current?.focus(),
window.VascoNavigationContract.DURATION_MS + 24);
}} onChanged={() => void load(true)} />;
else if (showStats) accountScreen = ;
else if (showReminders) accountScreen = ;
else if (section) accountScreen = {sectionContent};
else accountScreen = <>
{error &&
{error}
}
{typeof BottomNav !== 'undefined' && { if (onTabChange) onTabChange(id); }} accent={accent} />}
>;
return
{accountScreen}
{profileOpen &&
setProfileOpen(false)} onSave={saveProfile} />}
{clinic && { setClinic(null); setClinicError(null); }} onSave={saveClinic} />}
setDocumentTypesOpen(false)} />
{logoutAllOpen && setLogoutAllOpen(false)} onConfirm={confirmLogoutAll} />}
;
}
AccountScreenConnected.acceptsClinicContext = true;
AccountScreenConnected.acceptsCorrespondents = true;
AccountScreen = AccountScreenConnected;
window.AccountScreen = AccountScreenConnected;