const PATIENT_VIEW_MOTION = window.VascoUI?.tokens?.motion || Object.freeze({ quick: 150 }); // La maquette V5 conserve sa palette d'origine. La couche produit remplace les // tons très saturés par cinq familles plus sourdes, tout en gardant la même // couleur pour un patient dans la pile, la liste et la fiche dépliée. const PATIENT_FOLDER_PALETTE = [ { id: 'sand', body: '#F2E9D8', tab: '#E8DBC3', spine: '#D5C09C', deep: '#BFA77D' }, { id: 'sage', body: '#E2EBE4', tab: '#D2E0D5', spine: '#B4C9BA', deep: '#91AB99' }, { id: 'dusty-rose', body: '#F0E3E5', tab: '#E4D2D6', spine: '#CFAEB7', deep: '#B88996' }, { id: 'mist-blue', body: '#DFE8ED', tab: '#CDDCE3', spine: '#AFC4CF', deep: '#8DAAB8' }, { id: 'soft-plum', body: '#E8E2EB', tab: '#D9CFDE', spine: '#BCAEC5', deep: '#9B899F' }, ]; FOLDER_COLORS.splice(0, FOLDER_COLORS.length, ...PATIENT_FOLDER_PALETTE); // Une icône par sexe : mêmes couleurs et tracés, indépendamment du patient. const PATIENT_PORTRAIT_PALETTE = Object.freeze({ skin: '#E3B894', hair: '#3D2817', bg: '#9B7355' }); function PatientSexAwarePortrait({ patient, rounded = false }) { const palette = PATIENT_PORTRAIT_PALETTE; const hair = window.VascoPatientViewFilter.portraitHairForSex(patient.sex); return ( ); } Portrait = PatientSexAwarePortrait; // L'écran Patients actif conserve les deux présentations utiles de la maquette : // la pile détaillée et la liste compacte. La grille d'icônes répétait les mêmes // patients avec moins d'informations, donc elle ne fait plus partie du sélecteur. function PatientViewToggle({ mode, onChange }) { const { IconButton } = window.VascoUI; const modes = [ { id: 'stack', label: 'Cartes superposées', icon: <>, }, { id: 'list', label: 'Liste compacte', icon: <>, }, ]; return (
{modes.map((m) => { const selected = mode === m.id; return ( onChange(m.id)} size={30} style={{ width: 30, height: 28, borderRadius: 8, background: selected ? PF_SURF : 'transparent', border: selected ? `1px solid ${PF_HAIR}` : '1px solid transparent', boxShadow: selected ? '0 1px 3px rgba(10,10,10,0.08)' : 'none', cursor: 'pointer', padding: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', transition: `all ${PATIENT_VIEW_MOTION.quick}ms ease`, }}> {m.icon} ); })}
); } ViewToggle = PatientViewToggle; // La V5 rassemble toutes les lignes de la fiche dépliée dans une seule carte. // Le rendez-vous relève de l'agenda : il devient une carte autonome, tandis que // les informations du patient conservent leur carte et leur ordre d'origine. const PatientFocusDetailsBase = FocusDetails; function patientViewContainsText(node, text) { if (typeof node === 'string') return node.includes(text); if (Array.isArray(node)) return node.some((child) => patientViewContainsText(child, text)); if (!React.isValidElement(node)) return false; return patientViewContainsText(node.props?.children, text); } function patientViewNormalizeRows(rows) { return rows.map((row, index) => React.cloneElement(row, { ...row.props, style: { ...row.props.style, borderTop: index === 0 ? 'none' : `1px solid ${PF_HAIR}`, }, })); } function PatientSectionedFocusDetails(props) { const rendered = PatientFocusDetailsBase(props); if (!props.patient?.nextRdv || !React.isValidElement(rendered)) return rendered; const sections = React.Children.toArray(rendered.props.children); const infoIndex = sections.findIndex((child) => ( React.isValidElement(child) && child.type === 'div' && patientViewContainsText(child, 'Prochain RDV') )); if (infoIndex < 0) return rendered; const infoCard = sections[infoIndex]; const rows = React.Children.toArray(infoCard.props.children); const appointmentRows = rows.filter((row) => patientViewContainsText(row, 'Prochain RDV')); const patientRows = rows.filter((row) => !patientViewContainsText(row, 'Prochain RDV')); if (!appointmentRows.length) return rendered; const sharedStyle = { ...infoCard.props.style }; const replacements = []; if (patientRows.length) { replacements.push(React.cloneElement(infoCard, { ...infoCard.props, key: 'patient-focus-information', role: 'group', 'aria-label': 'Informations patient', style: { ...sharedStyle, margin: '0 8px 8px' }, }, patientViewNormalizeRows(patientRows))); } replacements.push( , ); sections.splice( infoIndex, 1, ...replacements, ); return React.cloneElement(rendered, rendered.props, sections); } FocusDetails = PatientSectionedFocusDetails; // Le filtre s'insère à côté du tri sans recopier l'écran Patients gelé. Un // contexte porte le choix, puis les deux vues actives reçoivent la même liste. const LegacyPatientFoldersForFilter = PatientFolders; const LegacyPatientSortPickerForFilter = SortPicker; const LegacyPatientWalletCardsForFilter = WalletCards; const LegacyPatientListViewForFilter = PatientListView; const PatientFilterContext = React.createContext({ criteria: { status: 'all', sex: 'all' }, setCriteria: () => {}, }); function PatientFilterPicker() { const { Button, IconButton, InteractiveRow } = window.VascoUI; const { criteria, setCriteria } = React.useContext(PatientFilterContext); const [open, setOpen] = React.useState(false); const { FILTERS: statusFilters, SEX_FILTERS: sexFilters } = window.VascoPatientViewFilter; const active = criteria.status !== 'all' || criteria.sex !== 'all'; const activeLabels = [ criteria.status === 'all' ? null : statusFilters.find((item) => item.id === criteria.status)?.label, criteria.sex === 'all' ? null : sexFilters.find((item) => item.id === criteria.sex)?.label, ].filter(Boolean); const change = (key, value) => setCriteria((current) => ({ ...current, [key]: value })); const choices = (items, key) => items.map((item) => { const checked = item.id === criteria[key]; return ( change(key, item.id)} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 9, padding: '7px 10px', borderRadius: 9, border: 'none', background: checked ? PF_MED_TINT : 'transparent', color: checked ? PF_MED : PF_INK, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 600, textAlign: 'left', }}> ); }); return (
setOpen((value) => !value)} style={{ height: 32, borderRadius: 12, background: active ? PF_MED_TINT : 'rgba(10,10,10,0.04)', border: 'none', cursor: 'pointer', position: 'relative', }}> {active && {open && <>