import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import type { Appointment, AppointmentStatus, AppointmentEvent } from '../types';
import { formatDate, formatDateTime, toDate } from '../lib/utils';
import PageHeader from '../components/ui/PageHeader';
import AppointmentInvoiceCard from '../components/AppointmentInvoiceCard';
import StatusBadge from '../components/ui/StatusBadge';
import CancelAppointmentDialog from '../components/CancelAppointmentDialog';
import AppointmentSegmentsCard from '../components/AppointmentSegmentsCard';
import { useAppointmentSegments } from '../hooks/useResourceBooking';
import SearchableSelect from '../components/ui/SearchableSelect';
import ConfirmAppointmentModal from '../components/appointments/ConfirmAppointmentModal';
const ALL_STATUSES: { value: AppointmentStatus; label: string }[] = [
{ value: 'pending', label: 'رزرو شده' },
{ value: 'confirmed', label: 'تأیید شده' },
{ value: 'completed', label: 'تکمیل شده' },
{ value: 'cancelled_by_doctor', label: 'لغو پزشک' },
{ value: 'cancelled_by_user', label: 'لغو بیمار' },
{ value: 'no_show', label: 'غیبت' },
{ value: 'expired', label: 'منقضی' },
];
// روزِ محلیِ نوبت (YYYY-MM-DD) برای بازگشت به همان تاریخ در لیست.
const isoDay = (ts?: number | null) => {
if (!ts) return '';
const d = new Date(ts * 1000);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
};
const timeOf = (ts?: number | null) => {
const d = toDate(ts ?? null);
return d
? new Intl.DateTimeFormat('fa-IR-u-nu-latn', { hour: '2-digit', minute: '2-digit', hour12: false }).format(d)
: '—';
};
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
return (
{label}
{value ?? '—'}
);
}
export default function AppointmentDetailPage() {
const { uuid } = useParams<{ uuid: string }>();
const navigate = useNavigate();
const qc = useQueryClient();
const [cancelOpen, setCancelOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [newStatus, setNewStatus] = useState('');
const { data, isLoading } = useQuery({
queryKey: ['appointment', uuid],
queryFn: () => api.get>(`/api/v1/appointment/${uuid}`),
enabled: !!uuid,
});
const eventsQuery = useQuery({
queryKey: ['appointment-events', uuid],
queryFn: () => api.get>(`/api/v1/appointment/${uuid}/events`),
enabled: !!uuid,
});
const events: AppointmentEvent[] = (eventsQuery.data?.data as any) ?? [];
// خالی بودن یعنی نوبت اسلاتی است؛ همین تفاوت تعیین میکند جابهجایی منبعمحور دیده شود یا نه.
const { segments } = useAppointmentSegments(uuid);
const statusMutation = useMutation({
mutationFn: (status: string) =>
api.patch>(`/api/v1/appointment/${uuid}/status`, { status, version: appt?.version }),
onSuccess: () => {
toast.success('وضعیت نوبت بروزرسانی شد');
qc.invalidateQueries({ queryKey: ['appointment', uuid] });
qc.invalidateQueries({ queryKey: ['appointment-events', uuid] });
},
onError: (err: Error) => toast.error(err.message),
});
// پاسخ single تودرتو است: { data: { data: {...} } }
const appt: any = (data?.data as any)?.data ?? data?.data;
// قطعیکردن هزینه و پرداخت دارد؛ از مسیر مودال میرود، نه PATCH وضعیت.
function applyStatus() {
if (!newStatus) return;
if (newStatus === 'confirmed') {
setConfirmOpen(true);
return;
}
statusMutation.mutate(newStatus);
}
// بازگشت به همان روزِ نوبت (نه امروز).
const day = isoDay(appt?.slot_start);
const backTo = day ? `/admin/appointments?date=${day}` : '/admin/appointments';
return (
{isLoading ? (
{Array.from({ length: 6 }).map((_, i) => (
))}
) : appt ? (
اطلاعات بیمار
{appt.patient_mobile}} />
{appt.patient_reason && }
{appt.note && }
وضعیت و اقدامات
{appt.status === 'pending' && (
setConfirmOpen(true)}
className="btn primary w-full"
>
قطعی کردن نوبت
)}
تغییر وضعیت:
({ value: s.value, label: s.label }))}
value={newStatus || null}
onChange={(v) => setNewStatus(v ? String(v) : '')}
placeholder="انتخاب وضعیت..."
isClearable
/>
applyStatus()}
disabled={!newStatus || statusMutation.isPending}
className="btn primary sm"
>
اعمال
{/* جابهجایی منبعمحور فقط برای نوبتی معنا دارد که بخش ثبتشده دارد؛
نوبت اسلاتی از مسیر ویرایشِ خودش جابهجا میشود. */}
{segments.length > 0 && (
navigate(`/admin/resource-booking?rebook=${uuid}`)}
className="btn secondary w-full mt-3"
>
جابهجایی نوبت
)}
setCancelOpen(true)}
className="w-full py-2 border border-[var(--danger)] text-[var(--danger)] text-sm rounded-[10px] hover:bg-[var(--danger-bg)] transition-colors"
>
لغو نوبت
تاریخچه رویدادها
{eventsQuery.isLoading ? (
) : events.length === 0 ? (
رویدادی برای این نوبت ثبت نشده است.
) : (
{events.map((ev, i) => (
{ev.title}
{formatDateTime(ev.created_at)}
{ev.actor_name && (
توسط: {ev.actor_name}
)}
{ev.reason && (
دلیل: {ev.reason}
)}
))}
)}
) : (
نوبتی یافت نشد
)}
setCancelOpen(false)}
onCancelled={() => {
qc.invalidateQueries({ queryKey: ['appointment', uuid] });
qc.invalidateQueries({ queryKey: ['appointment-events', uuid] });
}}
/>
setConfirmOpen(false)}
queryKey={['appointment', uuid]}
/>
);
}