bugfix: overlapped appointments now made possible. selection popup added for overlapped banners.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { DialogCloseButton } from '@/components/ui/common/DialogCloseButton';
|
||||
import {
|
||||
APPOINTMENT_PURPOSE_LABEL,
|
||||
purposeStyle,
|
||||
} from '@/components/ui/appointments/appointmentPurposeStyles';
|
||||
import type { AppointmentRecord } from '@/types/appointment';
|
||||
|
||||
type AppointmentOverlapPopoverProps = {
|
||||
appointments: AppointmentRecord[];
|
||||
anchorRect: DOMRect;
|
||||
onSelect: (appointment: AppointmentRecord) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function formatTimeRange(apt: AppointmentRecord): string {
|
||||
const start = new Date(apt.startAt);
|
||||
const end = new Date(apt.endAt);
|
||||
const opts: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit' };
|
||||
return `${start.toLocaleTimeString(undefined, opts)} – ${end.toLocaleTimeString(undefined, opts)}`;
|
||||
}
|
||||
|
||||
export function AppointmentOverlapPopover({
|
||||
appointments,
|
||||
anchorRect,
|
||||
onSelect,
|
||||
onClose,
|
||||
}: AppointmentOverlapPopoverProps) {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function onPointerDown(event: MouseEvent) {
|
||||
if (!panelRef.current?.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', onPointerDown);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onPointerDown);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
const sorted = [...appointments].sort(
|
||||
(a, b) => new Date(a.startAt).getTime() - new Date(b.startAt).getTime(),
|
||||
);
|
||||
|
||||
const viewportPadding = 12;
|
||||
const panelWidth = Math.min(320, window.innerWidth - viewportPadding * 2);
|
||||
let top = anchorRect.bottom + 8;
|
||||
let left = anchorRect.left + anchorRect.width / 2 - panelWidth / 2;
|
||||
left = Math.max(viewportPadding, Math.min(left, window.innerWidth - panelWidth - viewportPadding));
|
||||
const estimatedHeight = 56 + sorted.length * 52;
|
||||
if (top + estimatedHeight > window.innerHeight - viewportPadding) {
|
||||
top = Math.max(viewportPadding, anchorRect.top - estimatedHeight - 8);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[65] pointer-events-none" aria-hidden>
|
||||
<div
|
||||
ref={panelRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="overlap-popover-title"
|
||||
className="pointer-events-auto fixed rounded-[var(--radius-md)] border border-border bg-background-secondary p-3 shadow-xl"
|
||||
style={{ top, left, width: panelWidth }}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<h3 id="overlap-popover-title" className="text-sm font-semibold text-text-primary pr-2">
|
||||
Overlapping appointments ({sorted.length})
|
||||
</h3>
|
||||
<DialogCloseButton onClick={onClose} />
|
||||
</div>
|
||||
<ul className="space-y-1.5 max-h-[min(16rem,50vh)] overflow-y-auto">
|
||||
{sorted.map((apt) => {
|
||||
const purpose = apt.purpose as keyof typeof APPOINTMENT_PURPOSE_LABEL;
|
||||
return (
|
||||
<li key={apt.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelect(apt);
|
||||
onClose();
|
||||
}}
|
||||
className={`w-full rounded-[var(--radius-sm)] border px-2.5 py-2 text-left transition-colors hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/35 ${purposeStyle(apt.purpose)}`}
|
||||
>
|
||||
<p className="text-xs font-medium truncate">
|
||||
{apt.patient.firstName} {apt.patient.lastName}
|
||||
</p>
|
||||
<p className="text-[11px] opacity-90 tabular-nums">{formatTimeRange(apt)}</p>
|
||||
<p className="text-[10px] opacity-80 truncate">
|
||||
{APPOINTMENT_PURPOSE_LABEL[purpose] ?? apt.purpose}
|
||||
</p>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user