import { useRef, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useSearchParams, Link } from 'react-router-dom'; import { ChevronRightIcon, PencilIcon, ClipboardDocumentCheckIcon, DocumentTextIcon, CalendarDaysIcon, CreditCardIcon, BanknotesIcon, ChatBubbleLeftRightIcon, PhoneArrowUpRightIcon, PaperClipIcon, ClipboardDocumentListIcon, ArrowUpTrayIcon, TrashIcon, DocumentIcon, } from '@heroicons/react/24/outline'; import { PlusIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import type { PatientRecord } from '../types'; import { useAuthStore } from '../stores/authStore'; import { formatDate, formatRial } from '../lib/utils'; import Modal from '../components/ui/Modal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import PersianDateInput from '../components/ui/PersianDateInput'; import PriceInput from '../components/ui/PriceInput'; type TabKey = 'services' | 'info' | 'appointments' | 'payments' | 'wallet' | 'messages' | 'callcenter' | 'attach' | 'records'; const TABS: { key: TabKey; label: string; icon: React.ElementType }[] = [ { key: 'services', label: 'سرویس‌ها', icon: ClipboardDocumentCheckIcon }, { key: 'info', label: 'اطلاعات پرونده', icon: DocumentTextIcon }, { key: 'appointments', label: 'نوبت‌ها', icon: CalendarDaysIcon }, { key: 'payments', label: 'پرداخت‌ها', icon: CreditCardIcon }, { key: 'wallet', label: 'کیف پول', icon: BanknotesIcon }, { key: 'messages', label: 'پیام‌ها', icon: ChatBubbleLeftRightIcon }, { key: 'callcenter', label: 'کال سنتر', icon: PhoneArrowUpRightIcon }, { key: 'attach', label: 'ضمیمه', icon: PaperClipIcon }, { key: 'records', label: 'پرونده پزشکی', icon: ClipboardDocumentListIcon }, ]; const GENDER_LABEL: Record = { male: 'مرد', female: 'زن' }; const PAYMENT_STATUS: Record = { pending: 'در انتظار', success: 'موفق', failed: 'ناموفق', canceled: 'لغو شده', refunded: 'بازگشت', }; function Placeholder({ label }: { label: string }) { return (
محتوای «{label}» به‌زودی تکمیل می‌شود.
); } function InfoRow({ label, value }: { label: string; value?: string | null }) { return (
{label} {value || '—'}
); } /** پرونده — the tabbed patient case-file (Figma "جزئیات پرونده"). */ export default function PatientDetailPage() { const { uuid } = useParams<{ uuid: string }>(); // ?tab=wallet etc. lets other pages (e.g. appointment forms) deep-link a tab const [searchParams] = useSearchParams(); const requested = searchParams.get('tab') as TabKey | null; const [tab, setTab] = useState( requested && TABS.some((t) => t.key === requested) ? requested : 'services', ); const { data, isLoading } = useQuery>({ queryKey: ['patient', uuid], queryFn: () => api.get(`/api/v1/patient/${uuid}`), enabled: !!uuid, }); const record = data?.data; const profile: any = record?.profile ?? {}; const sessionsQ = useQuery>({ queryKey: ['patient-sessions', uuid], queryFn: () => api.get(`/api/v1/patient/${uuid}/sessions`), enabled: !!uuid && tab === 'services', }); const appointmentsQ = useQuery>({ queryKey: ['patient-appointments', uuid], queryFn: () => api.get(`/api/v1/patient/${uuid}/appointments`), enabled: !!uuid && tab === 'appointments', }); const paymentsQ = useQuery>({ queryKey: ['patient-payments', uuid], queryFn: () => api.get(`/api/v1/patient/${uuid}/payments`), enabled: !!uuid && tab === 'payments', }); return (
{/* header */}
بازگشت
{record?.user_name || 'پرونده'}
{record?.record_number && #{record.record_number}}
ویرایش
{/* tab bar */}
{TABS.map((t) => { const on = t.key === tab; const Icon = t.icon; return ( ); })}
{/* tab content */} {isLoading ? (
در حال بارگذاری...
) : tab === 'info' ? (
) : tab === 'services' ? (
سرویس جدید
({ title: s.section_name || s.name || 'سرویس', meta: s.created_at ? formatDate(s.created_at) : '', badge: s.status })} />
) : tab === 'appointments' ? ( ({ title: a.service_name || a.doctor_name || 'نوبت', meta: a.date ? formatDate(a.date) : (a.starts_at ? formatDate(a.starts_at) : ''), badge: a.status_label || a.status })} /> ) : tab === 'payments' ? ( ({ title: formatRial(p.amount_rials), meta: [p.created_at ? formatDate(p.created_at) : '', p.gateway].filter(Boolean).join(' · '), badge: PAYMENT_STATUS[p.status] || p.status })} /> ) : tab === 'wallet' ? ( ) : tab === 'callcenter' ? ( ) : tab === 'attach' ? ( ) : tab === 'records' ? ( ) : tab === 'messages' ? ( ) : ( t.key === tab)!.label} /> )}
); } interface Attachment { uuid: string; name: string; url: string; mime?: string | null; size?: number | null } const formatBytes = (n?: number | null) => { if (!n) return ''; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`; return `${(n / 1024 / 1024).toFixed(1)} MB`; }; /** ضمیمه — patient attachments: upload (raw body), list, delete. */ function AttachmentsTab({ uuid }: { uuid: string }) { const qc = useQueryClient(); const fileRef = useRef(null); const [uploading, setUploading] = useState(false); const { data, isLoading } = useQuery>({ queryKey: ['patient-attachments', uuid], queryFn: () => api.get(`/api/v1/patient/${uuid}/attachments`), }); const items = data?.data ?? []; const del = useMutation({ mutationFn: (attUuid: string) => api.delete(`/api/v1/patient/attachment/${attUuid}`), onSuccess: () => { qc.invalidateQueries({ queryKey: ['patient-attachments', uuid] }); toast.success('ضمیمه حذف شد'); }, onError: (e: any) => toast.error(e.message), }); const onFile = async (file: File) => { setUploading(true); try { const token = useAuthStore.getState().token; const res = await fetch(`/api/v1/patient/${uuid}/attachment?name=${encodeURIComponent(file.name)}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${file.name}"`, ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: file, }); if (!res.ok) throw new Error('خطا در آپلود فایل'); qc.invalidateQueries({ queryKey: ['patient-attachments', uuid] }); toast.success('فایل آپلود شد'); } catch (e: any) { toast.error(e.message); } finally { setUploading(false); if (fileRef.current) fileRef.current.value = ''; } }; return (
e.target.files?.[0] && onFile(e.target.files[0])} />
{isLoading ? (
در حال بارگذاری...
) : items.length === 0 ? (
هنوز فایلی ضمیمه نشده است.
) : (
{items.map((a) => (
{a.name} {a.size ?
{formatBytes(a.size)}
: null}
))}
)}
); } interface MedicalItem { uuid: string; title: string; body?: string | null; recorded_at: number } /** پرونده پزشکی — medical exam entries: list + add/edit modal + delete. */ function MedicalRecordsTab({ uuid }: { uuid: string }) { const qc = useQueryClient(); const [modal, setModal] = useState<'create' | MedicalItem | null>(null); const [delTarget, setDelTarget] = useState(null); const [title, setTitle] = useState(''); const [date, setDate] = useState(''); const [body, setBody] = useState(''); const { data, isLoading } = useQuery>({ queryKey: ['patient-medical', uuid], queryFn: () => api.get(`/api/v1/patient/${uuid}/medical-records`), }); const items = data?.data ?? []; const invalidate = () => qc.invalidateQueries({ queryKey: ['patient-medical', uuid] }); const open = (m?: MedicalItem) => { setTitle(m?.title ?? ''); setBody(m?.body ?? ''); setDate(m?.recorded_at ? new Date(m.recorded_at * 1000).toISOString().slice(0, 10) : ''); setModal(m ?? 'create'); }; const save = useMutation({ mutationFn: () => { const payload = { title, body: body || null, recorded_at: date ? Math.floor(new Date(date).getTime() / 1000) : undefined }; return modal === 'create' ? api.post(`/api/v1/patient/${uuid}/medical-record`, payload) : api.patch(`/api/v1/patient/medical-record/${(modal as MedicalItem).uuid}`, payload); }, onSuccess: () => { invalidate(); setModal(null); toast.success('ذخیره شد'); }, onError: (e: any) => toast.error(e.message), }); const del = useMutation({ mutationFn: (u: string) => api.delete(`/api/v1/patient/medical-record/${u}`), onSuccess: () => { invalidate(); setDelTarget(null); toast.success('حذف شد'); }, onError: (e: any) => { toast.error(e.message); setDelTarget(null); }, }); return (
{isLoading ? (
در حال بارگذاری...
) : items.length === 0 ? (
معاینه‌ای ثبت نشده است.
) : (
{items.map((m) => (
{m.title}
{formatDate(m.recorded_at)}
{m.body &&
{m.body}
}
))}
)} setModal(null)} title={modal === 'create' ? 'ثبت معاینه جدید' : 'ویرایش معاینه'}>
setTitle(e.target.value)} placeholder="مثلاً: معاینه اولیه" autoFocus />