diff --git a/assets/admin/pages/MyPatientsPage.tsx b/assets/admin/pages/MyPatientsPage.tsx index 1010f017..0e437b49 100644 --- a/assets/admin/pages/MyPatientsPage.tsx +++ b/assets/admin/pages/MyPatientsPage.tsx @@ -1,86 +1,401 @@ -import React, { useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import { PhoneIcon } from '@heroicons/react/24/outline'; +import React, { useState, useCallback } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + MagnifyingGlassIcon, PlusIcon, ChevronRightIcon, PencilIcon, PhoneIcon, +} 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 { PaginatedResponse } from '../lib/api'; -import { formatDate, formatNumber } from '../lib/utils'; +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'; -interface Patient { - uuid: string; - name: string; - mobile: string; - total_appointments: number; - last_appointment: number | null; +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 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; } -const EMPTY: Patient[] = []; - export default function MyPatientsPage() { + 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 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, isLoading } = useQuery>({ - queryKey: ['my-patients', page, search], - queryFn: () => api.get(`/api/v1/my/patients?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`), + const { data: recordsData, isLoading } = useQuery>({ + queryKey: ['patients', page, search], + queryFn: () => api.get(`/api/v1/patients?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`), }); - const patients = data?.data ?? EMPTY; - const total = data?.meta?.totalRecords ?? 0; + 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 columns: Column[] = [ + 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 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: 'name', - header: 'نام بیمار', - render: (p) => ( + key: 'user', + header: 'بیمار', + render: (r) => (
- {(p.name || '؟').charAt(0)} + {(r.user.fullName ?? '?').charAt(0)} +
+
+
{r.user.fullName ?? '—'}
+
+ + {r.user.phone ?? '—'} +
- {p.name || '—'}
), }, + { key: 'created_at', header: 'تاریخ ثبت', render: (r) => formatDate(r.created_at) }, { - key: 'mobile', - header: 'موبایل', - render: (p) => ( -
- - {p.mobile} -
+ key: 'uuid', + header: 'عملیات', + render: (r) => ( + ), }, - { - key: 'total_appointments', - header: 'تعداد نوبت', - render: (p) => {formatNumber(p.total_appointments)}, - }, - { - key: 'last_appointment', - header: 'آخرین نوبت', - render: (p) => p.last_appointment ? formatDate(String(p.last_appointment)) : '—', - }, ]; - return ( -
- + if (!selectedRecord) { + return ( + <> + +
+
+
+ + handleSearch(e.target.value)} + placeholder="جستجو بر اساس نام یا تلفن..." + /> +
+
+ +
+ +
+
+ + ); + } - { setSearch(v); setPage(1); }} - searchPlaceholder="جستجوی نام یا موبایل..." - emptyMessage="بیماری یافت نشد" + return ( + <> + + + +
+ } /> - +
+
مراجعات
+ {sessionsLoading ? ( +
در حال بارگذاری...
+ ) : sessions.length === 0 ? ( +
مراجعه‌ای ثبت نشده است
+ ) : ( + sessions.map((s) => ) + )} + {totalSes > 20 && ( +
+ +
+ )} +
+ + {/* Modal مراجعه جدید */} + setSessionModal(false)} title="ثبت مراجعه جدید"> +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +