// Agenda en disposition bureau : la semaine en sept colonnes.
//
// L'écran de l'agenda garde tout ce qu'il sait faire : sa navigation de période,
// son sélecteur de vue, son périmètre, ses salles, sa création et sa fiche. Seul
// le corps des vues est recomposé ici. Les calculs ne sont pas réécrits : les
// horaires, les positions, les chevauchements et les bornes de journée viennent
// des fonctions pures de `window.VascoAgendaGeneral`, celles-là mêmes que la
// timeline du téléphone utilise.
//
// Sous le seuil de bureau, chaque vue rend exactement la version enveloppée
// précédente : le téléphone ne change pas d'un pixel.
const VASCO_AGENDA_DESKTOP_WEEK = WeekView;
// Le périmètre affiché et les congés lus par l’écran du téléphone : le bureau
// les relit, il ne les recalcule pas. Le repli sert aux harnais qui montent
// cette disposition sans l’écran Agenda.
const VASCO_AGENDA_DESKTOP_SCOPE = window.VascoAgendaScope
|| { Context: React.createContext(null), visibleAbsences: () => [] };
// Une heure de bureau est plus courte qu'une heure de téléphone : sept journées
// côte à côte n'ont pas la place d'en montrer une de 120 px. La géométrie reste
// celle du contrat, à une hauteur d'heure près.
function vascoAgendaDesktopGeometry() {
return window.VascoAgendaGeneral.timelineGeometry(
window.VascoUI.tokens.layout.agenda.hourHeight,
);
}
function vascoAgendaDesktopNow() {
// Même règle que l'écran du téléphone : l'instant vient d'ici, le calcul du
// contrat. Un contrat exécuté ailleurs ne décide pas de l'heure qu'il est.
const timeZone = window.VASCO_RUNTIME?.dashboard?.time_zone || undefined;
let formatter = null;
try {
formatter = new Intl.DateTimeFormat('en-CA', {
timeZone,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
});
} catch (failure) { return null; }
return window.VascoAgendaGeneral.nowInZone(timeZone, new Date(), formatter);
}
function vascoAgendaDesktopDayKey(date) {
const value = date instanceof Date ? date : new Date(date);
if (Number.isNaN(value.getTime())) return null;
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
}
function vascoAgendaDesktopWeekDays(date) {
const monday = new Date(date instanceof Date ? date.getTime() : Date.now());
monday.setHours(0, 0, 0, 0);
// Lundi comme premier jour, comme la bande de jours du téléphone.
monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7));
return Array.from({ length: 7 }, (unused, index) => {
const day = new Date(monday.getTime());
day.setDate(monday.getDate() + index);
return day;
});
}
// L'étiquette d'un rendez-vous : ce que la carte du téléphone porte déjà.
function vascoAgendaDesktopLabel(rdv) {
return [rdv?.patient, rdv?.notes, rdv?.lieu].filter(Boolean).join(', ');
}
function VascoAgendaDesktopDayHeader({ date, dayKey, todayKey, selected, onSelect, count }) {
const tokens = window.VascoUI.tokens;
const { InteractiveRow } = window.VascoUI;
const today = dayKey === todayKey;
const weekday = new Intl.DateTimeFormat('fr-FR', { weekday: 'short' }).format(date);
return onSelect(new Date(date.getTime()))}
style={{
width: '100%', minHeight: tokens.layout.agenda.dayHeaderHeight,
padding: `0 ${tokens.space.xs}px`, border: 'none', borderRadius: 0,
background: 'transparent', justifyContent: 'center', gap: tokens.space.sm,
}}>
{weekday.toUpperCase()}
{date.getDate()}
{count > 0 && {count}}
;
}
// Une colonne de journée : les blocs déjà placés par le contrat, rendus par la
// primitive officielle. Aucune règle d'horaire n'est réécrite ici.
function vascoAgendaDesktopColumn({
date, rdvs, geometry, todayKey, selectedKey, onSelect, onSelectDay, absentKeys,
}) {
const contract = window.VascoAgendaGeneral;
const { CalendarEvent } = window.VascoUI;
const dayKey = vascoAgendaDesktopDayKey(date);
const { timed } = contract.splitDayEvents(rdvs, dayKey);
const laid = contract.layoutTimelineEvents(timed);
const source = new Map();
(Array.isArray(rdvs) ? rdvs : []).forEach((rdv) => { if (rdv) source.set(rdv.id, rdv); });
// `TYPE_STYLE` est une liaison globale de la référence, pas une propriété
// de `window` : la teinte d'un type reste celle que l'agenda emploie déjà.
const typeStyle = typeof TYPE_STYLE === 'object' && TYPE_STYLE ? TYPE_STYLE : {};
return {
key: dayKey,
muted: dayKey !== todayKey && (date.getDay() === 0 || date.getDay() === 6),
// Une journée de congé : la primitive la hachure sous ses blocs et grise
// son en-tête. Le calcul des journées couvertes reste au contrat.
absent: Boolean(absentKeys && absentKeys.has(dayKey)),
header: ,
children: laid.map((event) => {
const top = contract.minutesToOffset(event.startMinutes, geometry);
const bottom = contract.minutesToOffset(event.endMinutes, geometry);
const range = contract.timelineRangeLabel(event.startMinutes,
event.endMinutes - event.startMinutes);
return onSelect(source.get(event.id) || event) : null} />;
}),
};
}
function VascoAgendaDesktopGrid({ days, rdvs, selectedKey, onSelectRdv, onSelectDay, ariaLabel }) {
const contract = window.VascoAgendaGeneral;
const { TimeGrid } = window.VascoUI;
const geometry = vascoAgendaDesktopGeometry();
const hours = contract.timelineHours(geometry);
const now = vascoAgendaDesktopNow();
const todayKey = contract.todayKeyFrom(
now?.dayKey, window.VASCO_RUNTIME?.dashboard?.local_date || null,
);
const scope = React.useContext(VASCO_AGENDA_DESKTOP_SCOPE.Context);
const absentKeys = contract.absenceDayKeySet(
VASCO_AGENDA_DESKTOP_SCOPE.visibleAbsences(scope),
);
const columns = days.map((date) => vascoAgendaDesktopColumn({
date, rdvs, geometry, todayKey, selectedKey, onSelect: onSelectRdv, onSelectDay,
absentKeys,
}));
// Le repère d'heure courante n'a de sens que si aujourd'hui est affiché.
const showsToday = columns.some((column) => column.key === todayKey);
const nowOffset = showsToday && now && Number.isFinite(now.minutes)
&& now.minutes > geometry.startMinutes && now.minutes < geometry.endMinutes
? contract.minutesToOffset(now.minutes, geometry) : null;
// La grille s'ouvre là où la journée a un sens : sur l'heure qu'il est, sur le
// premier rendez-vous sinon, sur l'heure de cabinet en dernier recours. La
// cible et le centrage sont ceux du contrat, pas une règle inventée ici.
const scrollRef = React.useRef(null);
const focusKey = selectedKey || (days[0] ? vascoAgendaDesktopDayKey(days[0]) : null);
const focused = columns.find((column) => column.key === focusKey) || columns[0] || null;
const target = contract.timelineScrollTarget({
events: contract.splitDayEvents(rdvs, focusKey).timed,
geometry, dayKey: focusKey, todayKey,
nowMinutes: now && now.dayKey === focusKey ? now.minutes : null,
});
React.useEffect(() => {
const node = scrollRef.current;
if (!node || !focused) return;
node.scrollTop = contract.centeredScrollTop(
contract.minutesToOffset(target, geometry),
node.clientHeight,
Math.max(0, node.scrollHeight - node.clientHeight),
);
}, [focusKey, target, geometry.height]);
return ;
}
function VascoAgendaDesktopWeek({ date, setDate, rdvs, onSelectRdv }) {
const tokens = window.VascoUI.tokens;
const contract = window.VascoAgendaGeneral;
const days = vascoAgendaDesktopWeekDays(date);
// L'écran figé compte les rendez-vous du jour choisi sans le dire. Au bureau,
// sept journées sont sous les yeux : le seul décompte qui ne prête pas à
// confusion est celui de la période affichée, et il le dit.
const weekCount = days.reduce(
(total, day) => total + contract.splitDayEvents(rdvs, vascoAgendaDesktopDayKey(day)).timed.length,
0,
);
// La semaine prend la hauteur que la page lui laisse : c'est la grille, et
// elle seule, qui défile. Les en-têtes de jours vivent alors au-dessus de ce
// défilement et ne le quittent jamais.
return
{weekCount} {weekCount > 1 ? 'rendez-vous cette semaine' : 'rendez-vous cette semaine'}
;
}
WeekView = function VascoWeekViewForLayout(props) {
if (window.VascoUI.useLayout() !== 'desktop') {
return ;
}
return ;
};
window.WeekView = WeekView;
// Le mois n'est pas recomposé : sa grille de la référence est en `1fr` et
// s'élargit sans rien casser, et elle ouvre déjà la journée choisie sous elle.
// La recomposer reviendrait à réécrire ce qu'elle fait déjà bien.