// Extensions généralistes de l'Agenda : lecture et modification des Staff/RCP // (la maquette n'en crée plus). // Elles enveloppent les feuilles V5 sans modifier la référence gelée. (function installGeneralAgenda() { 'use strict'; const contract = window.VascoAgendaGeneral; const LegacyNewRdvSheet = NewRdvSheet; const LegacyRdvCard = RdvCard; const LegacyWidgetRDV = WidgetRDV; const LegacyAgendaScreen = AgendaScreen; const LegacyViewToggleAG = ViewToggleAG; const AgendaChoice = window.VascoUI?.SimpleSelect; const AgendaScopeTabs = window.VascoUI?.ScopeTabs; const AGENDA_PATIENT_SCOPES = Object.freeze([ { id: 'living', label: 'Vivants' }, { id: 'deceased', label: 'Décédés' }, ]); // L'application lit toujours les tokens officiels. Le second objet ne sert // qu'aux harnais unitaires qui montent cet écran sans charger la coque Web. const AG_MOTION = window.VascoUI?.tokens?.motion || Object.freeze({ quick: 150, responsive: 160, selectEnter: 180, standard: 'cubic-bezier(.2,.7,.2,1)', }); TYPE_STYLE.staff_rcp = { bg: 'rgba(91,69,139,.09)', border: '#6d5797', text: '#58417f', label: 'Staff / RCP', }; // Un type historique se lit sous son propre nom : l'imagerie s'affichait // jusqu'ici avec l'étiquette « Suivi », qui n'était pas la sienne. TYPE_STYLE.imagerie = { bg: 'rgba(124,58,237,.09)', border: '#7c3aed', text: '#6d28d9', label: 'Imagerie', }; // L'intervention devient orange pour rester distincte de la priorité urgente. // Cette même palette pilote la pastille, le fond et le bord du rendez-vous. TYPE_STYLE.intervention = { bg: 'rgba(249,115,22,.10)', border: '#f97316', text: '#c2410c', label: 'Intervention', }; // « Urgence » n'est plus un type : c'est une priorité cumulable avec le type. // Elle reste rouge partout (badge, bascule, point et bord de timeline), tandis // que l'ambre reste réservé aux conflits d'horaire. const URGENT_STYLE = Object.freeze({ bg: 'rgba(185,28,28,.10)', border: '#b91c1c', text: '#991b1b', label: 'Urgent', }); // Le résumé du jour écrit l'urgence dans le type – « 1 intervention · urgent » // – et non à côté. Le mot ne s'accorde pas : il ne qualifie pas le nom, il // marque la pastille. Il reste donc « urgent » au féminin comme au pluriel, // séparé du décompte par un point médian plutôt que soudé à lui. function summaryLabel(type, label, urgent, count) { const noun = `${count} ${label}${count > 1 ? 's' : ''}`; // « Urgence · urgent » ne se dit pas : le type historique porte déjà le mot. if (!urgent || type === 'urgence') return noun; return `${noun} · urgent`; } // Le repère de l'heure courante est rouge, comme dans Apple Calendar. Il ne // porte pas l'accent de l'organisation : l'accent dit « ceci est à vous », il // ne dit pas l'heure, et il change d'un cabinet à l'autre – le trait du temps, // lui, doit se reconnaître partout. Il n'est pas non plus l'ambre du conflit // d'horaire (`AG_WARN`, #d97706) qui borde déjà les blocs en litige : deux // messages différents ne partagent pas une couleur. const AG_NOW = '#ff3b30'; // La couche de ce qui reste collé en haut d'une zone qui défile – la bande des // jours de l'écran Agenda, l'en-tête horaire de la feuille de création. Une // seule constante pour les deux : elle se lit entre les couches de la journée // (voile du passé 5, repère « maintenant » 6, qui passent dessous) et les // feuilles posées sur la couche overlay officielle (200, au-dessus de tout). // Écrite en clair des deux côtés, elle valait 6 dans la création : à égalité // avec le repère, et aucun des cadres de la timeline ne créant de contexte // d'empilement, c'est l'ordre du DOM qui tranchait – le trait rouge, rendu // après, traversait la carte DÉBUT/FIN. const AG_STICKY_Z = 8; // Le refus que le serveur pose sur les événements des confrères : il se lit en // toutes lettres là où la feuille V5 affiche déjà les autres raisons. AG_REASONS[contract.FOREIGN_REASON] = 'Événement d’un autre praticien'; const fieldStyle = { width: '100%', minHeight: 44, borderRadius: 14, border: `1px solid ${AG_HAIR2}`, padding: '0 12px', fontFamily: 'inherit', fontSize: 13, color: AG_INK, background: AG_SURF, outline: 'none', }; const buttonStyle = { minHeight: 44, borderRadius: 14, padding: '0 14px', fontFamily: 'inherit', fontSize: 13, fontWeight: 700, cursor: 'pointer', }; // Le libellé d'un champ, en petites capitales : c'est la grammaire de l'étape // Détails de la création, et la fiche parle la même. Il ne s'écrit qu'une // fois – il était recopié à chaque champ. const captionStyle = { display: 'block', fontSize: 10, fontWeight: 600, color: AG_SUB, letterSpacing: '0.4px', textTransform: 'uppercase', marginBottom: 6, }; function sheet(children, onClose, title, layout = null, contentRef = null, headingAside = null) { const stableAvailability = layout === 'availability'; const { BottomSheet } = window.VascoUI; return
{children}
; } // Un choix parmi quelques possibilités, dans le gabarit de la pastille V5 : // même hauteur, même arrondi, la sélection en encre pleine. function ChoiceChips({ legend, options, value, onChange, columns = 2, disabled = false, error = false, alert }) { const { ChoiceChip } = window.VascoUI; return (
{legend}
{options.map(([id, label]) => { const active = value === id; return ( onChange(id)} selected={active} disabled={disabled} style={{ ...buttonStyle, background: active ? AG_INK : AG_SURF, color: active ? '#fff' : AG_INK, border: `1px solid ${active ? AG_INK : AG_HAIR2}`, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.45 : 1, }}>{label} ); })}
{error && alert &&
{alert}
}
); } // Une seule palette pour la création et la modification : le type garde la // couleur qui le représente dans l'agenda, même lorsqu'il est sélectionné. // Le noir plein de l'ancien éditeur effaçait le sens bleu / orange. function typeChoiceAppearance(type, selected, disabled = false) { const typeStyle = TYPE_STYLE[type] || TYPE_STYLE.consultation; return { typeStyle, style: { border: `1px solid ${selected ? typeStyle.border : AG_HAIR}`, background: selected ? typeStyle.bg : AG_SURF, color: selected ? typeStyle.text : AG_INK, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.45 : 1, }, }; } // La feuille gelée conserve ses anciennes couleurs en dur. La couche active // remet donc sur chaque choix la palette du type qui colore aussi le rendez-vous. function restyleCreationTypeChip(chip, type) { const selected = chip.props.style?.background && chip.props.style.background !== AG_SURF; const appearance = typeChoiceAppearance(type, selected); const { typeStyle } = appearance; const children = React.Children.toArray(chip.props.children).map((child) => { if (!React.isValidElement(child)) return child; if (child.type === 'div') { return React.cloneElement(child, { style: { ...child.props.style, background: typeStyle.border }, }); } if (child.type === 'span') { return React.cloneElement(child, { style: { ...child.props.style, color: selected ? typeStyle.text : AG_INK }, }); } return child; }); return React.cloneElement(chip, { style: { ...chip.props.style, ...appearance.style, }, }, ...children); } // Le type d'un rendez-vous existant se change entre les deux types encore // créables – la fiche pose la même question que la création, dans le même // geste. Un type historique (suivi, imagerie, Staff / RCP) ne se recrée pas : // il se lit sous son nom, et `updateFields` s'abstient déjà de le réécrire. function TypeChoice({ type, onType, disabled = false }) { const { ChoiceChip } = window.VascoUI; const ts = TYPE_STYLE[type] || TYPE_STYLE.consultation; if (!contract.CREATION_TYPES.some((entry) => entry.id === type)) { return (
Type
{ts.label}
Ce type ne se crée plus : il se lit, il ne se réécrit pas.
); } return (
Type
{contract.CREATION_TYPES.map((entry) => { const active = type === entry.id; const appearance = typeChoiceAppearance(entry.id, active, disabled); return ( onType(entry.id)} style={{ ...buttonStyle, ...appearance.style, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, }}> ); })}
); } // Une consultation ne se saisit pas en deux horodatages : le praticien pose // un début, puis une durée. La fin en découle et reste modifiable – soit à la // minute ici, soit par le champ « Fin » là où il existe. function DurationField({ start, end, onEnd, label = 'Durée' }) { const { ChoiceChip } = window.VascoUI; const minutes = contract.durationOf(start, end); const [free, setFree] = React.useState(() => (minutes == null ? '' : String(minutes))); React.useEffect(() => { setFree(minutes == null ? '' : String(minutes)); }, [minutes]); const apply = (value) => { const next = contract.endFromDuration(start, value); if (next) onEnd(next); }; return (
{label}
{contract.DURATION_CHOICES.map((choice) => ( apply(choice)} selected={minutes === choice} style={{ ...buttonStyle, padding: 0, background: minutes === choice ? AG_INK : AG_SURF, color: minutes === choice ? '#fff' : AG_INK, border: `1px solid ${minutes === choice ? AG_INK : AG_HAIR2}`, }}>{contract.formatDuration(choice)} ))}
{ setFree(event.target.value); const value = contract.sanitizeDuration(event.target.value); if (value) apply(value); }} style={{ ...fieldStyle, width: 88, flex: 'none' }} /> minutes{contract.localTimeLabel(end) ? ` · fin à ${contract.localTimeLabel(end)}` : ''}
); } // ═══════════════════════════════════════════════════════════════════════ // La journée à l'échelle – timeline de l'étape « Choisir l'horaire » // // La grille de pilules supposait que tout est constant : trente minutes, // toutes pareilles, toutes libres ou « occupées ». Une journée de cabinet // n'est pas ça. On la regarde maintenant comme dans Apple Calendar – un axe // vertical d'heures, les rendez-vous déjà posés à leur vraie hauteur, et le // nouveau rendez-vous posé du doigt là où il reste de la place. // // Toute l'arithmétique temps ↔ pixels vit dans le contrat ; ce qui suit n'en // est que l'habillage. // ═══════════════════════════════════════════════════════════════════════ // Réglage fin, toujours disponible : le glissement est le geste naturel, mais // il ne doit jamais être le seul moyen d'atteindre la minute voulue. function TimeStepper({ label, value, onStep, disabled }) { const { IconButton } = window.VascoUI; const round = { width: 28, height: 28, borderRadius: 10, flexShrink: 0, padding: 0, border: `1px solid ${AG_HAIR2}`, background: AG_SURF, color: AG_INK, fontFamily: 'inherit', fontSize: 16, fontWeight: 600, lineHeight: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.35 : 1, transition: `opacity ${AG_MOTION.quick}ms ease`, }; const step = contract.TIMELINE_SNAP_MINUTES; return (
{label}
onStep(-step)}>− {value} onStep(step)}>+
); } function WarnGlyph({ size = 13, color = AG_WARN }) { return ( ); } // Un rendez-vous posé sur l'axe. Le même bloc sert aux deux écrans, et c'est // `onSelect` qui les sépare : // // – à la création, il n'y en a pas : le bloc laisse passer le doigt // (`pointerEvents: none`), parce que le geste attendu là est de poser le // nouveau rendez-vous, y compris par-dessus. Programmer en parallèle d'une // urgence est légitime – le chevauchement se voit, il ne s'interdit pas ; // – à l'affichage, il y en a un : le bloc devient le bouton qui ouvre la // fiche, comme la carte de la liste qu'il remplace. // // Un événement de confrère reste touchable : c'est sa fiche qui dit le refus // que le serveur y a posé, et la désaturation dit qu'il n'est pas à nous. function TimelineEvent({ event, geometry, onSelect }) { const ts = TYPE_STYLE[event.type] || TYPE_STYLE.consultation; const top = contract.minutesToOffset(event.startMinutes, geometry); const height = contract.minutesToOffset(event.endMinutes, geometry) - top; const density = contract.blockDensity(height); const width = 100 / event.columns; const foreign = contract.isOtherPractitioner(event); const clash = event.clashing; const urgent = event.urgent === true; // L'urgence a la priorité visuelle sur le type : sa pastille est rouge, // donc le rendez-vous l'est aussi. Sans urgence, la couleur reste celle du type. const eventStyle = urgent ? URGENT_STYLE : ts; const mark = urgent ? contract.urgentMark(density) : 'none'; const line = { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }; const edge = clash ? AG_WARN : urgent ? URGENT_STYLE.border : ts.border; const dot = (