feat: enhance appointment scheduling with session management and availability checks
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
@@ -28,16 +29,39 @@ interface Props {
|
||||
|
||||
export default function AppointmentStatusDropdown({ uuid, currentStatus, version, queryKey }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null);
|
||||
const btnRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const qc = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handler(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
const target = e.target as Node;
|
||||
if (!btnRef.current?.contains(target) && !menuRef.current?.contains(target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
}, [open]);
|
||||
|
||||
// Reposition on scroll/resize while open
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function reposition() {
|
||||
if (btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setMenuPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', reposition, true);
|
||||
window.addEventListener('resize', reposition);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', reposition, true);
|
||||
window.removeEventListener('resize', reposition);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (newStatus: string) =>
|
||||
@@ -52,10 +76,19 @@ export default function AppointmentStatusDropdown({ uuid, currentStatus, version
|
||||
const meta = STATUS_META[currentStatus] ?? { label: currentStatus, color: '#9ca3af' };
|
||||
const nextStatuses = TRANSITIONS[currentStatus] ?? [];
|
||||
|
||||
function handleToggle() {
|
||||
if (!open && btnRef.current) {
|
||||
const rect = btnRef.current.getBoundingClientRect();
|
||||
setMenuPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
||||
}
|
||||
setOpen(o => !o);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
ref={btnRef}
|
||||
onClick={handleToggle}
|
||||
disabled={nextStatuses.length === 0}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
@@ -74,16 +107,19 @@ export default function AppointmentStatusDropdown({ uuid, currentStatus, version
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && nextStatuses.length > 0 && (
|
||||
<div style={{
|
||||
position: 'absolute', top: '100%', right: 0, marginTop: 4, zIndex: 100,
|
||||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', boxShadow: 'var(--shadow-lg)',
|
||||
minWidth: 160, overflow: 'hidden',
|
||||
}}>
|
||||
{open && menuPos && nextStatuses.length > 0 && ReactDOM.createPortal(
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{
|
||||
position: 'fixed', top: menuPos.top, right: menuPos.right,
|
||||
zIndex: 9000,
|
||||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', boxShadow: 'var(--shadow-lg)',
|
||||
minWidth: 160, overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{nextStatuses.map(s => {
|
||||
const sm = STATUS_META[s] ?? { label: s, color: '#9ca3af' };
|
||||
const isCurrent = s === currentStatus;
|
||||
return (
|
||||
<button
|
||||
key={s}
|
||||
@@ -93,7 +129,7 @@ export default function AppointmentStatusDropdown({ uuid, currentStatus, version
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
width: '100%', padding: '8px 12px', fontSize: 13,
|
||||
background: 'transparent', border: 'none', cursor: 'pointer',
|
||||
color: sm.color, fontWeight: isCurrent ? 700 : 400,
|
||||
color: sm.color, fontWeight: 400,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = 'var(--surface-2)')}
|
||||
@@ -101,14 +137,14 @@ export default function AppointmentStatusDropdown({ uuid, currentStatus, version
|
||||
>
|
||||
<span style={{
|
||||
width: 10, height: 10, borderRadius: '50%', flexShrink: 0,
|
||||
background: isCurrent ? sm.color : 'transparent',
|
||||
border: `2px solid ${sm.color}`,
|
||||
}} />
|
||||
{sm.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user