import React, { useState, useCallback, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { MagnifyingGlassIcon, PlusIcon, ChevronRightIcon, PencilIcon, PhoneIcon, FolderOpenIcon, UsersIcon, UserPlusIcon, CheckCircleIcon, } from '@heroicons/react/24/outline'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse, PaginatedResponse } from '../lib/api'; import type { PatientRecord, PatientSession, ServiceSection, ServiceItem } from '../types'; import { formatDate, formatDateTime, formatRial, formatNumber } from '../lib/utils'; import DataTable, { type Column } from '../components/ui/DataTable'; import Pagination from '../components/ui/Pagination'; import Modal from '../components/ui/Modal'; import PageHeader from '../components/ui/PageHeader'; import SearchableSelect from '../components/ui/SearchableSelect'; import FeatureGate from '../components/ui/FeatureGate'; const sessionSchema = z.object({ visit_price_rials: z.coerce.number().min(0), base_insurance_discount_percent: z.coerce.number().min(0).max(100), supplementary_discount_percent: z.coerce.number().min(0).max(100), payment_method: z.enum(['cash', 'card', 'insurance', 'online', 'pending']), notes: z.string().optional(), }); type SessionFormData = z.infer; const PAYMENT_LABELS: Record = { cash: 'نقدی', card: 'کارت', insurance: 'بیمه', online: 'آنلاین', pending: 'در انتظار', }; const PAYMENT_BADGE: Record = { cash: 'green', card: 'blue', insurance: 'purple', online: 'blue', pending: 'amber', }; const EMPTY_RECORDS: PatientRecord[] = []; const EMPTY_SESSIONS: PatientSession[] = []; function calcFinalPrice(visitPrice: number, baseDiscount: number, suppDiscount: number, servicesTotal: number) { const afterBase = visitPrice * (1 - baseDiscount / 100); const afterSupp = afterBase * (1 - suppDiscount / 100); return Math.round(afterSupp) + servicesTotal; } function MyPatientsPageInner() { const qc = useQueryClient(); const [selectedRecord, setSelectedRecord] = useState(null); const [page, setPage] = useState(1); const [search, setSearch] = useState(''); const [sessionPage, setSessionPage] = useState(1); const [sessionModal, setSessionModal] = useState(false); const [editSession, setEditSession] = useState(null); const form = useForm({ resolver: zodResolver(sessionSchema), defaultValues: { visit_price_rials: 0, base_insurance_discount_percent: 0, supplementary_discount_percent: 0, payment_method: 'cash' }, }); const watchVisit = form.watch('visit_price_rials') ?? 0; const watchBase = form.watch('base_insurance_discount_percent') ?? 0; const watchSupp = form.watch('supplementary_discount_percent') ?? 0; const [selectedServices, setSelectedServices] = useState<{ service_item_uuid: string; name: string; price_rials: number }[]>([]); const [sectionUuid, setSectionUuid] = useState(''); const [itemUuid, setItemUuid] = useState(''); const [createRecordOpen, setCreateRecordOpen] = useState(false); const [searchMobile, setSearchMobile] = useState(''); const [foundUser, setFoundUser] = useState<{ uuid: string; name: string | null; mobile: string } | null>(null); const [searchError, setSearchError] = useState(''); const mobileInputRef = useRef(null); const servicesTotal = selectedServices.reduce((sum, s) => sum + s.price_rials, 0); const finalPrice = calcFinalPrice(Number(watchVisit), Number(watchBase), Number(watchSupp), servicesTotal); const limit = 20; const { data: recordsData, isLoading } = useQuery>({ queryKey: ['patients', page, search], queryFn: () => api.get(`/api/v1/patients?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`), }); const { data: sessionsData, isLoading: sessionsLoading } = useQuery>({ queryKey: ['patient-sessions', selectedRecord?.uuid, sessionPage], queryFn: () => api.get(`/api/v1/patient/${selectedRecord!.uuid}/sessions?page=${sessionPage}&limit=20`), enabled: !!selectedRecord, }); const { data: sectionsData } = useQuery>({ queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'), }); const { data: itemsData } = useQuery>({ queryKey: ['service-items-for-session', sectionUuid], queryFn: () => api.get(`/api/v1/service-items/${sectionUuid}`), enabled: !!sectionUuid, }); const records = recordsData?.data ?? EMPTY_RECORDS; const sessions = sessionsData?.data ?? EMPTY_SESSIONS; const totalRec = recordsData?.meta?.totalRecords ?? 0; const totalSes = sessionsData?.meta?.totalRecords ?? 0; const sectionOptions = (sectionsData?.data ?? []).map((s) => ({ value: s.uuid, label: s.name })); const itemOptions = (itemsData?.data ?? []).filter((i) => i.active).map((i) => ({ value: i.uuid, label: `${i.name} — ${formatRial(i.price_rials)}`, })); const createSessionMut = useMutation({ mutationFn: (body: object) => api.post(`/api/v1/patient/${selectedRecord!.uuid}/session`, body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['patient-sessions', selectedRecord?.uuid] }); setSessionModal(false); form.reset(); setSelectedServices([]); toast.success('مراجعه ثبت شد'); }, onError: (e: any) => toast.error(e.message), }); const updateSessionMut = useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: object }) => api.patch(`/api/v1/session/${uuid}`, body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['patient-sessions', selectedRecord?.uuid] }); setEditSession(null); toast.success('مراجعه ویرایش شد'); }, onError: (e: any) => toast.error(e.message), }); const searchUserMut = useMutation({ mutationFn: (mobile: string) => api.get(`/api/v1/patient/search-user?mobile=${encodeURIComponent(mobile)}`), onSuccess: (res: any) => { setFoundUser(res?.data); setSearchError(''); }, onError: () => { setFoundUser(null); setSearchError('کاربری با این شماره در سیستم یافت نشد'); }, }); const createRecordMut = useMutation({ mutationFn: (userUuid: string) => api.post('/api/v1/patient', { user_uuid: userUuid }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['patients'] }); setCreateRecordOpen(false); setSearchMobile(''); setFoundUser(null); setSearchError(''); toast.success('پرونده بیمار ایجاد شد'); }, onError: (e: any) => toast.error(e.message), }); const handleSearchMobile = () => { const digits = searchMobile.replace(/\D/g, ''); if (!/^09\d{9}$/.test(digits)) { setSearchError('شماره موبایل معتبر نیست'); return; } setSearchError(''); setFoundUser(null); searchUserMut.mutate(digits); }; const handleCreateRecordClose = () => { setCreateRecordOpen(false); setSearchMobile(''); setFoundUser(null); setSearchError(''); }; const handleAddService = () => { if (!itemUuid) return; const found = itemsData?.data?.find((i) => i.uuid === itemUuid); if (!found || selectedServices.some((s) => s.service_item_uuid === found.uuid)) return; setSelectedServices((p) => [...p, { service_item_uuid: found.uuid, name: found.name, price_rials: found.price_rials }]); setItemUuid(''); }; const handleSubmitSession = form.handleSubmit((d) => { createSessionMut.mutate({ ...d, services: selectedServices.map((s) => ({ service_item_uuid: s.service_item_uuid })) }); }); const handleSearch = useCallback((v: string) => { setSearch(v); setPage(1); }, []); const recordColumns: Column[] = [ { key: 'user', header: 'بیمار', render: (r) => (
{(r.user.fullName ?? '?').charAt(0)}
{r.user.fullName ?? '—'}
{r.user.phone ?? '—'}
), }, { key: 'created_at', header: 'تاریخ ثبت', render: (r) => ( {formatDate(r.created_at)} ), }, { key: 'uuid', header: '', render: (r) => ( ), }, ]; if (!selectedRecord) { return ( <> setCreateRecordOpen(true)}> پرونده جدید } />
handleSearch(e.target.value)} placeholder="جستجو بر اساس نام یا تلفن..." />
{records.length === 0 && !isLoading ? (
{search ? 'بیماری یافت نشد' : 'هنوز بیماری ثبت نشده است'}
{search ? 'عبارت جستجو را تغییر دهید' : 'پس از ثبت نوبت، پرونده بیمار به طور خودکار ایجاد می‌شود'}
) : ( <>
)}
{/* Modal ایجاد پرونده دستی */} } >
{ const digits = e.target.value.replace(/\D/g, ''); setSearchMobile(digits); setFoundUser(null); setSearchError(''); }} onKeyDown={(e) => { if (e.key === 'Enter') handleSearchMobile(); }} />
{searchError && {searchError}}
{foundUser && (
{foundUser.name ?? 'بدون نام'}
{foundUser.mobile}
)}

بیمار باید قبلاً در سیستم ثبت‌نام کرده باشد. با شماره موبایل جستجو کنید، سپس پرونده ایجاد کنید.

); } return ( <> } /> {/* بنر اطلاعات بیمار */}
{(selectedRecord.user.fullName ?? '?').charAt(0)}
{selectedRecord.user.fullName ?? '—'}
{selectedRecord.user.phone ?? '—'}
{totalSes}
مراجعه
تاریخچه مراجعات
{sessionsLoading ? (
در حال بارگذاری...
) : sessions.length === 0 ? (
مراجعه‌ای ثبت نشده است
) : ( sessions.map((s) => ) )} {totalSes > 20 && (
)}
{/* Modal مراجعه جدید */} setSessionModal(false)} title="ثبت مراجعه جدید">