// 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
{title}
{children}
; } 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} />