// Extension Web de l'annuaire. Les trois liaisons globales sont résolues au // rendu par la coque et les composants du snapshot : aucun fichier de référence // n'est modifié. const CORR_INK = window.VT?.ink || '#0a0a0a'; // Le papier des écrans du Compte : `surface` est le blanc des cartes, jamais le // fond sur lequel elles se posent. const CORR_PAPER = window.VT?.paper || '#f6f5f1'; const CORR_ACCENT = '#3d6b8f'; const CorrespondentsBaseAccountScreen = AccountScreen; const CorrespondentSearchControl = window.VascoUI?.CreatableSelect; const CorrespondentChoiceControl = window.VascoUI?.SimpleSelect; function correspondentError(error, fallback) { return window.VascoCorrespondents.errorMessage(error, fallback); } function correspondentSubtitle(item) { return [item.specialty, item.organization].filter(Boolean).join(' · ') || 'Coordonnées professionnelles'; } // Ce qu'une ligne d'annuaire dit sous le nom. « Coordonnées professionnelles » // était écrit sur chacune : répété huit fois, il n'apprenait rien. La ligne dit // désormais ce qui distingue la fiche, spécialité, établissement, qualité de la // correspondance, et se tait quand elle n'a rien à dire. function correspondentRowSub(item) { const parts = []; const identity = [item.specialty, item.organization].filter(Boolean).join(' · '); if (identity) parts.push(identity); if (item.match === 'exact') parts.push('Correspondance exacte'); if (item.match === 'approximate') parts.push('Correspondance approchée'); return parts.join(' · ') || null; } // Les champs de l'annuaire reprennent la mesure des autres écrans : même // hauteur de contrôle, même arrondi, même filet que le socle. function correspondentFieldStyle() { const tokens = window.VascoUI.tokens; return { width: '100%', minHeight: tokens.control.large, padding: '0 12px', borderRadius: tokens.radius.md, border: `1px solid ${tokens.color.borderStrong}`, background: tokens.color.surface, color: tokens.color.ink, fontFamily: tokens.type.body, fontSize: 14, fontWeight: 500, outline: 'none', }; } function correspondentCaptionStyle() { const tokens = window.VascoUI.tokens; return { display: 'block', marginBottom: 6, ...tokens.type.caption, color: tokens.color.secondary, textTransform: 'uppercase', }; } function correspondentNoticeStyle() { const tokens = window.VascoUI.tokens; return { padding: '28px 20px', textAlign: 'center', color: tokens.color.secondary, ...tokens.type.supporting, }; } // La recherche patient unifiée rend les dossiers tels que le serveur les nomme : // `family_name`, `given_name`, `medical_record_number`. L'annuaire les lit comme // le reste de l'application : le nom d'abord, l'IPP en dessous. function correspondentPatientLabel(item) { if (!item) return 'Dossier sans nom'; const name = [item.family_name, item.given_name].filter(Boolean).join(' ').trim(); return name || item.medical_record_number || 'Dossier sans nom'; } function correspondentPatientMeta(item) { return [ item.medical_record_number ? `IPP ${item.medical_record_number}` : null, Number.isFinite(item.age) ? `${item.age} ans` : null, ].filter(Boolean).join(' · '); } const correspondentSelectCache = { items: null, promise: null }; function preloadCorrespondents(runtime) { if (correspondentSelectCache.items) return Promise.resolve(correspondentSelectCache.items); if (!correspondentSelectCache.promise) { correspondentSelectCache.promise = runtime.list({ limit: 100 }) .then((page) => { correspondentSelectCache.items = page.items; return page.items; }) .catch((failure) => { correspondentSelectCache.promise = null; throw failure; }); } return correspondentSelectCache.promise; } function correspondentOptions(items, current, selected) { const values = [...items]; if (current && !values.some((item) => item.id === current.id)) values.unshift(current); if (selected) { const selectedIndex = values.findIndex((item) => item.id === selected.id); if (selectedIndex >= 0) values.splice(selectedIndex, 1); values.unshift(selected); } return values; } function rememberCorrespondent(item) { if (!correspondentSelectCache.items) return; correspondentSelectCache.items = [ item, ...correspondentSelectCache.items.filter((existing) => existing.id !== item.id), ]; } function CorrespondentSelect({ id, value, onChange, current = null, label = 'Non renseigné', ...accessibility }) { const runtime = window.VascoApi.correspondents; const [items, setItems] = React.useState(() => current ? [current] : []); const [selectedItem, setSelectedItem] = React.useState(() => current?.id === value ? current : null); const [query, setQuery] = React.useState(''); const [open, setOpen] = React.useState(false); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const [editor, setEditor] = React.useState(null); const [editorError, setEditorError] = React.useState(null); const [saving, setSaving] = React.useState(false); const selected = (selectedItem?.id === value ? selectedItem : null) || items.find((item) => item.id === value) || (current?.id === value ? current : null); React.useEffect(() => { if (current?.id === value) setSelectedItem(current); }, [current?.id, value]); React.useEffect(() => { let active = true; setLoading(true); preloadCorrespondents(runtime) .then((values) => { if (active) setItems(correspondentOptions(values, current, selected)); }) .catch((failure) => { if (active) setError(correspondentError(failure, 'Annuaire indisponible')); }) .finally(() => { if (active) setLoading(false); }); return () => { active = false; }; }, [current?.id]); React.useEffect(() => { if (!open) return undefined; if (!query.trim()) { if (correspondentSelectCache.items) { setItems(correspondentOptions(correspondentSelectCache.items, current, selected)); } return undefined; } const controller = new AbortController(); const timer = window.setTimeout(() => { setLoading(true); setError(null); runtime.list({ query: query.trim(), limit: 20, signal: controller.signal }) .then((page) => setItems(page.items)) .catch((failure) => setError(correspondentError(failure, 'Annuaire indisponible'))) .finally(() => setLoading(false)); }, runtime.debounceMs); return () => { window.clearTimeout(timer); controller.abort(); }; }, [open, query, current?.id, value]); const choose = (nextValue) => { const chosen = items.find((item) => item.id === nextValue) || (current?.id === nextValue ? current : null); setSelectedItem(chosen); onChange(nextValue || null, chosen); setQuery(''); setOpen(false); setError(null); }; const openCreate = (displayName = query.trim()) => { const form = window.VascoCorrespondents.initialForm({ display_name: displayName }); setEditorError(null); setEditor({ record: null, baseline: form, form, attempt: runtime.newAttempt() }); setOpen(false); }; const changeEditor = (form) => setEditor((active) => ( window.VascoCorrespondents.changeEditor(active, form, runtime.newAttempt) )); const create = async (analysis) => { setSaving(true); setEditorError(null); try { const created = await runtime.create(analysis.body, editor.attempt); rememberCorrespondent(created); setItems((existing) => [created, ...existing.filter((item) => item.id !== created.id)]); setSelectedItem(created); onChange(created.id, created); setQuery(''); setEditor(null); } catch (failure) { setEditorError(correspondentError(failure, 'Création impossible')); } finally { setSaving(false); } }; const selectOptions = [ ...(!query.trim() ? [{ id: '', displayName: label, specialty: '', organization: '' }] : []), ...items, ]; return <> item.id} getOptionLabel={(item) => item.displayName} getOptionMeta={(item) => item.id ? correspondentSubtitle(item) : ''} getOptionDisabled={(item) => Boolean(item.archived)} onChange={(nextValue) => choose(nextValue)} onCreate={openCreate} allowEmptyCreate createLabel={(name) => name ? `Ajouter « ${name} »` : 'Ajouter un correspondant'} /> {editor && { if (!saving) setEditor(null); }} onSave={create} saving={saving} error={editorError} />} ; } window.VascoCorrespondentSelect = CorrespondentSelect; // ── Associer un correspondant à un patient ──────────────────────────────── // // Le lien existait déjà, mais dans un seul sens : on ouvrait le dossier d'un // patient pour y désigner son médecin traitant. Depuis l'annuaire, c'est le // geste inverse, « ce docteur suit un tel », et il obligeait jusqu'ici à quitter // l'écran, retrouver le dossier, puis rouvrir sa modification. // // Rien de neuf côté serveur : c'est le même champ du même dossier // (`primary_physician_correspondent_id` ou `referred_by_correspondent_id`), // posé par le PATCH versionné de la modification de dossier. // // Chaque rôle porte ses propres mots : « ce dossier a déjà un adressé par » ne // se dit pas, et la phrase de réussite se lit dans le sens du soin. const CORRESPONDENT_ROLES = Object.freeze([ Object.freeze({ field: 'primary_physician_correspondent_id', label: 'Médecin traitant', detailKey: 'primaryCorrespondentId', held: 'Ce dossier a déjà un médecin traitant. L’associer le remplacera.', done: (correspondent, patient) => `${correspondent} est médecin traitant de ${patient}.`, }), Object.freeze({ field: 'referred_by_correspondent_id', label: 'Adressé par', detailKey: 'referredByCorrespondentId', held: 'Ce dossier porte déjà un correspondant adresseur. L’associer le remplacera.', done: (correspondent, patient) => `${patient} est adressé par ${correspondent}.`, }), ]); // Les deux populations de la recherche patient, celles de l'agenda. « Vivants » // à l'ouverture : viser un dossier décédé reste un geste délibéré. const CORRESPONDENT_PATIENT_SCOPES = Object.freeze([ Object.freeze({ id: 'living', label: 'Vivants' }), Object.freeze({ id: 'deceased', label: 'Décédés' }), ]); function CorrespondentPatientLink({ open, record, onClose, onDone }) { const { BottomSheet, Button, ChoiceChip, SearchSelect, StatusNotice } = window.VascoUI; const search = window.VascoApi.search; const patients = window.VascoApi.patient; const [query, setQuery] = React.useState(''); const [scope, setScope] = React.useState(CORRESPONDENT_PATIENT_SCOPES[0].id); const [options, setOptions] = React.useState([]); const [loading, setLoading] = React.useState(false); const [choice, setChoice] = React.useState(null); const [detail, setDetail] = React.useState(null); const [role, setRole] = React.useState(CORRESPONDENT_ROLES[0].field); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(null); const attempt = React.useRef(window.VascoApi.correspondents.newAttempt()); // La feuille reste montée pour garder son animation de sortie. Elle repart // donc vierge à chaque ouverture, plutôt qu'avec la recherche précédente. React.useEffect(() => { if (!open) return; setQuery(''); setOptions([]); setChoice(null); setDetail(null); setRole(CORRESPONDENT_ROLES[0].field); setScope(CORRESPONDENT_PATIENT_SCOPES[0].id); setBusy(false); setError(null); attempt.current = window.VascoApi.correspondents.newAttempt(); }, [open, record?.id]); // La recherche est celle de toute l'application, à sa cadence, et le serveur // applique le même prédicat tolérant aux fautes qu'ailleurs. React.useEffect(() => { if (!open) return undefined; const timer = window.setTimeout(() => { setLoading(true); setError(null); search.patientOptions(query, { clinicalState: scope }) .then((items) => setOptions(items)) .catch((failure) => setError(correspondentError(failure, 'Recherche indisponible'))) .finally(() => setLoading(false)); }, search.debounceMs); return () => window.clearTimeout(timer); }, [open, query, scope]); // Le dossier est relu au choix : le PATCH a besoin de sa version, et l'écran // de savoir si la place visée est déjà prise. const choosePatient = async (value) => { const picked = options.find((item) => String(item.id) === String(value)) || null; setChoice(picked); setDetail(null); setError(null); if (!picked) return; setBusy(true); try { setDetail(await patients.load(picked.id)); } catch (failure) { setError(correspondentError(failure, 'Ce dossier n’a pas pu être relu')); } finally { setBusy(false); } }; const chosenRole = CORRESPONDENT_ROLES.find((item) => item.field === role); // Ce que ce dossier porte déjà à cette place. On le dit avant de remplacer : // un médecin traitant qui disparaît sans un mot serait une perte silencieuse. const held = Boolean(detail && record && detail[chosenRole.detailKey] && detail[chosenRole.detailKey] !== record.id); const already = Boolean(detail && record && detail[chosenRole.detailKey] === record.id); const associate = async () => { if (!detail || !record || already) return; setBusy(true); setError(null); try { await patients.update(detail, { [role]: record.id }, attempt.current); setBusy(false); onDone(chosenRole.done(record.displayName, correspondentPatientLabel(choice))); } catch (failure) { attempt.current = window.VascoApi.correspondents.newAttempt(); if (failure?.status === 409) { try { setDetail(await patients.load(detail.id)); setError('Ce dossier a été modifié entre-temps. Vérifiez la place visée puis réessayez.'); } catch (reloadFailure) { setError(`${correspondentError(reloadFailure, 'Conflit de version')} La version actuelle n’a pas pu être relue.`); } } else setError(correspondentError(failure, 'Association impossible')); setBusy(false); } }; return }>
Patient void choosePatient(value)} query={query} onQueryChange={setQuery} filterOptions={false} scopes={CORRESPONDENT_PATIENT_SCOPES} scope={scope} onScopeChange={setScope} scopesLabel="Population cherchée" searchPlaceholder="Rechercher un nom ou un IPP" loading={loading} placeholder="Choisir un dossier" options={options.map((item) => [ String(item.id), correspondentPatientLabel(item), correspondentPatientMeta(item), ])} />
Rôle dans le dossier
{CORRESPONDENT_ROLES.map((item) => ( setRole(item.field)} style={{ flex: '1 1 0' }}>{item.label} ))}
{already && } {held && } {error && }
; } function CorrespondentEditor({ editor, onChange, onClose, onSave, saving, error, onArchive = null, onLink = null, }) { const form = editor.form; const { FormSheet, Button, Field, StatusNotice } = window.VascoUI; // Les champs passent par la primitive officielle : même libellé, même // contrôle, même mesure que partout ailleurs dans l'application. const field = (name, label, props = {}) => ( onChange({ ...form, [name]: event.target.value })} style={correspondentFieldStyle()} /> ); const analysis = editor.record ? window.VascoCorrespondents.buildPatch(editor.record, form) : window.VascoCorrespondents.buildCreate(form); const enabled = Boolean(analysis.body) && !saving; return }> {field('display_name', 'Nom affiché *', { autoFocus: true, placeholder: 'Dr Claire Dupont' })} {field('specialty', 'Spécialité', { placeholder: 'Cardiologie' })} {field('organization', 'Établissement', { placeholder: 'Clinique…' })} {field('email', 'E-mail professionnel', { type: 'email', placeholder: 'adresse@exemple.fr' })} {field('phone', 'Téléphone professionnel', { type: 'tel' })} {field('postal_address', 'Adresse postale')} {Object.values(analysis.errors || {}).map((message) => (
))} {error &&
} {/* Associer se lit d'abord : c'est ce qu'on vient faire depuis l'annuaire. Archiver reste au bout, en rouge, comme une sortie. Cette action ne se répète plus sur chacune des lignes de la liste : posée huit fois en rouge, elle se touche un jour par erreur, et le rouge de cette application est réservé au danger. */} {(onLink || onArchive) && (
{onLink && } {onArchive && ( )}
)}
; } function CorrespondentsDirectory({ onBack, onTabChange = null, accent = CORR_ACCENT }) { const { Button, IconButton, InlineAction, StatusNotice, SettingGroup, SettingRow, ScreenHeader, ConfirmDialog, } = window.VascoUI; const runtime = window.VascoApi.correspondents; const [query, setQuery] = React.useState(''); // Décision de Sfar : l'annuaire ne montre plus que les fiches vivantes. Une // fiche archivée ne se choisit plus nulle part, il n'a pas à la montrer, et // l'interrupteur qui le réglait disparaît avec elle. const includeArchived = false; const [page, setPage] = React.useState({ items: [], nextCursor: null, hasMore: false }); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [editor, setEditor] = React.useState(null); const [editorError, setEditorError] = React.useState(null); const [saving, setSaving] = React.useState(false); // La feuille d'association et la confirmation d'archivage vivent à côté de la // fiche, jamais dedans : imbriquées, elles se poseraient dans le formulaire au // lieu de le recouvrir. Elles gardent leur fiche le temps de leur sortie. const [linking, setLinking] = React.useState({ record: null, open: false }); const [confirming, setConfirming] = React.useState({ record: null, open: false }); const [archiving, setArchiving] = React.useState(false); const archiveAttempts = React.useRef(new Map()); const load = React.useCallback(async ({ cursor = null, signal = null } = {}) => { if (!cursor) setLoading(true); setError(null); try { const next = await runtime.list({ query, includeArchived, limit: 20, cursor, signal }); setPage((current) => cursor ? window.VascoCorrespondents.mergePage(current, next) : next); } catch (failure) { const message = correspondentError(failure, 'Annuaire indisponible'); if (message) setError(message); } finally { if (!cursor) setLoading(false); } }, [query]); React.useEffect(() => { const controller = new AbortController(); setPage({ items: [], nextCursor: null, hasMore: false }); setLoading(true); const timer = window.setTimeout(() => void load({ signal: controller.signal }), runtime.debounceMs); return () => { window.clearTimeout(timer); controller.abort(); }; }, [load]); const openCreate = () => { setEditorError(null); setEditor({ record: null, baseline: window.VascoCorrespondents.initialForm(), form: window.VascoCorrespondents.initialForm(), attempt: runtime.newAttempt(), }); }; const openEdit = (record) => { setEditorError(null); setEditor(window.VascoCorrespondents.openEditor(record, runtime.newAttempt)); }; const changeEditor = (form) => setEditor((current) => ( window.VascoCorrespondents.changeEditor(current, form, runtime.newAttempt) )); const save = async (analysis) => { setSaving(true); setEditorError(null); try { if (editor.record) { const { expected_version: _version, ...changes } = analysis.body; await runtime.update(editor.record, changes, editor.attempt); } else { await runtime.create(analysis.body, editor.attempt); } setEditor(null); await load(); } catch (failure) { if (failure?.status === 409 && failure?.code !== 'correspondent_duplicate' && editor.record) { try { const freshPage = await runtime.list({ query: editor.record.displayName, includeArchived: true, limit: 100 }); const fresh = freshPage.items.find((item) => item.id === editor.record.id); if (fresh) { setEditor((current) => window.VascoCorrespondents.rebaseEditor(current, fresh, runtime.newAttempt)); setEditorError('Cette fiche a été modifiée entre-temps. Vos saisies sont conservées : vérifiez-les puis réessayez.'); } else setEditorError('Cette fiche n’existe plus. Vos saisies sont conservées.'); } catch (reloadFailure) { setEditorError(`${correspondentError(failure, 'Conflit de version')} La version actuelle n’a pas pu être relue.`); } } else setEditorError(correspondentError(failure, 'Enregistrement impossible')); } finally { setSaving(false); } }; // L'archivage est demandé depuis la fiche et confirmé par le dialogue du // socle : plus de fenêtre du navigateur, qui ne ressemble à aucun écran. const archive = async (record) => { if (!record) return; const attempt = archiveAttempts.current.get(record.id) || runtime.newAttempt(); archiveAttempts.current.set(record.id, attempt); setArchiving(true); try { await runtime.archive(record, attempt); archiveAttempts.current.delete(record.id); setConfirming((current) => ({ ...current, open: false })); setEditor(null); await load(); showToast(`${record.displayName} est archivé.`, 'success'); } catch (failure) { setConfirming((current) => ({ ...current, open: false })); setEditorError(correspondentError(failure, 'Archivage impossible')); if (failure?.status === 409) { await load(); archiveAttempts.current.delete(record.id); } } finally { setArchiving(false); } }; const state = window.VascoCorrespondents.listState({ items: page.items, loading, error }); return ( // L'annuaire est un écran du Compte : il en reprend la mise en page, un // en-tête officiel, des groupes en cartes, la barre d'onglets qui reste, au // lieu de recouvrir l'application avec sa propre grammaire.
} />
setQuery(event.target.value)} placeholder="Rechercher un nom, une spécialité…" aria-label="Rechercher un correspondant" style={correspondentFieldStyle()} />
{state === 'loading' && (
Chargement de l’annuaire…
)} {state === 'error' && (
load()}>Réessayer
)} {state === 'empty' && (
Aucun correspondant trouvé.
)} {page.items.length > 0 && ( {page.items.map((item) => ( } label={item.displayName} sub={correspondentRowSub(item)} onClick={() => openEdit(item)} /> ))} )} {/* Une erreur survenue alors que la liste est déjà à l'écran ne la remplace pas : elle se pose dessous, là où elle se lit sans faire disparaître ce qu'on regardait. */} {error && page.items.length > 0 && (
)} {page.hasMore && (
)}
{typeof BottomNav !== 'undefined' && { if (onTabChange) onTabChange(id); }} accent={accent} />} {editor && { if (!saving) setEditor(null); }} onSave={save} saving={saving} error={editorError} onLink={editor.record ? () => setLinking({ record: editor.record, open: true }) : null} onArchive={editor.record ? () => setConfirming({ record: editor.record, open: true }) : null} />} { if (!archiving) setLinking((current) => ({ ...current, open: false })); }} onDone={(message) => { setLinking((current) => ({ ...current, open: false })); showToast(message, 'success'); }} /> { if (!archiving) setConfirming((current) => ({ ...current, open: false })); }} onConfirm={() => void archive(confirming.record)} />
); } function CorrespondentsAccountScreen(props) { const { Button } = window.VascoUI; const [open, setOpen] = React.useState(false); const [direction, setDirection] = React.useState('none'); const [returnSection, setReturnSection] = React.useState(null); const integrated = Boolean(CorrespondentsBaseAccountScreen.acceptsCorrespondents); const openDirectory = (section = null) => { setReturnSection(section); setDirection('forward'); setOpen(true); }; const closeDirectory = () => { setDirection('back'); setOpen(false); }; const account = integrated ? openDirectory('clinic')} /> : ; return (
{open ? : account} {!integrated && !open && }
); } CorrespondentsAccountScreen.acceptsClinicContext = Boolean(CorrespondentsBaseAccountScreen.acceptsClinicContext); CorrespondentsAccountScreen.acceptsCorrespondents = Boolean(CorrespondentsBaseAccountScreen.acceptsCorrespondents); function CorrespondentsLetterWidget({ data, actions, footer = null }) { const { Button } = window.VascoUI; const msg = actions?.props?.msg; const runtime = actions?.props?.runtime; const onWidgetUpdated = actions?.props?.onWidgetUpdated; const [items, setItems] = React.useState([]); const [directoryLoading, setDirectoryLoading] = React.useState(true); const [selected, setSelected] = React.useState(data.correspondentId || ''); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(null); const baseline = React.useRef(data.correspondentId || ''); const attempt = React.useRef(window.VascoApi.correspondents.newAttempt()); React.useEffect(() => { const previous = baseline.current; baseline.current = data.correspondentId || ''; setSelected((current) => current === previous ? baseline.current : current); }, [data.correspondentId]); React.useEffect(() => { const controller = new AbortController(); window.VascoApi.correspondents.list({ includeArchived: true, limit: 100, signal: controller.signal }) .then((result) => setItems(result.items)) .catch((failure) => setError(correspondentError(failure, 'Annuaire indisponible'))) .finally(() => setDirectoryLoading(false)); return () => controller.abort(); }, []); const change = (value) => { setSelected(value); setError(null); attempt.current = window.VascoApi.correspondents.newAttempt(); }; const save = async () => { if (!runtime || !msg?.widgetId || saving || selected === baseline.current) return; setSaving(true); setError(null); try { const result = await runtime.widgetAction( { id: msg.widgetId, version: msg.widgetVersion, type: 'courrier' }, 'edit', { replacement: window.VascoCorrespondents.letterReplacement(data, selected || null), idempotencyKey: attempt.current.idempotencyKey, }, ); onWidgetUpdated?.(result.widget); } catch (failure) { if (failure?.status === 409 && msg.conversationId) { try { const conversation = await runtime.getConversation(msg.conversationId); const fresh = conversation.messages.find((message) => message.widgetId === msg.widgetId); if (fresh) onWidgetUpdated?.({ id: fresh.widgetId, data: fresh.data, version: fresh.widgetVersion, status: fresh.widgetStatus, capabilities: fresh.widgetCapabilities, }); attempt.current = window.VascoApi.correspondents.newAttempt(); setError('Ce courrier a été modifié entre-temps. Votre destinataire est conservé : vérifiez puis réessayez.'); } catch { setError('Conflit de version. Votre destinataire est conservé, mais le courrier n’a pas pu être rechargé.'); } } else setError(correspondentError(failure, 'Modification impossible')); } finally { setSaving(false); } }; const options = items; return (
Courrier confrère {actions}
{selected !== baseline.current && } {error &&
{error}
}
À : {data.to}
Objet : {data.subject}
{data.body}
Envoi indisponible : aucun canal sécurisé MSSanté ou Apicrypt n’est configuré.
{footer}
); } AccountScreen = CorrespondentsAccountScreen; window.AccountScreen = CorrespondentsAccountScreen; WidgetCourrier = CorrespondentsLetterWidget; window.WidgetCourrier = CorrespondentsLetterWidget; // La création patient V5 demandait encore un nom libre. On conserve la // maquette figée et remplace uniquement ce champ par l'annuaire partagé. Le // serveur reçoit l'identifiant ; il porte lui-même le nom officiel dans le // dossier afin de ne pas entretenir deux valeurs concurrentes. const CorrespondentsPatientWizardBaseStep1 = Step1; const CorrespondentsPatientWizardBasePayload = wzPayload; WZ_EMPTY.primaryPhysicianCorrespondentId = ''; WZ_EMPTY.primaryPhysicianCorrespondent = null; function correspondentPatientWizardReplace(node, props) { if (!React.isValidElement(node)) return node; if (node.props?.placeholder === 'Dr. ...') { return { props.set('primaryPhysicianCorrespondentId', value || ''); props.set('primaryPhysicianCorrespondent', item || null); }} />; } const content = React.Children.toArray(node.props?.children); if (!content.length) return node; return React.cloneElement( node, node.props, content.map((child) => correspondentPatientWizardReplace(child, props)), ); } function CorrespondentsPatientWizardStep1(props) { return correspondentPatientWizardReplace(CorrespondentsPatientWizardBaseStep1(props), props); } function CorrespondentsPatientWizardPayload(data) { const payload = CorrespondentsPatientWizardBasePayload(data); if (!payload) return payload; const correspondentId = String(data.primaryPhysicianCorrespondentId || '').trim(); return { ...payload, primary_physician: correspondentId ? null : payload.primary_physician, primary_physician_correspondent_id: correspondentId || null, }; } Step1 = CorrespondentsPatientWizardStep1; wzPayload = CorrespondentsPatientWizardPayload;