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'; import { api } from '../../lib/api'; import ConfirmAppointmentModal from '../appointments/ConfirmAppointmentModal'; // Labels follow the Figma نوبت‌ها design (ثبت شده / قطعی شده / ویزیت شده …). export const STATUS_META: Record = { pending: { label: 'ثبت شده', color: '#3b82f6' }, confirmed: { label: 'قطعی شده', color: '#14b8a6' }, following_up: { label: 'در حال پیگیری', color: '#f59e0b' }, salon: { label: 'سالن', color: '#8b5cf6' }, completed: { label: 'ویزیت شده', color: '#22c55e' }, cancelled_by_doctor: { label: 'لغو شده', color: '#ef4444' }, cancelled_by_user: { label: 'لغو توسط بیمار', color: '#ef4444' }, no_show: { label: 'غیبت', color: '#9ca3af' }, expired: { label: 'منقضی شده', color: '#9ca3af' }, }; // Mirrors Appointment::ALLOWED_TRANSITIONS on the backend. const TRANSITIONS: Record = { pending: ['confirmed', 'following_up', 'cancelled_by_doctor', 'cancelled_by_user'], confirmed: ['completed', 'following_up', 'salon', 'cancelled_by_doctor', 'cancelled_by_user', 'no_show'], following_up: ['confirmed', 'salon', 'completed', 'cancelled_by_doctor', 'cancelled_by_user', 'no_show'], salon: ['completed', 'following_up', 'cancelled_by_doctor', 'cancelled_by_user', 'no_show'], }; interface Props { uuid: string; currentStatus: string; version: number; queryKey: unknown[]; } export default function AppointmentStatusDropdown({ uuid, currentStatus, version, queryKey }: Props) { const [open, setOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null); const btnRef = useRef(null); const menuRef = useRef(null); const qc = useQueryClient(); useEffect(() => { if (!open) return; function handler(e: MouseEvent) { 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) => api.patch(`/api/v1/appointment/${uuid}/status`, { status: newStatus, version }), onSuccess: () => { qc.invalidateQueries({ queryKey }); setOpen(false); }, // پیام سرور را نشان بده: تداخل نسخه (۴۰۹) و نبودِ دسترسی (۴۰۳) پیام فارسی // دقیق دارند و «خطا در تغییر وضعیت» آن را پنهان می‌کرد. onError: (e: any) => toast.error(e?.message || 'خطا در تغییر وضعیت'), }); 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); } /** * «قطعی شده» راه میان‌بر ندارد: قطعی‌کردن یعنی ثبت هزینه‌ها و پرداخت در پرونده، * پس همیشه از مودال رد می‌شود. بقیهٔ وضعیت‌ها همان PATCH ساده‌اند. */ function handlePick(status: string) { if (status === 'confirmed') { setOpen(false); setConfirmOpen(true); return; } mutation.mutate(status); } return (
{open && menuPos && nextStatuses.length > 0 && ReactDOM.createPortal(
{nextStatuses.map(s => { const sm = STATUS_META[s] ?? { label: s, color: '#9ca3af' }; return ( ); })}
, document.body )} setConfirmOpen(false)} queryKey={queryKey} />
); }