feat: integrate insurance coverage management for clinic services
- Updated NewSessionPage to calculate patient share based on insurance coverage rules. - Refactored billing calculations to utilize new patientShareOf function for service items. - Enhanced API documentation to reflect changes in service coverage structure. - Implemented ServiceInsuranceModal for managing insurance coverage per service. - Added UI components for displaying and editing insurance coverage details. - Removed obsolete toggle switch styles and adjusted CSS for new components. - Ensured backend endpoints support both service_item_id and service_item_uuid for flexibility.
This commit is contained in:
@@ -23,16 +23,34 @@ const schema = z.object({
|
||||
});
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
interface Contract { insurance_id: number; insurance_name: string | null; insurance_kind: string | null; coverage_percent: number }
|
||||
interface Contract { uuid: string; insurance_id: number; insurance_name: string | null; insurance_kind: string | null; coverage_percent: number; franchise_rials: number; annual_ceiling_rials: number | null }
|
||||
interface CoverageRow { service_item_uuid: string | null; covered: boolean; coverage_percent: number | null; franchise_rials: number | null; ceiling_rials: number | null }
|
||||
|
||||
const PAYMENT_LABELS: Record<string, string> = {
|
||||
cash: 'نقدی', card: 'کارت', insurance: 'بیمه', online: 'آنلاین', pending: 'در انتظار',
|
||||
};
|
||||
|
||||
function calcFinal(visit: number, base: number, supp: number, services: number) {
|
||||
const afterBase = visit * (1 - base / 100);
|
||||
const afterSupp = afterBase * (1 - supp / 100);
|
||||
return Math.round(afterSupp) + services;
|
||||
interface Rule { covered: boolean; percent: number; franchise: number; ceiling: number | null }
|
||||
|
||||
// (calcFinal قدیمی حذف شد — حالا سهم بیمار خدمات با patientShareOf و قاعدهی هر بیمهگر محاسبه میشود)
|
||||
|
||||
// آینهی BillingCalculator سمت سرور: سهم بیمار یک خدمت با پوشش پایه/مکمل.
|
||||
function patientShareOf(total: number, base: Rule | null, supp: Rule | null): number {
|
||||
let baseShare = 0;
|
||||
let remaining = total;
|
||||
if (base && base.covered) {
|
||||
baseShare = Math.round(total * (base.percent / 100));
|
||||
if (base.ceiling !== null) baseShare = Math.min(baseShare, base.ceiling);
|
||||
remaining = total - baseShare;
|
||||
}
|
||||
let suppShare = 0;
|
||||
if (supp && supp.covered) {
|
||||
suppShare = Math.round(remaining * (supp.percent / 100));
|
||||
if (supp.ceiling !== null) suppShare = Math.min(suppShare, supp.ceiling);
|
||||
remaining = remaining - suppShare;
|
||||
}
|
||||
const franchise = (base?.franchise ?? 0) + (supp?.franchise ?? 0);
|
||||
return Math.min(remaining + franchise, total);
|
||||
}
|
||||
|
||||
const sectionTitle: React.CSSProperties = { fontWeight: 700, fontSize: 13.5, color: 'var(--text-2)', margin: '0 0 12px' };
|
||||
@@ -91,6 +109,35 @@ export default function NewSessionPage() {
|
||||
const sectionOptions = (sectionsData?.data ?? []).map(s => ({ value: s.uuid, label: s.name }));
|
||||
const itemOptions = (itemsData?.data ?? []).filter(i => i.active).map(i => ({ value: i.uuid, label: `${i.name} — ${formatRial(i.price_rials)}` }));
|
||||
|
||||
const baseContract = contracts.find(c => String(c.insurance_id) === baseId) ?? null;
|
||||
const suppContract = contracts.find(c => String(c.insurance_id) === suppId) ?? null;
|
||||
|
||||
const baseCoverageQ = useQuery<{ data: { data: CoverageRow[] } }>({
|
||||
queryKey: ['service-coverage', baseContract?.uuid],
|
||||
queryFn: () => api.get(`/api/v1/billing/tenant-insurances/${baseContract!.uuid}/service-coverage`),
|
||||
enabled: !!baseContract,
|
||||
});
|
||||
const suppCoverageQ = useQuery<{ data: { data: CoverageRow[] } }>({
|
||||
queryKey: ['service-coverage', suppContract?.uuid],
|
||||
queryFn: () => api.get(`/api/v1/billing/tenant-insurances/${suppContract!.uuid}/service-coverage`),
|
||||
enabled: !!suppContract,
|
||||
});
|
||||
const baseCoverage = (baseCoverageQ.data as any)?.data?.data as CoverageRow[] | undefined ?? [];
|
||||
const suppCoverage = (suppCoverageQ.data as any)?.data?.data as CoverageRow[] | undefined ?? [];
|
||||
|
||||
// قاعدهی پوشش یک خدمت تحت یک قرارداد: override خدمت اگر باشد، وگرنه پیشفرض قرارداد.
|
||||
const ruleFor = (contract: Contract | null, coverage: CoverageRow[], serviceUuid: string): Rule | null => {
|
||||
if (!contract) return null;
|
||||
const ov = coverage.find(r => r.service_item_uuid === serviceUuid);
|
||||
if (ov && !ov.covered) return { covered: false, percent: 0, franchise: 0, ceiling: null };
|
||||
return {
|
||||
covered: true,
|
||||
percent: ov?.coverage_percent ?? contract.coverage_percent,
|
||||
franchise: ov?.franchise_rials ?? contract.franchise_rials,
|
||||
ceiling: ov?.ceiling_rials ?? contract.annual_ceiling_rials,
|
||||
};
|
||||
};
|
||||
|
||||
const coverageOf = (id: string): number => contracts.find(c => String(c.insurance_id) === id)?.coverage_percent ?? 0;
|
||||
const applyBase = (id: string) => {
|
||||
setBaseId(id);
|
||||
@@ -118,9 +165,21 @@ export default function NewSessionPage() {
|
||||
const base = Number(form.watch('base_insurance_discount_percent')) || 0;
|
||||
const supp = Number(form.watch('supplementary_discount_percent')) || 0;
|
||||
const servicesTotal = useMemo(() => selectedServices.reduce((s, x) => s + x.price * x.qty, 0), [selectedServices]);
|
||||
const servicesPatient = useMemo(
|
||||
() => selectedServices.reduce((sum, x) => {
|
||||
const total = x.price * x.qty;
|
||||
return sum + patientShareOf(
|
||||
total,
|
||||
ruleFor(baseContract, baseCoverage, x.uuid),
|
||||
ruleFor(suppContract, suppCoverage, x.uuid),
|
||||
);
|
||||
}, 0),
|
||||
[selectedServices, baseContract, suppContract, baseCoverage, suppCoverage],
|
||||
);
|
||||
const servicesInsured = servicesTotal - servicesPatient;
|
||||
const afterBase = Math.round(visit * (1 - base / 100));
|
||||
const afterSupp = Math.round(afterBase * (1 - supp / 100));
|
||||
const finalPrice = calcFinal(visit, base, supp, servicesTotal);
|
||||
const finalPrice = Math.round(afterSupp) + servicesPatient;
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (body: object) => api.post(`/api/v1/patient/${recordUuid}/session`, body),
|
||||
@@ -243,6 +302,8 @@ export default function NewSessionPage() {
|
||||
{base > 0 && summaryRow('پس از بیمه پایه', afterBase)}
|
||||
{supp > 0 && summaryRow('پس از بیمه تکمیلی', afterSupp)}
|
||||
{servicesTotal > 0 && summaryRow('جمع خدمات', servicesTotal)}
|
||||
{servicesInsured > 0 && summaryRow('سهم بیمه از خدمات', servicesInsured)}
|
||||
{servicesInsured > 0 && summaryRow('سهم بیمار از خدمات', servicesTotal - servicesInsured)}
|
||||
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 8, marginTop: 2 }}>
|
||||
{summaryRow('مبلغ نهایی (سهم بیمار)', finalPrice, true)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user