// Extension connectée de la feuille d'édition du snapshot. PatientDetail appelle // `EditPatientSheet` au moment du rendu : remplacer cette liaison après le // chargement de la référence garde celle-ci figée, sans recopier son écran. const CONNECTED_EDIT_STATUSES = [ ['ambulatory', 'Ambulatoire'], ['hospitalized', 'Hospitalisé'], ['icu', 'Réa'], ['palliative', 'Soins palliatifs'], ]; const CONNECTED_EDIT_BED_STATUSES = new Set(['hospitalized', 'icu', 'palliative']); const CONNECTED_EDIT_SEXES = [ ['female', 'Femme'], ['male', 'Homme'], ['other', 'Autre'], ]; const CONNECTED_WIZARD_SEX_VALUES = { M: 'male', F: 'female', Autre: 'other' }; const CONNECTED_WIZARD_SEX_LABELS = { male: 'M', female: 'F', other: 'Autre' }; const CONNECTED_BLOOD_TYPES = ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-']; const CONNECTED_BLOOD_TYPE_OPTIONS = [ ['', 'Non renseigné'], ...CONNECTED_BLOOD_TYPES.map((bloodType) => [bloodType, bloodType]), ]; const PE_VT = window.VT || {}; const PE_INK = PE_VT.ink || '#0a0a0a'; const PE_SUB = PE_VT.sub || '#6b7280'; const PE_HAIR = PE_VT.hair || 'rgba(10,10,10,0.08)'; const PE_HAIR2 = PE_VT.hair2 || 'rgba(10,10,10,0.14)'; const PE_SURFACE = PE_VT.surface || '#f6f5f1'; const PE_DANGER = PE_VT.danger || '#b91c1c'; const PE_ACCENT = '#3d6b8f'; const CorrespondentSelectControl = window.VascoCorrespondentSelect; function PatientEditField({ id, label, required = false, full = false, error, hint, children }) { const describedBy = [hint ? `${id}-hint` : null, error ? `${id}-error` : null].filter(Boolean).join(' '); return (
{React.cloneElement(children, { id, 'aria-required': required || undefined, 'aria-invalid': error ? 'true' : undefined, 'aria-describedby': describedBy || undefined, })} {hint &&
{hint}
} {error && }
); } const patientEditControlStyle = (multiline = false) => ({ width: '100%', minHeight: multiline ? 74 : 42, resize: multiline ? 'vertical' : undefined, background: '#fff', border: `1px solid ${PE_HAIR2}`, borderRadius: 16, outline: 'none', padding: multiline ? '9px 12px' : '0 12px', color: PE_INK, fontFamily: 'inherit', fontSize: 14, lineHeight: multiline ? 1.45 : undefined, }); // ── Les traitements, chacun sa fiche ────────────────────────────────────── // // La zone de texte demandait au praticien d'écrire ses dates lui-même, chacun // à sa façon – et personne ne le faisait. Chaque traitement a maintenant sa // carte : son libellé, son début, sa fin, et de quoi le retirer. Le serveur // porte les trois : rien ne s'écrit dans le texte du traitement. function PatientTreatmentRow({ treatment, index, todayKey, onChange, onRemove }) { const { IconButton, InlineAction, DayPicker } = window.VascoUI; const reversed = window.VascoPatientEdit.treatmentDatesReversed(treatment); const set = (patch) => onChange({ ...treatment, ...patch }); return ( // Chaque rangée déclare `minWidth: 0` : sans cela, la largeur naturelle du // champ de saisie – une vingtaine de caractères – devient le minimum de la // carte, et la carte déborde du téléphone en emportant la croix de retrait.
set({ label: event.target.value })} style={{ ...patientEditControlStyle(), minHeight: 38, flex: 1, minWidth: 0 }} />
{/* Les deux dates partagent une rangée quand la carte est assez large ; sinon elles s'empilent. « 16 sept. 2026 » ne tient pas dans une demi-carte de téléphone : coupé, il ne dit plus quelle date c'est. */}
set({ startsOn: value })} />
set({ endsOn: value })} />
{/* Un traitement au long cours n'a pas de fin : on doit pouvoir la retirer, pas seulement la changer. */} {(treatment.startsOn || treatment.endsOn) && (
{treatment.startsOn && ( set({ startsOn: '' })}>Sans début )} {treatment.endsOn && ( set({ endsOn: '' })}>Sans fin )}
)} {reversed && (
La fin est avant le début.
)}
); } function PatientTreatmentsEditor({ value, error, onChange }) { const { Button } = window.VascoUI; const treatments = window.VascoPatientEdit.treatmentRows(value); const todayKey = window.VascoTreatmentStops?.todayIso?.() || null; const replace = (index, treatment) => onChange(treatments.map( (current, position) => (position === index ? treatment : current), )); return (
{treatments.map((treatment, index) => ( replace(index, next)} onRemove={() => onChange(treatments.filter((_, position) => position !== index))} /> ))} {!treatments.length && (
Aucun traitement enregistré.
)}
{error &&
{error}
}
); } // Les écrans historiques consomment désormais la même primitive officielle. // Cette liaison globale disparaîtra avec leurs derniers adaptateurs. const PatientChoiceControl = window.VascoUI.SimpleSelect; window.VascoPatientChoiceSelect = window.VascoUI.SimpleSelect; function ConnectedEditPatientSheet({ p, accent, runtime, onClose, onSaved, scope = 'all' }) { const { BottomSheet, Button } = window.VascoUI; const effectiveScope = React.useMemo(() => ( window.VascoPatientSectionEdit?.consume(p.id, scope) || scope ), [p.id, scope]); const initial = React.useMemo(() => window.VascoPatientEdit.initialForm(p), [p]); // Ne jamais resynchroniser cet état sur `initial` : un rechargement après // conflit actualise la version du dossier, mais la saisie du praticien reste. const [form, setForm] = React.useState(initial); const [saving, setSaving] = React.useState(false); const [fieldErrors, setFieldErrors] = React.useState({}); const [error, setError] = React.useState(null); // Une coupure réseau rejoue exactement la même tentative. Toute nouvelle // saisie – ou le rechargement réussi après conflit – ouvre une autre clé. const [attempt, setAttempt] = React.useState(() => window.VascoApi.correspondents.newAttempt()); const analysis = window.VascoPatientEdit.buildChanges(initial, form); const canDismissFromBackdrop = !saving && !analysis.dirty; const identityOnly = effectiveScope === 'identity' || effectiveScope === 'all'; const clinicalOnly = effectiveScope === 'clinical'; const contactOnly = effectiveScope === 'contact'; const treatmentsOnly = effectiveScope === 'treatments'; const scopeTitle = identityOnly ? 'Identité et statut' : clinicalOnly ? 'Informations cliniques' : contactOnly ? 'Coordonnées' : 'Traitements'; const set = (key, value) => { setForm((current) => ({ ...current, [key]: value })); setAttempt(window.VascoApi.correspondents.newAttempt()); setFieldErrors((current) => { if (!current[key]) return current; const next = { ...current }; delete next[key]; return next; }); setError(null); }; const save = async (event) => { event?.preventDefault?.(); if (!analysis.dirty || saving) return; if (Object.keys(analysis.errors).length) { setFieldErrors(analysis.errors); setError('Certains champs ne peuvent pas être enregistrés. Corrigez les valeurs indiquées.'); return; } setSaving(true); setFieldErrors({}); setError(null); try { await runtime.update(p, analysis.changes, attempt); try { // La réponse du PATCH est déjà intégrée, puis la fiche est relue : le // succès affiché correspond ainsi à l'état durable rendu par le serveur. await runtime.load(p.id); onSaved(); showToast('Dossier mis à jour', 'success'); onClose(); } catch (reloadFailure) { onSaved(); setError(`Le dossier a été mis à jour, mais sa relecture a échoué : ${reloadFailure?.message || 'serveur indisponible'}.`); setSaving(false); } } catch (failure) { if (failure?.status === 409) { try { await runtime.load(p.id); onSaved(); setAttempt(window.VascoApi.correspondents.newAttempt()); setError('Le dossier a été modifié entre-temps. Vos saisies sont conservées – vérifiez-les puis réessayez.'); } catch (reloadFailure) { setError(`Le dossier a été modifié entre-temps et sa version actuelle n’a pas pu être relue : ${reloadFailure?.message || 'serveur indisponible'}. Vos saisies sont conservées.`); } } else { setError(failure?.message || 'La modification a échoué'); } setSaving(false); } }; const input = (key, props = {}) => ( set(key, event.target.value)} style={patientEditControlStyle()} /> ); const textarea = (key, props = {}) => (