import React, { useState } from 'react'; import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { PlusIcon, PencilIcon, WrenchScrewdriverIcon, BanknotesIcon, ShieldCheckIcon, MagnifyingGlassIcon, EyeIcon, EyeSlashIcon, EllipsisHorizontalIcon, CheckCircleIcon, XCircleIcon, ChevronRightIcon, XMarkIcon, UsersIcon, ClockIcon, } 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 } from '../lib/api'; import type { ServiceSection, ServiceItem } from '../types'; import { formatRial, formatNumber } from '../lib/utils'; import { usePermissions } from '../hooks/usePermissions'; import PageHeader from '../components/ui/PageHeader'; import Modal from '../components/ui/Modal'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import ServiceInsuranceModal from '../components/ServiceInsuranceModal'; import ServiceItemFormModal from '../components/ServiceItemFormModal'; import FeatureGate from '../components/ui/FeatureGate'; import Switch from '../components/ui/Switch'; const sectionSchema = z.object({ name: z.string().min(1, 'نام بخش الزامی است') }); type SectionForm = z.infer; const EMPTY_SECTIONS: ServiceSection[] = []; const EMPTY_ITEMS: ServiceItem[] = []; function Avatar({ name }: { name: string }) { return (
{name.charAt(0)}
); } function ClinicServicesPageInner() { const qc = useQueryClient(); // مجوزهای منشی؛ برای owner/پزشک همیشه true (usePermissions بدون context آزاد است). const { can } = usePermissions(); const canCreate = can('services', 'create'); const canUpdate = can('services', 'update'); const navigate = useNavigate(); const [selectedSection, setSelectedSection] = useState(null); const [sectionModal, setSectionModal] = useState<'create' | ServiceSection | null>(null); const [itemModal, setItemModal] = useState<'create' | ServiceItem | null>(null); const [toggleItem, setToggleItem] = 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 sections = sectionsData?.data ?? EMPTY_SECTIONS; const allItems = itemsData?.data ?? EMPTY_ITEMS; const sectionForm = useForm({ resolver: zodResolver(sectionSchema) }); 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 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 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 openCreateItem = () => setItemModal('create'); return ( <> {!selectedSection ? ( /* ═════════ نمای بخش‌ها ═════════ */ <> {/* تیتر دست‌ساز جای `PageHeader` بود: بدون آن این صفحه تنها فهرستِ پنل است که نه دکمهٔ بازگشت دارد و نه می‌گوید چیست. */} { sectionForm.reset({ name: '' }); setSectionModal('create'); }}> بخش جدید ) : undefined } /> {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()}> وضعیت: toggleSection.mutate({ uuid: s.uuid, active: !s.active })} ariaLabel={`وضعیت ${s.name}`} /> {s.active ? 'فعال' : 'غیرفعال'}
{canUpdate && (
e.stopPropagation()}>
)}
))}
)} ) : ( /* ═════════ نمای سرویس‌های یک بخش ═════════ */ <>
setSelectedSection(null)}>بخش‌ها {selectedSection.name}
setSearch(e.target.value)} style={{ width: 170, paddingInlineStart: 30, height: 36 }} />
{canCreate && ( )}
{itemsLoading ? (
در حال بارگذاری...
) : items.length === 0 ? (
{allItems.length === 0 ? 'سرویسی در این بخش وجود ندارد' : 'سرویسی با این فیلتر یافت نشد'}
{allItems.length === 0 && canCreate && ( )}
) : (
{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 (
navigate(`/admin/clinic-services/${item.uuid}`)} onKeyDown={(e) => { if (e.key === 'Enter') navigate(`/admin/clinic-services/${item.uuid}`); }} style={{ position: 'relative', background: 'var(--surface)', borderRadius: 8, boxShadow: '0 1px 24.8px rgba(204,204,204,0.18)', border: '1px solid var(--border)', padding: 14, cursor: 'pointer', opacity: item.active ? 1 : 0.7, }} > {/* نوار بالا: ⋮ (چپ) + وضعیت و نام (راست) */}
{canUpdate ? ( ) : }
{item.active ? 'فعال' : 'غیرفعال'} {item.name}
{menuOpen === item.uuid && ( <>
{ e.stopPropagation(); setMenuOpen(null); }} />
e.stopPropagation()} style={{ position: 'absolute', top: 40, insetInlineStart: 8, zIndex: 41, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--r-sm)', boxShadow: 'var(--shadow)', padding: 6, minWidth: 176, display: 'flex', flexDirection: 'column', gap: 2 }}>
)}
قیمت پایه: {formatRial(item.price_rials)}
زمان متوسط: {item.bookable && در نوبت‌دهی} {item.duration_minutes ? {formatNumber(Number(item.duration_minutes))} دقیقه : }
بیمه: {item.insurance_covered ? تحت پوشش : }
پرسنل:
{(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)} onManageInsurance={setInsuranceItem} /> setInsuranceItem(null)} /> {/* Confirm فعال/غیرفعال سرویس */} toggleItem && toggleActive.mutate({ uuid: toggleItem.uuid, active: !toggleItem.active })} onCancel={() => setToggleItem(null)} loading={toggleActive.isPending} /> ); } export default function ClinicServicesPage() { return ( ); }