// 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 (
{label}{required && * }
{React.cloneElement(children, {
id,
'aria-required': required || undefined,
'aria-invalid': error ? 'true' : undefined,
'aria-describedby': describedBy || undefined,
})}
{hint &&
{hint}
}
{error &&
{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.
{/* 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. */}
Début
set({ startsOn: value })} />
Fin
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 (
Traitements en cours
{treatments.map((treatment, index) => (
replace(index, next)}
onRemove={() => onChange(treatments.filter((_, position) => position !== index))} />
))}
{!treatments.length && (
Aucun traitement enregistré.
)}
onChange([...treatments, { label: '', startsOn: '', endsOn: '' }])}>
Ajouter un traitement
{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 = {}) => (