import { useState } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { BanknotesIcon, ShieldCheckIcon, ClockIcon, UsersIcon, PencilIcon, CalendarDaysIcon, WrenchScrewdriverIcon, CubeIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import type { ServiceItem } from '../types'; import type { InventoryPackage } from '../hooks/useInventory'; import { formatRial, formatNumber, formatYear, formatDateTime } from '../lib/utils'; import PageHeader from '../components/ui/PageHeader'; import FeatureGate from '../components/ui/FeatureGate'; import ConfirmDialog from '../components/ui/ConfirmDialog'; import ServiceTariffModal from '../components/ServiceTariffModal'; import ServiceInsuranceModal from '../components/ServiceInsuranceModal'; import ServiceItemFormModal from '../components/ServiceItemFormModal'; interface Tariff { uuid: string; year: number; price_rials: number; is_active: boolean; } interface TariffList { current_year: number; default_price_rials: number; data: Tariff[]; } interface TenantInsurance { uuid: string; insurance_name: string | null; insurance_kind: 'basic' | 'supplementary' | null; coverage_percent: number; } interface CoverageRow { service_item_uuid: string | null; covered: boolean; coverage_percent: number | null; franchise_rials: number | null; ceiling_rials: number | null; } const TABS = [ { id: 'info', label: 'اطلاعات سرویس' }, { id: 'tariffs', label: 'تعرفه‌ها' }, { id: 'insurance', label: 'بیمه‌ها' }, { id: 'goods', label: 'کالاهای مرتبط' }, { id: 'history', label: 'لاگ تغییرات' }, ] as const; type TabId = typeof TABS[number]['id']; interface AuditLog { uuid: string; field: string; operation: 'create' | 'update'; old_value: string | null; new_value: string | null; actor_name: string | null; created_at: number; } /** برچسب فارسی فیلدهای لاگ — باید با ServiceItemAuditService::TRACKED هم‌تراز بماند. */ const FIELD_LABELS: Record = { name: 'نام سرویس', price_rials: 'قیمت پایه', active: 'وضعیت', duration_minutes: 'زمان متوسط', bookable: 'نمایش در نوبت‌دهی', insurance_covered: 'پوشش بیمه', inventory_package: 'پکیج کالا', }; /** مقدار خام لاگ را برای نمایش قابل‌فهم می‌کند (بولین، مبلغ، خالی). */ function auditValue(field: string, raw: string | null): string { if (raw === null || raw === '') return '—'; if (['active', 'bookable', 'insurance_covered'].includes(field)) return raw === '1' ? 'بله' : 'خیر'; if (field === 'price_rials') return formatRial(Number(raw)); if (field === 'duration_minutes') return `${formatNumber(Number(raw))} دقیقه`; if (field === 'inventory_package') return 'پکیج متصل'; return raw; } const KIND = { basic: { label: 'پایه', cls: 'blue' }, supplementary: { label: 'تکمیلی', cls: 'violet' }, } as const; function Row({ icon, label, children }: { icon: React.ReactNode; label: string; children: React.ReactNode }) { return (
{icon} {label} {children}
); } function InfoTab({ item }: { item: ServiceItem }) { const members = item.staff_members ?? (item.staff ? [item.staff] : []); return (
} label="قیمت پایه"> {formatRial(item.price_rials)} } label="بخش"> {item.section_name ?? '—'} } label="زمان متوسط"> {item.duration_minutes ? {formatNumber(Number(item.duration_minutes))} دقیقه : '—'} } label="نمایش در نوبت‌دهی"> {item.bookable ? فعال : غیرفعال} } label="پوشش بیمه"> {item.insurance_covered ? تحت پوشش : '—'} } label="پرسنل مسئول"> {members.length > 0 ? ( {members.map((m) => ( {m.full_name} ))} ) : '—'} } label="تاریخ ایجاد"> {item.created_at ? formatDateTime(item.created_at) : '—'}
آخرین ویرایش {item.updated_at ? formatDateTime(item.updated_at) : '—'}
); } function TariffsTab({ item, onManage }: { item: ServiceItem; onManage: () => void }) { const { data, isLoading } = useQuery>({ queryKey: ['service-tariffs', item.uuid], queryFn: () => api.get(`/api/v1/service-items/${item.uuid}/tariffs`), }); const list = data?.data; const tariffs = list?.data ?? []; return (
تعرفه‌های سالانه
قیمت هر سال شمسی؛ قیمت سال جاری مبنای صورتحساب است.
{isLoading ? (
در حال بارگذاری...
) : tariffs.length === 0 ? (

تعرفه‌ای ثبت نشده است

) : (
{tariffs.map((t) => (
{formatYear(t.year)} {t.year === list?.current_year && سال جاری} {!t.is_active && غیرفعال} {formatRial(t.price_rials)}
))}
)}
); } function InsuranceTab({ item, onManage }: { item: ServiceItem; onManage: () => void }) { const { data: contractsData, isLoading } = useQuery<{ data: { data: TenantInsurance[] } }>({ queryKey: ['tenant-insurances'], queryFn: () => api.get('/api/v1/billing/tenant-insurances'), }); const contracts = (contractsData as any)?.data?.data as TenantInsurance[] | undefined ?? []; return (
بیمه‌های مرتبط
درصد پوشش، فرانشیز و سقف هر بیمه‌گر برای این خدمت.
{isLoading ? (
در حال بارگذاری...
) : contracts.length === 0 ? (

قرارداد بیمه‌ی فعالی ندارید

) : (
{contracts.map((c) => ( ))}
)}
); } function ContractCoverageRow({ contract, itemUuid }: { contract: TenantInsurance; itemUuid: string }) { const { data } = useQuery<{ data: { data: CoverageRow[] } }>({ queryKey: ['service-coverage', contract.uuid], queryFn: () => api.get(`/api/v1/billing/tenant-insurances/${contract.uuid}/service-coverage`), }); const row = ((data as any)?.data?.data as CoverageRow[] | undefined) ?.find((r) => r.service_item_uuid === itemUuid); const kind = contract.insurance_kind ? KIND[contract.insurance_kind] : null; const percent = row?.coverage_percent ?? contract.coverage_percent; return (
{contract.insurance_name ?? 'بیمه'} {kind && {kind.label}} {row && !row.covered ? ( بدون پوشش ) : ( پوشش {formatNumber(percent)}٪ {row?.franchise_rials ? فرانشیز {formatRial(row.franchise_rials)} : null} {row?.ceiling_rials ? سقف {formatRial(row.ceiling_rials)} : null} {!row && (ارث از قرارداد)} )}
); } function GoodsTab({ item, onEdit }: { item: ServiceItem; onEdit: () => void }) { const { data, isLoading } = useQuery>({ queryKey: ['inventory-packages'], queryFn: () => api.get('/api/v1/inventory-packages'), }); const linked = (data?.data ?? []).find((p) => p.uuid === item.inventory_package_uuid); const consumables = item.consumables ?? []; const nothingLinked = !item.inventory_package_uuid && consumables.length === 0; return (
کالاهای مرتبط
پکیج آماده و کالاهای تکیِ مصرفی این خدمت؛ در «انبار» ساخته و در فرم سرویس انتخاب می‌شوند.
{isLoading ? (
در حال بارگذاری...
) : nothingLinked ? (

کالایی به این سرویس متصل نیست

مدیریت انبار
) : (
{item.inventory_package_uuid && (
پکیج
{!linked ? (
{item.inventory_package_title ?? 'پکیج متصل'} — جزئیات در دسترس نیست
) : ( <>
{linked.title} {!linked.available && موجودی ناکافی} {formatRial(linked.total)}
{linked.items.map((line) => (
{line.name} {formatNumber(line.amount)} {line.unit} {formatRial(line.price * line.amount)}
))}
)}
)} {consumables.length > 0 && (
کالاهای تکی
{consumables.map((line) => (
{line.name} {line.stock < line.amount && موجودی ناکافی} {formatNumber(line.amount)} {line.unit} {formatRial(line.price * line.amount)}
))}
)}
)}
); } function HistoryTab({ item }: { item: ServiceItem }) { const { data, isLoading } = useQuery>({ queryKey: ['service-item-audit', item.uuid], queryFn: () => api.get(`/api/v1/service-item/${item.uuid}/audit-logs`), }); const logs = data?.data ?? []; return (
لاگ تغییرات
چه کسی، چه فیلدی را، کِی و از چه مقداری به چه مقداری تغییر داد.
{isLoading ? (
در حال بارگذاری...
) : logs.length === 0 ? (

تغییری ثبت نشده است

) : (
{logs.map((log) => (
{FIELD_LABELS[log.field] ?? log.field} {log.operation === 'create' && ایجاد} {formatDateTime(log.created_at)}
{log.operation === 'update' && (
{auditValue(log.field, log.old_value)} {auditValue(log.field, log.new_value)}
)} {log.actor_name && (
توسط {log.actor_name}
)}
))}
)}
); } function ServiceDetailPageInner() { const { uuid } = useParams<{ uuid: string }>(); const navigate = useNavigate(); const qc = useQueryClient(); const [tab, setTab] = useState('info'); const [editOpen, setEditOpen] = useState(false); const [tariffOpen, setTariffOpen] = useState(false); const [insuranceOpen, setInsuranceOpen] = useState(false); const [toggleOpen, setToggleOpen] = useState(false); const { data, isLoading, isError } = useQuery>({ queryKey: ['service-item', uuid], queryFn: () => api.get(`/api/v1/service-item/${uuid}`), enabled: !!uuid, }); const item = data?.data; const toggleActive = useMutation({ mutationFn: () => api.patch(`/api/v1/service-item/${uuid}`, { active: !item!.active }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['service-item', uuid] }); qc.invalidateQueries({ queryKey: ['service-items'] }); setToggleOpen(false); toast.success(item!.active ? 'سرویس غیرفعال شد' : 'سرویس فعال شد'); }, onError: (e: Error) => { toast.error(e.message); setToggleOpen(false); }, }); if (isLoading) { return
در حال بارگذاری...
; } if (isError || !item) { return (
سرویس یافت نشد
); } return (
} />
{item.active ? 'فعال' : 'غیرفعال'}
{TABS.map((t) => ( ))}
{tab === 'info' && } {tab === 'tariffs' && setTariffOpen(true)} />} {tab === 'insurance' && setInsuranceOpen(true)} />} {tab === 'goods' && setEditOpen(true)} />} {tab === 'history' && } setEditOpen(false)} onManageInsurance={() => setInsuranceOpen(true)} /> setTariffOpen(false)} /> setInsuranceOpen(false)} /> toggleActive.mutate()} onCancel={() => setToggleOpen(false)} loading={toggleActive.isPending} /> ); } export default function ServiceDetailPage() { return ( ); }