// Tableau de bord en disposition bureau. // // Cette couche ne crée aucune donnée : elle redistribue en grille ce que le // tableau de bord téléphone affiche déjà, depuis les mêmes projections // (`VASCO_RUNTIME.dashboard`, `TODO_REPORTS`, `AGENDA_TODAY`, `ACTIVITE`) et les // mêmes chargeurs (`VascoApi`). Chaque geste appelle le même rappel et ouvre la // même feuille que sur le téléphone. // // Sous le seuil de bureau, le composant rend exactement la version enveloppée // précédente : le rendu téléphone ne change pas d'un pixel. Le fichier est donc // chargé après toutes les couches qui remplacent `Dashboard`, et enveloppe la // dernière version, jamais la référence figée. const VASCO_DESKTOP_LEGACY_DASHBOARD = Dashboard; function vascoDesktopPlural(count, one, many) { return count > 1 ? many : one; } function vascoDesktopCapability(capabilities, name) { return (capabilities && capabilities[name]) || null; } // Refus serveur : le motif vient du serveur, jamais d'un texte inventé ici. function vascoDesktopRefusal(capability) { return typeof dbReason === 'function' ? dbReason(capability) : 'Action indisponible'; } function vascoDesktopBlocked(capability) { return Boolean(capability && capability.enabled === false); } function VascoDesktopTypeBadge({ type }) { const tokens = window.VascoUI.tokens; return {type}; } // Toutes les lignes de panneau partagent la même hauteur, la même séparation et // le même geste clavier : `InteractiveRow` du socle porte le bouton, le focus et // l'étiquette ; la teinte de survol vient de la feuille de style de la scène. function VascoDesktopRowList({ children }) { const tokens = window.VascoUI.tokens; return
{React.Children.map(children, (child, index) => {index > 0 && ; } function VascoDesktopRow({ ariaLabel, onClick = null, leading = null, trailing = null, primary, secondary = null }) { const tokens = window.VascoUI.tokens; const { InteractiveRow } = window.VascoUI; const text = {primary} {secondary && {secondary}} ; const shared = { width: '100%', minHeight: tokens.layout.panel.rowHeight, padding: `0 ${tokens.space.xs}px`, borderRadius: 0, border: 'none', background: 'transparent', gap: tokens.space.md, }; if (!onClick) { return
{leading}{text}{trailing}
; } return {text}; } // ── À finaliser ───────────────────────────────────────────────────────────── // Source : `TODO_REPORTS`, la file de /api/v1/dashboard/reports projetée par // api-client.js. Geste : ouvrir la feuille de validation du téléphone, centrée // sur le document touché comme le fait déjà une notification. function VascoDesktopFinalizePanel({ reports, capability, onOpenAll, onOpenReport }) { const tokens = window.VascoUI.tokens; const { InlineAction, Panel } = window.VascoUI; const counts = reports.reduce((acc, report) => { acc[report.type] = (acc[report.type] || 0) + 1; return acc; }, {}); const spread = Object.keys(counts).sort().map((type) => `${counts[type]} ${type}`).join(' · '); const blocked = vascoDesktopBlocked(capability); return Tout ouvrir : null} footer={!blocked && spread ? {spread} : null}> {reports.map((report) => onOpenReport(report)} leading={} primary={report.patient} secondary={report.procedure} trailing={{report.started}} />)} ; } // ── Aujourd'hui ───────────────────────────────────────────────────────────── // Source : `AGENDA_TODAY`, rempli par `VascoApi.loadAgenda()` depuis la même // fenêtre que l'onglet Agenda. La charge du jour vient de `day_load`, comme la // carte « Charge du jour » du téléphone. Geste : ouvrir l'agenda, le seul que la // carte « Prochain RDV » propose sur le téléphone. function VascoDesktopTodayPanel({ dayLoad, nextTime, onOpenAgenda, capability }) { const tokens = window.VascoUI.tokens; const { InlineAction, Panel } = window.VascoUI; const [state, setState] = React.useState('loading'); const [rows, setRows] = React.useState([]); const [failure, setFailure] = React.useState(null); const load = React.useCallback(async () => { const api = window.VascoApi; if (!api || typeof api.loadAgenda !== 'function') { setState('error'); setFailure('L’agenda du jour n’est pas raccordé.'); return; } setState('loading'); setFailure(null); try { await api.loadAgenda(); setRows((window.AGENDA_TODAY || []).slice()); setState('ready'); } catch (error) { setRows([]); setState('error'); setFailure(error?.message || 'Agenda indisponible'); } }, []); React.useEffect(() => { void load(); }, [load]); // `day_load` est le décompte du serveur, pas la liste chargée : les deux // projections peuvent différer. On nomme donc la mesure au lieu de poser deux // nombres contradictoires côte à côte sans dire d'où ils viennent. const spread = dayLoad ? `Charge du jour : ${[ `${dayLoad.consultations} ${vascoDesktopPlural(dayLoad.consultations, 'consultation et suivi', 'consultations et suivis')}`, `${dayLoad.procedures} ${vascoDesktopPlural(dayLoad.procedures, 'intervention', 'interventions')}`, `${dayLoad.emergencies} ${vascoDesktopPlural(dayLoad.emergencies, 'urgence', 'urgences')}`, ].join(' · ')}` : null; const blocked = vascoDesktopBlocked(capability); return Ouvrir l’agenda : null} footer={spread ? {spread} : null}> {rows.map((row, index) => {row.time}} primary={row.label} secondary={row.sub} />)} ; } // ── Suivis en retard ──────────────────────────────────────────────────────── // Source et feuille : celles de `postop-followup.jsx`, sans les recopier. La // ligne ouvre le dossier à l'onglet post-opératoire, comme la feuille mobile. function VascoDesktopFollowUpPanel({ accent, onOpenPatient }) { const tokens = window.VascoUI.tokens; const { InlineAction, Panel } = window.VascoUI; const runtime = window.VascoApi?.dashboard?.overdueFollowUps; const [state, setState] = React.useState('loading'); const [payload, setPayload] = React.useState(null); const [failure, setFailure] = React.useState(null); const [open, setOpen] = React.useState(false); const load = React.useCallback(async () => { if (!runtime) { setState('error'); setFailure('Les surveillances en retard ne sont pas raccordées.'); return; } setState('loading'); setFailure(null); try { setPayload(await runtime.load()); setState('ready'); } catch (error) { setPayload(null); setState('error'); setFailure(error?.message || 'Chargement impossible'); } }, [runtime]); React.useEffect(() => { void load(); }, [load]); const items = state === 'ready' ? (payload?.items || []) : []; const visible = items.slice(0, 5); return <> setOpen(true)} ariaLabel="Ouvrir les surveillances en retard"> Tout ouvrir : null} footer={visible.length < items.length ? {items.length - visible.length} de plus : null}> {visible.map((item) => { const destination = window.VascoPostopFollowUp?.dashboardTarget(item) || null; return onOpenPatient && onOpenPatient(destination.patientId, destination.tab) : null} primary={item.patient.displayName} secondary={item.patient.archived ? 'Patient archivé' : item.interventionLabel} trailing={{item.daysOverdue} J} />; })} {open && setOpen(false)} onReload={load} onOpenPatient={onOpenPatient || (() => {})} />} ; } // ── Assistant ─────────────────────────────────────────────────────────────── // Source : la route d'historique de l'écran Assistant. Geste : ouvrir la // conversation choisie, par l'aiguillage d'onglet qui sert déjà au Compte. function VascoDesktopAssistantPanel({ onOpenConversation, onOpenHistory }) { const tokens = window.VascoUI.tokens; const { InlineAction, Panel } = window.VascoUI; const runtime = window.VascoApi?.assistant; const [state, setState] = React.useState('loading'); const [items, setItems] = React.useState([]); const [failure, setFailure] = React.useState(null); const load = React.useCallback(async () => { if (!runtime || typeof runtime.listConversations !== 'function') { setState('error'); setFailure('L’historique de l’assistant n’est pas raccordé.'); return; } setState('loading'); setFailure(null); try { setItems(await runtime.listConversations()); setState('ready'); } catch (error) { setItems([]); setState('error'); setFailure(error?.message || 'Historique indisponible'); } }, [runtime]); React.useEffect(() => { void load(); }, [load]); return Ouvrir l’assistant : null}> {items.slice(0, 4).map((conversation) => onOpenConversation(conversation)} primary={conversation.title} secondary={conversation.preview || null} trailing={{conversation.date}} />)} ; } // ── Mon activité ──────────────────────────────────────────────────────────── // Source : `ACTIVITE`, projeté depuis `dashboard.activity`. Même geste que la // carte du téléphone : ouvrir les statistiques du Compte. function VascoDesktopActivityPanel({ onOpenStats, capability }) { const tokens = window.VascoUI.tokens; const { InlineAction, Panel } = window.VascoUI; const activity = window.ACTIVITE || null; const kpis = (activity?.kpis || []).filter((kpi) => kpi.key === 'crs' || kpi.key === 'rdvs'); const blocked = vascoDesktopBlocked(capability); return Détail : null} footer={!blocked && activity?.periode ? {activity.periode} : null}>
{kpis.map((kpi) =>
{kpi.value} {kpi.delta}
{kpi.label.toLowerCase()}
)}
; } // ── Recherche CCAM ────────────────────────────────────────────────────────── // Le référentiel n'est pas raccordé sur cet environnement. Le panneau ne promet // rien : il affiche le motif renvoyé par le serveur, comme la carte du téléphone. function VascoDesktopCcamPanel({ capability, onOpen }) { const tokens = window.VascoUI.tokens; const { Panel } = window.VascoUI; const blocked = vascoDesktopBlocked(capability); return ; } // ── La page ───────────────────────────────────────────────────────────────── function VascoDesktopDashboardPage({ accent, onChangeTab, onOpenPatient, onSlideToChat, onOpenStats, runtime = null, dictationRuntime = null }) { const tokens = window.VascoUI.tokens; const { Button, PageHeader, PageGrid, PageColumn, icons } = window.VascoUI; const payload = window.VASCO_RUNTIME && window.VASCO_RUNTIME.dashboard; const capabilities = payload?.capabilities || null; const practitionerName = payload?.practitioner?.display_name || 'Dr.X'; const patientsRemaining = payload?.summary?.patients_remaining ?? 0; const timeZone = payload?.time_zone || 'Europe/Paris'; const [reports, setReports] = React.useState(() => (window.TODO_REPORTS || []).slice()); const [crSheet, setCrSheet] = React.useState(null); const [focusReportId, setFocusReportId] = React.useState(null); const [dictating, setDictating] = React.useState(false); const [pickingDictationPatient, setPickingDictationPatient] = React.useState(false); const [ccamOpen, setCcamOpen] = React.useState(false); const nextTime = payload?.next_appointment ? new Intl.DateTimeFormat('fr-FR', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone, }).format(new Date(payload.next_appointment.starts_at)) : null; // « Avec un patient » promettait un patient qu'il ne donnait jamais : au // téléphone c'est le micro d'accueil qui arme le dossier avant d'ouvrir le // parcours, et cette page de bureau ne monte pas ce micro. On arme donc par // la feuille de choix du parcours lui-même, avant d'ouvrir, plutôt que de // laisser un bouton ouvrir une dictée sans patient. const startDictation = (mode, name) => { const capability = vascoDesktopCapability(capabilities, name); if (vascoDesktopBlocked(capability)) { showToast(vascoDesktopRefusal(capability), 'error'); return; } if (mode !== 'full') { setDictating(mode); return; } if (!dictationRuntime?.patients) { showToast('La recherche patient est indisponible', 'error'); return; } setPickingDictationPatient(true); }; const openAgenda = () => onChangeTab && onChangeTab('AGE'); const actions = <> {/* Le micro de l'accueil devient l'action principale de l'en-tête : même tracé que l'onglet Dictée, à la moitié d'un petit contrôle. */} onChangeTab && onChangeTab(route)} onOpenPatient={(id) => onOpenPatient && onOpenPatient(id)} onOpenReport={(documentId) => { setFocusReportId(documentId); setCrSheet('all'); }} /> ; const subtitle = <> {patientsRemaining} {vascoDesktopPlural(patientsRemaining, 'patient restant', 'patients restants')} aujourd’hui · {reports.length} CR à finaliser ; return
Bonjour, {practitionerName}} subtitle={subtitle} actions={actions} /> { setFocusReportId(null); setCrSheet('all'); }} onOpenReport={(report) => { setFocusReportId(report.id); setCrSheet('all'); }} /> onChangeTab && onChangeTab('IA', { openHistory: true })} onOpenConversation={(conversation) => { if (onChangeTab) onChangeTab('IA', { conversationId: conversation.id }); else if (onSlideToChat) onSlideToChat(); }} /> setCcamOpen(true)} /> {crSheet && { setFocusReportId(null); setCrSheet(filter); }} onValidate={(document) => setReports((previous) => previous.filter((item) => item.id !== document.id))} onClose={() => { setFocusReportId(null); setCrSheet(null); }} />} {ccamOpen && setCcamOpen(false)} />} {pickingDictationPatient && { window.VascoDictationArmedPatients.arm(picked); setPickingDictationPatient(false); setDictating('full'); }} onClose={() => setPickingDictationPatient(false)} />} {dictating && setDictating(false)} onSent={(conversation) => { void runtime?.refresh().catch(() => {}); if (onChangeTab) onChangeTab('IA', { conversationId: conversation.id }); }} />}
; } Dashboard = function VascoDashboardForLayout(props) { if (window.VascoUI.useLayout() !== 'desktop') { return ; } return ; }; window.Dashboard = Dashboard;