import { CheckCircleIcon, ChevronRightIcon, FolderOpenIcon, IdentificationIcon, MagnifyingGlassIcon, PencilIcon, PhoneIcon, PlusIcon, UserPlusIcon, UsersIcon, } from "@heroicons/react/24/outline"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import React, { useCallback, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import FeatureGate from "../components/ui/FeatureGate"; import Modal from "../components/ui/Modal"; import PageHeader from "../components/ui/PageHeader"; import Pagination from "../components/ui/Pagination"; import SearchableSelect from "../components/ui/SearchableSelect"; import type { ApiResponse, PaginatedResponse } from "../lib/api"; import { api } from "../lib/api"; import { formatDate, formatDateTime, formatNumber, formatRial, } from "../lib/utils"; import type { PatientRecord, PatientSession, ServiceItem, ServiceSection, } from "../types"; 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 getPatientName(record?: PatientRecord | null) { return record?.user_name || "—"; } function getPatientPhone(record?: PatientRecord | null) { return record?.user_mobile || "—"; } function getPatientNationalCode(record?: PatientRecord | null) { return record?.user_national_code || null; } 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; national_code?: string | null; } | null>(null); const [recordNationalCode, setRecordNationalCode] = useState(""); 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< PaginatedResponse >({ queryKey: ["patients", page, search], queryFn: () => api.get( `/api/v1/patients?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`, ), }); const { data: sessionsData, isLoading: sessionsLoading } = useQuery< PaginatedResponse >({ 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 selectedPatientName = getPatientName(selectedRecord); const selectedPatientPhone = getPatientPhone(selectedRecord); 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); }, []); if (!selectedRecord) { return ( <> setCreateRecordOpen(true)} > پرونده جدید } />
handleSearch(e.target.value)} placeholder="جستجو بر اساس نام، شماره موبایل یا کد ملی..." />
{records.length === 0 && !isLoading ? (
{search ? "بیماری یافت نشد" : "هنوز بیماری ثبت نشده است"}
{search ? "عبارت جستجو را تغییر دهید" : "پس از ثبت نوبت، پرونده بیمار به طور خودکار ایجاد می‌شود"}
) : ( <>
{records.map((r) => ( { setSelectedRecord(r); setSessionPage(1); }} /> ))}
)} {/* 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 ?? "؟").charAt(0)}
{foundUser.name ?? "بدون نام"}
{foundUser.mobile}
یافت شد
)}
); } return ( <> } /> {/* بنر اطلاعات بیمار */}
{(selectedPatientName === "—" ? "?" : selectedPatientName ).charAt(0)}
{selectedPatientName}
{selectedPatientPhone}
{totalSes}
مراجعه
تاریخچه مراجعات
{sessionsLoading ? (
در حال بارگذاری...
) : sessions.length === 0 ? (
مراجعه‌ای ثبت نشده است
) : ( sessions.map((s) => ( )) )} {totalSes > 20 && (
)}
{/* Modal مراجعه جدید */} setSessionModal(false)} title="ثبت مراجعه جدید" >