import { useMemo, useState, type ReactNode } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { PlusIcon, PencilIcon, MagnifyingGlassIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import { formatRial, formatNumber, formatDate } from '../lib/utils'; import { useAuthStore } from '../stores/authStore'; import { usePermissions } from '../hooks/usePermissions'; import SearchableSelect from './ui/SearchableSelect'; import type { ClinicDoctorItem } from './ClinicDoctorsManager'; import InsuranceModal, { Contract, InsuranceOption, KIND_LABEL, buildInsurancePayload } from './InsuranceModal'; type Kind = 'basic' | 'supplementary'; const KINDS: { key: Kind; label: string; addLabel: string; emptyLabel: string }[] = [ { key: 'basic', label: 'بیمه پایه', addLabel: 'افزودن بیمه پایه', emptyLabel: 'هنوز بیمه‌ی پایه‌ای اضافه نکرده‌اید.' }, { key: 'supplementary', label: 'بیمه تکمیلی', addLabel: 'افزودن بیمه تکمیلی', emptyLabel: 'هنوز بیمه‌ی تکمیلی‌ای اضافه نکرده‌اید.' }, ]; /** Contract's effective kind, falling back to 'basic' for legacy rows with no kind/type. */ const contractKind = (c: Contract): Kind => (c.insurance_kind === 'supplementary' ? 'supplementary' : 'basic'); /** Case-insensitive filter over insurance name (and code) — the "جستجو در بیمه ها..." box. */ export function filterInsurances(list: Contract[], query: string): Contract[] { const q = query.trim().toLowerCase(); if (!q) return list; return list.filter((c) => (c.insurance_name ?? '').toLowerCase().includes(q) || String(c.insurance_id).includes(q), ); } /** One-line readable summary shown on the collapsed row: پوشش ۹۰٪ · فرانشیز … · سقف پوشش … */ export function contractSummary(c: Contract): string { const parts = [`پوشش ${formatNumber(c.coverage_percent)}٪`]; if (c.franchise_rials > 0) parts.push(`فرانشیز ${formatRial(c.franchise_rials)}`); parts.push(c.annual_ceiling_rials != null ? `سقف پوشش ${formatRial(c.annual_ceiling_rials)}` : 'سقف پوشش نامحدود'); return parts.join(' · '); } export default function TenantInsuranceContracts() { const qc = useQueryClient(); const { dbUuid, context, availableContexts } = useAuthStore(); // مجوزهای منشی؛ برای owner/پزشک همیشه true (usePermissions بدون context آزاد است). const { can } = usePermissions(); const canCreate = can('insurances', 'create'); const canUpdate = can('insurances', 'update'); const [tab, setTab] = useState('basic'); const [modalOpen, setModalOpen] = useState(false); const [editContract, setEditContract] = useState(null); const [search, setSearch] = useState(''); const [expanded, setExpanded] = useState(null); const [pickedDoctorUuid, setPickedDoctorUuid] = useState(null); // A user who is both a doctor and a clinic owner may have a doctor db_uuid active; // fall back to the clinic context so the roster query targets the clinic. Same // resolution as ClinicAppointmentSettingsPage. const clinicUuid = useMemo(() => { if (context?.type === 'clinic') return dbUuid; return availableContexts.find((c) => c.type === 'clinic')?.db_uuid ?? null; }, [context, dbUuid, availableContexts]); const doctorsQuery = useQuery({ queryKey: ['clinic-doctors', clinicUuid], queryFn: () => api.get>(`/api/v1/clinic/doctor-list/${clinicUuid}`), enabled: !!clinicUuid, }); const doctorList: ClinicDoctorItem[] = useMemo(() => { const raw = doctorsQuery.data?.data; return (raw as any)?.data ?? raw ?? []; }, [doctorsQuery.data]); // In a clinic, insurance is per-doctor: default to the first doctor. Solo doctors / // personal offices have no clinic context → doctorUuid stays null → backend keeps the // legacy tenant-scoped behavior. const isClinic = !!clinicUuid; const doctorUuid = useMemo( () => (isClinic ? pickedDoctorUuid ?? doctorList[0]?.uuid ?? null : null), [isClinic, pickedDoctorUuid, doctorList], ); const showDoctorPicker = isClinic && doctorList.length > 1; const dq = doctorUuid ? `?doctor_uuid=${encodeURIComponent(doctorUuid)}` : ''; const contractsQuery = useQuery({ queryKey: ['tenant-insurances', doctorUuid], queryFn: () => api.get(`/api/v1/billing/tenant-insurances${dq}`), }); const pricingQuery = useQuery({ queryKey: ['insurance-pricing', doctorUuid], queryFn: () => api.get(`/api/v1/insurance-pricing${dq}`), }); const contracts: Contract[] = (contractsQuery.data as any)?.data?.data ?? []; const allInsurances: InsuranceOption[] = (pricingQuery.data as any)?.data?.insurances ?? []; const activeIds = new Set(contracts.map((c) => c.insurance_id)); // Add-mode options: only the active tab's kind, excluding already-contracted insurances. const available = allInsurances.filter((i) => i.type === tab).filter((i) => !activeIds.has(i.insurance_id)); // The insurance's real type comes from the catalog, not the contract's stored kind // (legacy contracts carry a stale manually-picked kind). Catalog type is authoritative. const catalogTypeById = useMemo(() => { const m: Record = {}; for (const i of allInsurances) m[i.insurance_id] = i.type === 'supplementary' ? 'supplementary' : 'basic'; return m; }, [allInsurances]); const kindOf = (c: Contract): Kind => catalogTypeById[c.insurance_id] ?? contractKind(c); const byKind = useMemo(() => contracts.filter((c) => kindOf(c) === tab), [contracts, tab, catalogTypeById]); const rows = useMemo(() => filterInsurances(byKind, search), [byKind, search]); const counts = useMemo(() => ({ basic: contracts.filter((c) => kindOf(c) === 'basic').length, supplementary: contracts.filter((c) => kindOf(c) === 'supplementary').length, }), [contracts, catalogTypeById]); const invalidate = () => qc.invalidateQueries({ queryKey: ['tenant-insurances', doctorUuid] }); const toggleRow = (uuid: string) => setExpanded((p) => (p === uuid ? null : uuid)); const saveMut = useMutation({ mutationFn: (payload: ReturnType) => editContract ? api.patch(`/api/v1/billing/tenant-insurances/${editContract.uuid}`, payload) : api.post('/api/v1/billing/tenant-insurances', payload), onSuccess: () => { toast.success(editContract ? 'قرارداد بیمه ویرایش شد' : 'قرارداد بیمه فعال شد'); closeModal(); invalidate(); }, onError: (e: Error) => toast.error(e.message), }); const toggleMut = useMutation({ mutationFn: (c: Contract) => api.patch(`/api/v1/billing/tenant-insurances/${c.uuid}`, { is_active: !c.is_active, ...(doctorUuid ? { doctor_uuid: doctorUuid } : {}), }), onSuccess: () => invalidate(), onError: (e: Error) => toast.error(e.message), }); const openAdd = () => { setEditContract(null); setModalOpen(true); }; const openEdit = (c: Contract) => { setEditContract(c); setModalOpen(true); }; const closeModal = () => { setModalOpen(false); setEditContract(null); }; const activeKind = KINDS.find((k) => k.key === tab)!; return (

مدیریت بیمه

{canCreate && ( )}
{showDoctorPicker && (
({ value: d.uuid, label: d.name }))} value={doctorUuid ?? ''} onChange={(v) => { setPickedDoctorUuid(v ? String(v) : null); setExpanded(null); }} placeholder="انتخاب پزشک..." /> تنظیمات بیمه برای هر پزشک جداگانه ذخیره می‌شود.
)}
{KINDS.map((k) => { const active = k.key === tab; return ( ); })}
setSearch(e.target.value)} />
{contractsQuery.isLoading ? (
در حال بارگذاری...
) : rows.length === 0 ? (
{search ? 'بیمه‌ای یافت نشد.' : activeKind.emptyLabel}
) : (
{rows.map((c) => ( toggleRow(c.uuid)} onEdit={() => openEdit(c)} onToggleStatus={() => toggleMut.mutate(c)} statusPending={toggleMut.isPending} canUpdate={canUpdate} /> ))}
)} saveMut.mutate(payload)} isPending={saveMut.isPending} />
); } interface RowProps { contract: Contract; open: boolean; onToggleRow: () => void; onEdit: () => void; onToggleStatus: () => void; statusPending?: boolean; canUpdate?: boolean; } function ContractCard({ contract: c, open, onToggleRow, onEdit, onToggleStatus, statusPending, canUpdate }: RowProps) { const stop = (fn: () => void) => (e: React.MouseEvent) => { e.stopPropagation(); fn(); }; return (
{c.insurance_name ?? `#${c.insurance_id}`}
{canUpdate && ( )}
{contractSummary(c)}
{canUpdate && (
{})}>
)} {open &&
}
); } /** Expanded full detail of a contract (all fields), shown when its card is open. */ function ContractDetails({ contract: c }: { contract: Contract }) { return (
0 ? formatRial(c.franchise_rials) : '—'} /> {formatNumber(c.version)}} /> {c.insurance_id}} /> {c.is_active ? 'فعال' : 'غیرفعال'} } />
); } function DetailCell({ label, value }: { label: string; value: ReactNode }) { return (
{label} {value}
); } function StatusToggle({ contract, onToggle, disabled }: { contract: Contract; onToggle: (e: React.MouseEvent) => void; disabled?: boolean }) { return ( ); }