import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { PlusIcon, PencilIcon, TrashIcon, WrenchScrewdriverIcon, BanknotesIcon, ShieldCheckIcon, MagnifyingGlassIcon, EyeIcon, EyeSlashIcon, EllipsisHorizontalIcon, CheckCircleIcon, XCircleIcon, ChevronRightIcon, XMarkIcon, UsersIcon, ClockIcon, } from '@heroicons/react/24/outline'; import SettingsLayout from '../components/layout/SettingsLayout'; 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 } from '../lib/api'; import type { ServiceSection, ServiceItem, ClinicStaff } from '../types'; import { formatRial, formatNumber, rialToToman, tomanToRial } from '../lib/utils'; import Modal from '../components/ui/Modal'; import PriceInput from '../components/ui/PriceInput'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import ServiceTariffModal from '../components/ServiceTariffModal'; import ServiceInsuranceModal from '../components/ServiceInsuranceModal'; import SearchableSelect from '../components/ui/SearchableSelect'; import FeatureGate from '../components/ui/FeatureGate'; const sectionSchema = z.object({ name: z.string().min(1, 'نام بخش الزامی است') }); const itemSchema = z.object({ name: z.string().min(1, 'نام سرویس الزامی است'), price_rials: z.coerce.number().min(0, 'مبلغ نمی‌تواند منفی باشد'), staff_uuids: z.array(z.string()).optional(), insurance_covered: z.boolean().optional(), insurance_price_rials: z.coerce.number().min(0).optional(), duration_minutes: z.coerce.number().min(0).optional(), }); type SectionForm = z.infer; type ItemForm = z.infer; const EMPTY_SECTIONS: ServiceSection[] = []; const EMPTY_ITEMS: ServiceItem[] = []; function Avatar({ name }: { name: string }) { return (
{name.charAt(0)}
); } function SrvRow({ label, value, strong, danger }: { label: string; value: string; strong?: boolean; danger?: boolean }) { return (
{label}: {value}
); } function ClinicServicesPageInner() { const qc = useQueryClient(); const [selectedSection, setSelectedSection] = useState(null); const [sectionModal, setSectionModal] = useState<'create' | ServiceSection | null>(null); const [deleteSection, setDeleteSection] = useState(null); const [itemModal, setItemModal] = useState<'create' | ServiceItem | null>(null); const [toggleItem, setToggleItem] = useState(null); const [tariffItem, setTariffItem] = useState(null); const [insuranceItem, setInsuranceItem] = useState(null); const [search, setSearch] = useState(''); const [showInactive, setShowInactive] = useState(true); const [menuOpen, setMenuOpen] = useState(null); const { data: sectionsData, isLoading: sectionsLoading } = useQuery>({ queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'), }); const { data: itemsData, isLoading: itemsLoading } = useQuery>({ queryKey: ['service-items', selectedSection?.uuid], queryFn: () => api.get(`/api/v1/service-items/${selectedSection!.uuid}`), enabled: !!selectedSection, }); const { data: staffData } = useQuery>({ queryKey: ['staff'], queryFn: () => api.get('/api/v1/staff'), }); const sections = sectionsData?.data ?? EMPTY_SECTIONS; const allItems = itemsData?.data ?? EMPTY_ITEMS; const allStaff = staffData?.data ?? []; const sectionForm = useForm({ resolver: zodResolver(sectionSchema) }); const itemForm = useForm({ resolver: zodResolver(itemSchema) }); const selectedStaffUuids = itemForm.watch('staff_uuids') ?? []; const editingMembers = itemModal && typeof itemModal === 'object' ? (itemModal.staff_members ?? (itemModal.staff ? [itemModal.staff] : [])) : []; const staffOptions = allStaff .filter((s) => s.active || editingMembers.some((m) => m.uuid === s.uuid)) .filter((s) => !selectedStaffUuids.includes(s.uuid)) .map((s) => ({ value: s.uuid, label: s.active ? s.full_name : `${s.full_name} (غیرفعال)`, })); const staffNameOf = (uuid: string) => allStaff.find((s) => s.uuid === uuid)?.full_name ?? editingMembers.find((m) => m.uuid === uuid)?.full_name ?? uuid; const items = allItems.filter((it) => { if (!showInactive && !it.active) return false; if (search.trim() && !it.name.includes(search.trim())) return false; return true; }); const activeCount = allItems.filter((i) => i.active).length; const createSection = useMutation({ mutationFn: (body: SectionForm) => api.post('/api/v1/service-section', body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-sections'] }); setSectionModal(null); sectionForm.reset(); toast.success('بخش ایجاد شد'); }, onError: (e: any) => toast.error(e.message), }); const editSection = useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: SectionForm }) => api.patch(`/api/v1/service-section/${uuid}`, body), onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-sections'] }); setSectionModal(null); toast.success('بخش ویرایش شد'); }, onError: (e: any) => toast.error(e.message), }); const delSection = useMutation({ mutationFn: (uuid: string) => api.delete(`/api/v1/service-section/${uuid}`), onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-sections'] }); if (selectedSection?.uuid === deleteSection?.uuid) setSelectedSection(null); setDeleteSection(null); toast.success('بخش حذف شد'); }, onError: (e: any) => { toast.error(e.message); setDeleteSection(null); }, }); const toggleSection = useMutation({ mutationFn: ({ uuid, active }: { uuid: string; active: boolean }) => api.patch(`/api/v1/service-section/${uuid}`, { active }), onSuccess: (_d, v) => { qc.invalidateQueries({ queryKey: ['service-sections'] }); toast.success(v.active ? 'بخش فعال شد' : 'بخش غیرفعال شد'); }, onError: (e: any) => toast.error(e.message), }); const createItem = useMutation({ mutationFn: (body: ItemForm & { section_uuid: string }) => api.post('/api/v1/service-item', { ...body, price_rials: tomanToRial(body.price_rials), insurance_price_rials: tomanToRial(body.insurance_price_rials ?? 0), }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-items', selectedSection?.uuid] }); setItemModal(null); itemForm.reset(); toast.success('سرویس ایجاد شد'); }, onError: (e: any) => toast.error(e.message), }); const editItem = useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: ItemForm }) => api.patch(`/api/v1/service-item/${uuid}`, { ...body, price_rials: tomanToRial(body.price_rials), insurance_price_rials: tomanToRial(body.insurance_price_rials ?? 0), }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-items', selectedSection?.uuid] }); setItemModal(null); toast.success('سرویس ویرایش شد'); }, onError: (e: any) => toast.error(e.message), }); const toggleActive = useMutation({ mutationFn: ({ uuid, active }: { uuid: string; active: boolean }) => api.patch(`/api/v1/service-item/${uuid}`, { active }), onSuccess: (_d, v) => { qc.invalidateQueries({ queryKey: ['service-items', selectedSection?.uuid] }); setToggleItem(null); toast.success(v.active ? 'سرویس فعال شد' : 'سرویس غیرفعال شد'); }, onError: (e: any) => { toast.error(e.message); setToggleItem(null); }, }); const openEditSection = (s: ServiceSection) => { sectionForm.reset({ name: s.name }); setSectionModal(s); }; const openEditItem = (item: ServiceItem) => { itemForm.reset({ name: item.name, price_rials: rialToToman(item.price_rials), staff_uuids: (item.staff_members ?? (item.staff ? [item.staff] : [])).map((s) => s.uuid), insurance_covered: item.insurance_covered ?? false, insurance_price_rials: rialToToman(item.insurance_price_rials ?? 0), duration_minutes: item.duration_minutes ?? undefined, }); setItemModal(item); }; const openCreateItem = () => { itemForm.reset({ name: '', price_rials: 0, staff_uuids: [], insurance_covered: false, insurance_price_rials: 0, duration_minutes: undefined }); setItemModal('create'); }; return ( <> {!selectedSection ? ( /* ═════════ نمای بخش‌ها ═════════ */ <>
بخش‌ها
{sectionsLoading ? (
در حال بارگذاری...
) : sections.length === 0 ? (
برای اضافه کردن بخش روی دکمه بخش جدید کلیک نمایید.
) : (
{sections.map((s) => (
setSelectedSection(s)} style={{ cursor: 'pointer', background: 'var(--surface)', borderRadius: 'var(--r)', boxShadow: '0 1px 24.8px rgba(204,204,204,0.18)', border: '1px solid var(--border)', padding: '16px 16px 12px', opacity: s.active ? 1 : 0.62, }} >
{s.name}
تعداد: {formatNumber(s.items_count ?? 0)} سرویس
e.stopPropagation()}> وضعیت: {s.active ? 'فعال' : 'غیرفعال'}
e.stopPropagation()}>
))}
)} ) : ( /* ═════════ نمای سرویس‌های یک بخش ═════════ */ <>
setSelectedSection(null)}>بخش‌ها {selectedSection.name}
setSearch(e.target.value)} style={{ width: 170, paddingInlineStart: 30, height: 36 }} />
{itemsLoading ? (
در حال بارگذاری...
) : items.length === 0 ? (
{allItems.length === 0 ? 'سرویسی در این بخش وجود ندارد' : 'سرویسی با این فیلتر یافت نشد'}
{allItems.length === 0 && ( )}
) : (
{items.map((item) => { const menuItemStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '8px 10px', borderRadius: 8, border: 'none', cursor: 'pointer', background: 'transparent', color: 'var(--text-2)', fontSize: 13, fontFamily: 'inherit', textAlign: 'start', whiteSpace: 'nowrap', }; const chipStyle: React.CSSProperties = { fontSize: 12, color: 'var(--text-2)', background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 999, padding: '3px 10px', whiteSpace: 'nowrap', }; return (
{/* نوار بالا: ⋮ (چپ) + وضعیت و نام (راست) */}
{item.active ? 'فعال' : 'غیرفعال'} {item.name}
{menuOpen === item.uuid && ( <>
setMenuOpen(null)} />
)}
قیمت پایه: {formatRial(item.price_rials)}
زمان متوسط: {item.duration_minutes ? {formatNumber(Number(item.duration_minutes))} دقیقه : }
{item.insurance_covered && item.insurance_price_rials != null && ( )}
پرسنل:
{(item.staff_members && item.staff_members.length > 0) ? item.staff_members.map((m) => {m.full_name}) : item.staff ? {item.staff.full_name} : }
); })}
)} )} {/* ───────── Modal بخش ───────── */} setSectionModal(null)} title={sectionModal === 'create' ? 'بخش جدید' : 'ویرایش بخش'} >
{ if (sectionModal === 'create') createSection.mutate(d); else if (sectionModal !== null && typeof sectionModal === 'object') editSection.mutate({ uuid: sectionModal.uuid, body: d }); })}>
{sectionForm.formState.errors.name && ( {sectionForm.formState.errors.name.message} )}
{/* ───────── Modal سرویس ───────── */} setItemModal(null)} title={itemModal === 'create' ? 'سرویس جدید' : 'ویرایش سرویس'} size="md" footer={ <> } >
{ if (itemModal === 'create' && selectedSection) { createItem.mutate({ ...d, section_uuid: selectedSection.uuid }); } else if (itemModal !== null && typeof itemModal === 'object') { editItem.mutate({ uuid: itemModal.uuid, body: d }); } })} style={{ display: 'flex', flexDirection: 'column', gap: 20 }} > {/* اطلاعات پایه */}
{itemForm.formState.errors.name && ( {itemForm.formState.errors.name.message} )}
itemForm.setValue('price_rials', v)} placeholder="۸۵,۰۰۰" min={0} />
{itemForm.formState.errors.price_rials && ( {itemForm.formState.errors.price_rials.message} )}
{ if (v != null) itemForm.setValue('staff_uuids', [...selectedStaffUuids, String(v)]); }} placeholder="افزودن پرسنل (اختیاری)" noOptionsMessage="پرسنلی باقی نمانده" height={42} /> {selectedStaffUuids.length > 0 && (
{selectedStaffUuids.map((uuid) => ( {staffNameOf(uuid)} ))}
)}
{/* بیمه */}
{itemForm.watch('insurance_covered') && (
itemForm.setValue('insurance_price_rials', v)} placeholder="سهم تقریبی بیمار" min={0} />
برای محاسبه‌ی دقیق سهم بیمار و ساخت مطالبات، پوشش هر بیمه‌گر را از دکمه‌ی «پوشش بیمه» در فهرست سرویس‌ها تنظیم کنید.
)}
setTariffItem(null)} /> setInsuranceItem(null)} /> {/* Confirm حذف بخش */} deleteSection && delSection.mutate(deleteSection.uuid)} onCancel={() => setDeleteSection(null)} loading={delSection.isPending} /> {/* Confirm فعال/غیرفعال سرویس */} toggleItem && toggleActive.mutate({ uuid: toggleItem.uuid, active: !toggleItem.active })} onCancel={() => setToggleItem(null)} loading={toggleActive.isPending} /> ); } export default function ClinicServicesPage() { return ( ); }