refactor(insurance): merge pricing into contracts (basic+supp groups)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,161 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../lib/api';
|
||||
import { formatRial } from '../lib/utils';
|
||||
|
||||
interface InsuranceRow {
|
||||
insurance_id: number;
|
||||
insurance_name: string;
|
||||
type: string;
|
||||
patient_share_rials: number | null;
|
||||
}
|
||||
|
||||
interface PricingResponse {
|
||||
free_visit_price_rials: number;
|
||||
insurances: InsuranceRow[];
|
||||
}
|
||||
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
basic: 'بیمه پایه',
|
||||
supplementary: 'بیمه تکمیلی',
|
||||
};
|
||||
|
||||
export default function InsurancePricingSection() {
|
||||
const qc = useQueryClient();
|
||||
const [freeVisit, setFreeVisit] = useState('');
|
||||
const [shares, setShares] = useState<Record<number, string>>({});
|
||||
|
||||
const { data, isLoading } = useQuery<{ data: PricingResponse }>({
|
||||
queryKey: ['insurance-pricing'],
|
||||
queryFn: () => api.get('/api/v1/insurance-pricing'),
|
||||
});
|
||||
|
||||
const pricing = (data as any)?.data as PricingResponse | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!pricing) return;
|
||||
setFreeVisit(String(pricing.free_visit_price_rials ?? 0));
|
||||
const next: Record<number, string> = {};
|
||||
pricing.insurances.forEach((i) => {
|
||||
next[i.insurance_id] = i.patient_share_rials != null ? String(i.patient_share_rials) : '';
|
||||
});
|
||||
setShares(next);
|
||||
}, [pricing]);
|
||||
|
||||
const saveMut = useMutation({
|
||||
mutationFn: () =>
|
||||
api.put('/api/v1/insurance-pricing', {
|
||||
free_visit_price_rials: Number(freeVisit) || 0,
|
||||
insurances: (pricing?.insurances ?? []).filter((i) => i.type === 'basic').map((i) => ({
|
||||
insurance_id: i.insurance_id,
|
||||
patient_share_rials:
|
||||
shares[i.insurance_id] === '' || shares[i.insurance_id] == null
|
||||
? null
|
||||
: Number(shares[i.insurance_id]) || 0,
|
||||
})),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success('قیمتگذاری بیمه ذخیره شد');
|
||||
qc.invalidateQueries({ queryKey: ['insurance-pricing'] });
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const basics = (pricing?.insurances ?? []).filter((i) => i.type === 'basic');
|
||||
|
||||
const renderRow = (i: InsuranceRow) => {
|
||||
const share = shares[i.insurance_id] ?? '';
|
||||
const preview =
|
||||
share !== '' && freeVisit !== ''
|
||||
? `سهم بیمار: ${formatRial(Number(share) || 0)}`
|
||||
: 'تعیین نشده';
|
||||
return (
|
||||
<div
|
||||
key={i.insurance_id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13.5 }}>{i.insurance_name}</div>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 2 }}>{preview}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
dir="ltr"
|
||||
className="input"
|
||||
style={{ width: 150 }}
|
||||
placeholder="سهم بیمار (ریال)"
|
||||
value={share}
|
||||
onChange={(e) =>
|
||||
setShares((p) => ({ ...p, [i.insurance_id]: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card" style={{ padding: 20 }}>
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<h2 style={{ fontSize: 14, fontWeight: 700, margin: 0 }}>قیمتگذاری ویزیت بر اساس بیمه</h2>
|
||||
<p style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4, lineHeight: 1.7 }}>
|
||||
مبلغ ویزیت آزاد و سهم بیمار به ازای هر بیمه را تعیین کنید. خالیگذاشتن یک بیمه یعنی پذیرفته نمیشود.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="muted" style={{ fontSize: 13 }}>در حال بارگذاری...</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
|
||||
<div className="field" style={{ flexDirection: 'column', alignItems: 'stretch', height: 'auto', gap: 6, padding: 0, border: 'none', background: 'none' }}>
|
||||
<label style={{ fontSize: 12.5, fontWeight: 600 }}>مبلغ ویزیت آزاد (ریال)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
dir="ltr"
|
||||
className="input"
|
||||
style={{ maxWidth: 220 }}
|
||||
value={freeVisit}
|
||||
onChange={(e) => setFreeVisit(e.target.value)}
|
||||
/>
|
||||
<span style={{ fontSize: 11.5, color: 'var(--text-3)' }}>
|
||||
{freeVisit !== '' ? formatRial(Number(freeVisit) || 0) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{basics.length > 0 && (
|
||||
<div>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 600, marginBottom: 8, color: 'var(--text-2)' }}>{TYPE_LABEL.basic}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>{basics.map(renderRow)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{basics.length === 0 && (
|
||||
<div style={{ fontSize: 12.5, color: 'var(--text-3)' }}>بیمه پایهای در سیستم تعریف نشده است.</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<button
|
||||
className="btn primary sm"
|
||||
disabled={saveMut.isPending}
|
||||
onClick={() => saveMut.mutate()}
|
||||
>
|
||||
{saveMut.isPending ? 'در حال ذخیره...' : 'ذخیره قیمتگذاری'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -168,28 +168,38 @@ export default function TenantInsuranceContracts() {
|
||||
هنوز با هیچ بیمهای قرارداد فعال ندارید.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{contracts.map((c: Contract) => (
|
||||
<div key={c.uuid} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', borderRadius: 10, border: '1px solid var(--border)' }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13.5 }}>
|
||||
{c.insurance_name ?? `#${c.insurance_id}`}
|
||||
{c.insurance_kind && <span className="badge gray" style={{ fontSize: 10, marginInlineStart: 6 }}>{KIND_LABEL[c.insurance_kind] ?? c.insurance_kind}</span>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
|
||||
{(['basic', 'supplementary'] as const).map((kind) => {
|
||||
const group = contracts.filter((c: Contract) => c.insurance_kind === kind);
|
||||
if (group.length === 0) return null;
|
||||
return (
|
||||
<div key={kind}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--text-2)', marginBottom: 8 }}>
|
||||
بیمه {KIND_LABEL[kind]}
|
||||
</div>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 2 }}>
|
||||
پوشش {c.coverage_percent}٪
|
||||
{c.franchise_rials > 0 && ` · فرانشیز ${formatRial(c.franchise_rials)}`}
|
||||
{c.annual_ceiling_rials != null && ` · سقف ${formatRial(c.annual_ceiling_rials)}`}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{group.map((c: Contract) => (
|
||||
<div key={c.uuid} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 12px', borderRadius: 10, border: '1px solid var(--border)' }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 13.5 }}>{c.insurance_name ?? `#${c.insurance_id}`}</div>
|
||||
<div style={{ fontSize: 11.5, color: 'var(--text-3)', marginTop: 2 }}>
|
||||
پوشش {c.coverage_percent}٪
|
||||
{c.franchise_rials > 0 && ` · فرانشیز ${formatRial(c.franchise_rials)}`}
|
||||
{c.annual_ceiling_rials != null && ` · سقف ${formatRial(c.annual_ceiling_rials)}`}
|
||||
</div>
|
||||
</div>
|
||||
<button className="mini-btn" title="ویرایش" onClick={() => openEdit(c)}>
|
||||
<PencilIcon style={{ width: 15 }} />
|
||||
</button>
|
||||
<button className="mini-btn danger" title="غیرفعالسازی" disabled={delMut.isPending} onClick={() => delMut.mutate(c.uuid)}>
|
||||
<TrashIcon style={{ width: 15 }} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button className="mini-btn" title="ویرایش" onClick={() => openEdit(c)}>
|
||||
<PencilIcon style={{ width: 15 }} />
|
||||
</button>
|
||||
<button className="mini-btn danger" title="غیرفعالسازی" disabled={delMut.isPending} onClick={() => delMut.mutate(c.uuid)}>
|
||||
<TrashIcon style={{ width: 15 }} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user