Files
clinicpro/assets/admin/components/ServiceInsuranceModal.tsx
T
hamedandClaude Opus 4.8 2337cfd90c fix(services): restore separate insurance modal + fix ⋮ menu position
- revert the previous merge of insurance coverage into the service edit
  modal (per user): 'پوشش بیمه' is again a ⋮ menu action opening its own
  ServiceInsuranceModal, kept within the services page
- delete the inline ServiceInsuranceSection; restore ServiceInsuranceModal
- fix ⋮ dropdown position: left:8 → insetInlineStart:8 so the menu anchors
  under the ⋮ button (which sits at inline-start/right in RTL) instead of
  the opposite side
- keep the --primary-subtle → --primary-soft token fix in the modal
- test: assert the ⋮ menu exposes ویرایش / تعرفه‌های سالانه / پوشش بیمه

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:36:17 +03:30

222 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ShieldCheckIcon, CheckIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
import Modal from './ui/Modal';
import PriceInput from './ui/PriceInput';
import type { ServiceItem } from '../types';
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;
}
interface Draft {
covered: boolean;
coverage_percent: number | null;
franchise_rials: number | null;
ceiling_rials: number | null;
}
const KIND = {
basic: { label: 'پایه', cls: 'blue' },
supplementary: { label: 'تکمیلی', cls: 'violet' },
} as const;
function ContractCard({ contract, item }: { contract: TenantInsurance; item: ServiceItem }) {
const qc = useQueryClient();
const { data, isLoading } = useQuery<{ data: { data: CoverageRow[] } }>({
queryKey: ['service-coverage', contract.uuid],
queryFn: () => api.get(`/api/v1/billing/tenant-insurances/${contract.uuid}/service-coverage`),
});
const rows = (data as any)?.data?.data as CoverageRow[] | undefined;
const existing = rows?.find((r) => r.service_item_uuid === item.uuid);
const [draft, setDraft] = useState<Draft>({ covered: true, coverage_percent: null, franchise_rials: null, ceiling_rials: null });
useEffect(() => {
setDraft(existing
? {
covered: existing.covered,
coverage_percent: existing.coverage_percent,
franchise_rials: existing.franchise_rials,
ceiling_rials: existing.ceiling_rials,
}
: { covered: true, coverage_percent: null, franchise_rials: null, ceiling_rials: null });
}, [existing]);
const saveMut = useMutation({
mutationFn: () =>
api.put(`/api/v1/billing/tenant-insurances/${contract.uuid}/service-coverage`, {
service_item_uuid: item.uuid,
covered: draft.covered,
coverage_percent: draft.coverage_percent,
franchise_rials: draft.franchise_rials,
ceiling_rials: draft.ceiling_rials,
}),
onSuccess: () => {
toast.success('پوشش بیمه ذخیره شد');
qc.invalidateQueries({ queryKey: ['service-coverage', contract.uuid] });
},
onError: (e: Error) => toast.error(e.message),
});
const kind = contract.insurance_kind ? KIND[contract.insurance_kind] : null;
return (
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--r)', overflow: 'hidden' }}>
{/* سربرگ کارت */}
<div style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px',
background: 'var(--surface-2)', borderBottom: draft.covered ? '1px solid var(--border)' : 'none',
}}>
<div style={{
width: 32, height: 32, borderRadius: 9, flexShrink: 0,
display: 'grid', placeItems: 'center', background: 'var(--primary-soft)',
}}>
<ShieldCheckIcon style={{ width: 18, color: 'var(--primary)' }} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<b style={{ fontSize: 13.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{contract.insurance_name ?? 'بیمه'}
</b>
{kind && <span className={`badge ${kind.cls}`} style={{ fontSize: 10 }}>{kind.label}</span>}
{existing && (
<span className="badge green" style={{ fontSize: 10 }}>
<CheckIcon style={{ width: 10 }} /> تنظیم‌شده
</span>
)}
</div>
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 2 }}>
پوشش پیش‌فرض قرارداد: {contract.coverage_percent}٪
</div>
</div>
<label className="switch">
<input
type="checkbox"
checked={draft.covered}
disabled={isLoading}
onChange={(e) => setDraft((d) => ({ ...d, covered: e.target.checked }))}
/>
<span className="switch-track"><span className="switch-thumb" /></span>
</label>
</div>
{/* بدنه — فقط وقتی پوشش فعال است */}
{draft.covered && (
<div style={{ padding: 14 }}>
{isLoading ? (
<div className="muted" style={{ fontSize: 12.5 }}>در حال بارگذاری...</div>
) : (
<>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
<div style={{ minWidth: 0 }}>
<label className="field-label">درصد پوشش</label>
<input
type="number" min={0} max={100} dir="ltr" className="input"
style={{ height: 40, textAlign: 'left' }}
value={draft.coverage_percent ?? ''}
placeholder="ارث"
onChange={(e) => setDraft((d) => ({ ...d, coverage_percent: e.target.value === '' ? null : Number(e.target.value) }))}
/>
</div>
<div style={{ minWidth: 0 }}>
<label className="field-label">فرانشیز</label>
<PriceInput
className="input"
style={{ height: 40 }}
value={draft.franchise_rials ?? 0}
onChange={(v) => setDraft((d) => ({ ...d, franchise_rials: v || null }))}
placeholder="۰"
min={0}
/>
</div>
<div style={{ minWidth: 0 }}>
<label className="field-label">سقف پوشش</label>
<PriceInput
className="input"
style={{ height: 40 }}
value={draft.ceiling_rials ?? 0}
onChange={(v) => setDraft((d) => ({ ...d, ceiling_rials: v || null }))}
placeholder="بدون سقف"
min={0}
/>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, marginTop: 12 }}>
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>مقدار خالی از قرارداد بیمه ارث می‌برد</span>
<button className="btn primary sm" disabled={saveMut.isPending} onClick={() => saveMut.mutate()}>
{saveMut.isPending ? '...' : 'ذخیره'}
</button>
</div>
</>
)}
</div>
)}
{/* وقتی پوشش غیرفعال است — یک خط ذخیره */}
{!draft.covered && !isLoading && (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, padding: '12px 14px' }}>
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>این خدمت تحت این بیمه پوشش ندارد</span>
<button className="btn sm" disabled={saveMut.isPending} onClick={() => saveMut.mutate()}>
{saveMut.isPending ? '...' : 'ذخیره'}
</button>
</div>
)}
</div>
);
}
export default function ServiceInsuranceModal({ item, onClose }: { item: ServiceItem | null; onClose: () => void }) {
const { data, isLoading } = useQuery<{ data: { data: TenantInsurance[] } }>({
queryKey: ['tenant-insurances'],
queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
enabled: !!item,
});
const contracts = ((data as any)?.data?.data as TenantInsurance[] | undefined) ?? [];
return (
<Modal open={!!item} onClose={onClose} title={`پوشش بیمه — ${item?.name ?? ''}`} size="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{
display: 'flex', gap: 8, padding: '11px 13px', borderRadius: 'var(--r-sm)',
background: 'var(--primary-soft)', fontSize: 12, color: 'var(--text-2)', lineHeight: 1.7,
}}>
<ShieldCheckIcon style={{ width: 16, flexShrink: 0, marginTop: 1, color: 'var(--primary)' }} />
<span>درصد پوشش، فرانشیز و سقف هر بیمه‌گر برای این خدمت تعیین می‌شود. این تنظیمات مبنای محاسبه‌ی سهم بیمار و ساخت مطالبات بیمه است.</span>
</div>
{isLoading ? (
<div className="muted" style={{ fontSize: 13, padding: '8px 0' }}>در حال بارگذاری...</div>
) : contracts.length === 0 ? (
<div style={{
border: '2px dashed var(--border)', borderRadius: 'var(--r)',
padding: '36px 16px', textAlign: 'center',
}}>
<ShieldCheckIcon style={{ width: 34, margin: '0 auto 10px', display: 'block', opacity: 0.35 }} />
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-2)' }}>قرارداد بیمه‌ی فعالی ندارید</div>
<div style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4 }}>ابتدا از بخش بیمه‌ها یک بیمه را فعال کنید.</div>
</div>
) : (
item && contracts.map((c) => <ContractCard key={c.uuid} contract={c} item={item} />)
)}
</div>
</Modal>
);
}