// Suivi actionnable du dossier patient, connecté uniquement à des faits stockés.
// Cette couche produit remplace l'écran hérité sans modifier la référence figée.
const PATIENT_FOLLOW_UP_TAB_EVENT = 'vasco:patient-follow-up-tab';
function requestPatientTab(tab) {
window.dispatchEvent(new CustomEvent(PATIENT_FOLLOW_UP_TAB_EVENT, { detail: { tab } }));
}
// Le composant hérité garde son onglet en état local. Cette enveloppe produit lui
// transmet les raccourcis du Suivi comme un changement d'onglet normal.
const PatientFollowUpDetailBase = PatientDetail;
PatientDetail = function PatientFollowUpDetail(props) {
const [requestedTab, setRequestedTab] = React.useState(props.initialTab || 'resume');
React.useEffect(() => { setRequestedTab(props.initialTab || 'resume'); }, [props.initialTab, props.patientId]);
React.useEffect(() => {
const onTabRequest = (event) => {
if (['docs', 'agenda'].includes(event.detail?.tab)) setRequestedTab(event.detail.tab);
};
window.addEventListener(PATIENT_FOLLOW_UP_TAB_EVENT, onTabRequest);
return () => window.removeEventListener(PATIENT_FOLLOW_UP_TAB_EVENT, onTabRequest);
}, []);
return ;
};
function PatientFollowUpTabsStrip({ active, onChange }) {
const { ChoiceChip, IconButton, InteractiveRow } = window.VascoUI;
const [more, setMore] = React.useState(false);
const inMore = active === 'stats';
const stripRef = React.useRef(null);
const [canScroll, setCanScroll] = React.useState(false);
const measure = () => {
const element = stripRef.current;
if (element) setCanScroll(element.scrollWidth - element.clientWidth - element.scrollLeft > 4);
};
React.useLayoutEffect(measure, [active]);
React.useEffect(() => {
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, []);
return (
{TABS.map((item) => {
const selected = item.id === active;
return (
onChange(item.id)} selected={selected}
style={{
background: selected ? '#fff' : 'transparent', border: 'none', cursor: 'pointer',
padding: '8px 6px', borderRadius: 12, fontFamily: 'inherit', fontSize: 12,
fontWeight: selected ? 700 : 500, color: selected ? P_INK : P_SUB,
letterSpacing: '0.1px', whiteSpace: 'nowrap', flex: '1 1 auto',
minWidth: 'max-content', boxShadow: selected ? '0 1px 3px rgba(10,10,10,0.08)' : 'none',
}}>
{item.label}
);
})}
{canScroll &&
}
setMore((open) => !open)} label="Plus" expanded={more} size={34}
style={{
borderRadius: 12, background: inMore ? '#fff' : 'rgba(10,10,10,0.04)',
border: 'none', cursor: 'pointer', boxShadow: inMore ? '0 1px 3px rgba(10,10,10,0.08)' : 'none',
}}>
{more && <>
setMore(false)} style={{ position: 'fixed', inset: 0, zIndex: 20 }} />
{ onChange('stats'); setMore(false); }} selected={inMore}
style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 9,
background: inMore ? PD_MED_TINT : 'none', border: 'none', borderRadius: 8,
padding: '9px 10px', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left',
fontSize: 13, fontWeight: 600, color: inMore ? PD_MED : P_INK,
}}>
Suivi
{/* Entrées ajoutées par les extensions connectées. Elles ouvrent une
feuille et ne deviennent jamais l'onglet actif : le menu « Plus »
reste une liste d'accès, pas une barre d'onglets bis. */}
{(window.VASCO_PATIENT_MORE || []).map((entry) => (
{ setMore(false); entry.onSelect(); }}
style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 9,
background: 'none', border: 'none', borderRadius: 8,
padding: '9px 10px', cursor: 'pointer', fontFamily: 'inherit',
textAlign: 'left', fontSize: 13, fontWeight: 600, color: P_INK,
}}>
{entry.icon ? entry.icon(P_SUB) : null}
{entry.label}
))}
>}
);
}
TabsStrip = PatientFollowUpTabsStrip;
function FollowUpArrow() {
return (
);
}
function FollowUpActionCard({ eyebrow, title, detail, tone = 'neutral', onClick, children }) {
const palette = tone === 'attention'
? { background: '#fff9ea', border: '#ead8a6', color: '#70541c' }
: { background: '#fff', border: P_HAIR2, color: P_SUB };
const Tag = onClick ? 'button' : 'div';
return (
{eyebrow}
{title}
{detail &&
{detail}
}
{children}
{onClick && }
);
}
function FollowUpMetric({ label, value, detail, featured = false }) {
return (
);
}
function PatientFollowUpLoading() {
return (
Chargement du suivi…
Rendez-vous et documents du dossier
);
}
function PatientStatisticsTab({ p, accent = PD_MED, runtime = null }) {
const { Button, InteractiveRow } = window.VascoUI;
const [stats, setStats] = React.useState(null);
const [error, setError] = React.useState(null);
const [requestVersion, setRequestVersion] = React.useState(0);
React.useEffect(() => {
if (!runtime?.statistics) {
setError('Suivi indisponible');
return undefined;
}
let cancelled = false;
setStats(null);
setError(null);
runtime.statistics(p.id)
.then((view) => { if (!cancelled) setStats(view); })
.catch((failure) => { if (!cancelled) setError(failure?.message || 'Suivi indisponible'); });
return () => { cancelled = true; };
}, [runtime, p.id, requestVersion]);
const zone = stats?.timezone || 'Europe/Paris';
const formatDate = (value, withTime = false) => {
if (!value) return 'Aucune';
return new Intl.DateTimeFormat('fr-FR', {
day: 'numeric', month: 'short', year: 'numeric',
...(withTime ? { hour: '2-digit', minute: '2-digit' } : {}),
timeZone: zone,
}).format(new Date(value));
};
const next = stats?.next_appointment;
const pending = Number(stats?.awaiting_validation_document_count || 0);
const pendingLabel = pending === 1 ? '1 document à valider' : `${pending} documents à valider`;
const nextDetail = next
? [formatDate(next.starts_at, true), next.location].filter(Boolean).join(' · ')
: null;
const latest = stats?.latest_document;
const documentStatusLabels = {
awaiting_validation: 'À valider', validated: 'Validé', draft: 'Brouillon',
in_progress: 'En cours', archived: 'Archivé',
};
if (!stats && !error) return
;
if (error) {
return (
Impossible de charger le suivi
{error}
);
}
return (
À traiter
{pending > 0 ? (
requestPatientTab('docs')} />
) : (
)}
{next ? (
requestPatientTab('agenda')}>
{stats.upcoming_appointment_count > 1 && + {stats.upcoming_appointment_count - 1} autre{stats.upcoming_appointment_count > 2 ? 's' : ''} à venir
}
) : (
)}
Rendez-vous
12 derniers mois
Continuité du suivi
Dernière visite
{formatDate(stats.last_visit_at)}
Prochain suivi
{next ? formatDate(next.starts_at) : 'Non programmé'}
Documents
{stats.document_count} au total
{latest ? (
requestPatientTab('docs')} style={{
width: '100%', minWidth: 0, marginTop: 10, padding: '13px 14px', borderRadius: 17,
border: `1px solid ${P_HAIR2}`, background: '#fff', boxShadow: VT.shadowMd,
display: 'flex', alignItems: 'center', gap: 10, fontFamily: 'inherit', textAlign: 'left', cursor: 'pointer',
}}>
Dernier document
{latest.document_type || 'DOCUMENT'}
{documentStatusLabels[latest.status] || latest.status}
{latest.title}
Ajouté le {formatDate(latest.created_at)}
) : (
Aucun document dans ce dossier
)}
);
}
TabStats = PatientStatisticsTab;