feat(migrations): update franchise to percentage in tenant_insurances and tenant_service_coverage

- Changed franchise_rials to franchise_percent in tenant_insurances and tenant_service_coverage tables.
- Reset old rial values to 0/NULL as they are not convertible to percentage.

feat(command): add SeedInsuranceScenarioCommand for seeding insurance data

- Implemented a command to seed supplementary insurance contracts, patients, and claims for a specified doctor.
- Includes functionality for purging existing scenario data and generating new entries with predefined contracts and patient scenarios.
This commit is contained in:
hamed
2026-07-29 13:28:59 +03:30
parent 11b4dcdd34
commit 4f4bce9fe2
31 changed files with 1497 additions and 137 deletions
@@ -70,7 +70,7 @@ describe('AppointmentEditPage (ویرایش نوبت)', () => {
const CONTRACT = {
insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic', is_active: true,
coverage_percent: 70, franchise_rials: 0, annual_ceiling_rials: null,
coverage_percent: 70, franchise_percent: 0, annual_ceiling_rials: null,
category_coverages: { outpatient: 70, inpatient: 30 },
};
+43 -3
View File
@@ -32,6 +32,15 @@ const PAYMENT_OPTIONS = [
{ value: 'unpaid', label: 'وصول‌نشده' },
];
/** واژگان مطالبه `base` است، نه `basic` که در قرارداد بیمه به کار می‌رود. */
const KIND_OPTIONS = [
{ value: '', label: 'همه انواع' },
{ value: 'base', label: 'پایه' },
{ value: 'supplementary', label: 'تکمیلی' },
];
const KIND_LABEL: Record<string, string> = { base: 'پایه', supplementary: 'تکمیلی' };
const isoNDaysAgo = (days: number): string => {
const d = new Date();
d.setDate(d.getDate() - days);
@@ -69,6 +78,7 @@ export default function ClaimsPage() {
const search = params.get('search') ?? '';
const status = params.get('status') ?? '';
const insuranceId = params.get('insurance_id') ?? '';
const kind = params.get('kind') ?? '';
const paymentStatus = params.get('payment_status') ?? '';
const from = params.get('from') ?? '';
const to = params.get('to') ?? '';
@@ -76,7 +86,7 @@ export default function ClaimsPage() {
const sort = params.get('sort') ?? 'last_activity_at';
const dir = (params.get('dir') ?? 'desc') as 'asc' | 'desc';
const hasFilters = !!(search || status || insuranceId || paymentStatus || from || to);
const hasFilters = !!(search || status || insuranceId || kind || paymentStatus || from || to);
/** تغییر فیلتر همیشه به صفحه‌ی اول برمی‌گردد؛ ماندن روی صفحه ۵ با نتیجه‌ی جدید بی‌معناست. */
const setParam = (patch: Record<string, string>) => {
@@ -95,13 +105,14 @@ export default function ClaimsPage() {
if (search) qs.set('search', search);
if (status) qs.set('status', status);
if (insuranceId) qs.set('insurance_id', insuranceId);
if (kind) qs.set('kind', kind);
if (paymentStatus) qs.set('payment_status', paymentStatus);
const fromUnix = toUnix(from);
const toUnixVal = endOfDayUnix(to);
if (fromUnix) qs.set('from', String(fromUnix));
if (toUnixVal) qs.set('to', String(toUnixVal));
return qs.toString();
}, [page, sort, dir, search, status, insuranceId, paymentStatus, from, to]);
}, [page, sort, dir, search, status, insuranceId, kind, paymentStatus, from, to]);
const listQuery = useQuery<PaginatedResponse<ClaimPatientRow>>({
queryKey: ['claims-by-patient', queryString],
@@ -142,6 +153,25 @@ export default function ClaimsPage() {
{ key: 'full_name', header: 'بیمار', sortable: true, render: (r) => r.full_name ?? '—' },
{ key: 'mobile', header: 'موبایل', render: (r) => r.mobile ?? '—' },
{ key: 'national_code', header: 'کد ملی', render: (r) => r.national_code ?? '—' },
{
key: 'insurances',
header: 'بیمه',
render: (r) => (
(r.insurances ?? []).length === 0 ? '—' : (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{r.insurances.map((i) => (
<span
key={`${i.insurance_id}-${i.kind}`}
className={`badge ${i.kind === 'supplementary' ? 'violet' : 'blue'}`}
title={KIND_LABEL[i.kind ?? ''] ?? i.kind ?? ''}
>
{i.insurance_name ?? `بیمه #${i.insurance_id}`}
</span>
))}
</div>
)
),
},
{ key: 'claims_count', header: 'تعداد درخواست', sortable: true, render: (r) => formatNumber(r.claims_count) },
{ key: 'total_services_rials', header: 'مجموع خدمات', sortable: true, render: (r) => formatRial(r.total_services_rials) },
{ key: 'total_insurance_rials', header: 'سهم بیمه', sortable: true, render: (r) => formatRial(r.total_insurance_rials) },
@@ -176,6 +206,16 @@ export default function ClaimsPage() {
height={38}
/>
</div>
<div style={{ minWidth: 130 }}>
<label className="field-label">نوع بیمه</label>
<SearchableSelect
options={KIND_OPTIONS}
value={kind}
onChange={(v) => setParam({ kind: v ? String(v) : '' })}
placeholder="همه انواع"
height={38}
/>
</div>
<div style={{ minWidth: 150 }}>
<label className="field-label">وضعیت</label>
<SearchableSelect
@@ -225,7 +265,7 @@ export default function ClaimsPage() {
loading={listQuery.isLoading}
searchValue={search}
onSearchChange={(v) => setParam({ search: v })}
searchPlaceholder="نام، موبایل یا کد ملی بیمار"
searchPlaceholder="نام، موبایل، کد ملی بیمار یا نام بیمه"
emptyMessage="پرونده بیمه‌ای ثبت نشده است."
sortKey={sort}
sortDir={dir}
+2 -2
View File
@@ -43,7 +43,7 @@ interface CoverageRow {
service_item_uuid: string | null;
covered: boolean;
coverage_percent: number | null;
franchise_rials: number | null;
franchise_percent: number | null;
ceiling_rials: number | null;
}
@@ -278,7 +278,7 @@ function ContractCoverageRow({ contract, itemUuid }: { contract: TenantInsurance
) : (
<span style={{ fontSize: 12.5, color: 'var(--text-2)', display: 'inline-flex', gap: 10, whiteSpace: 'nowrap' }}>
<span>پوشش {formatNumber(percent)}٪</span>
{row?.franchise_rials ? <span>فرانشیز {formatRial(row.franchise_rials)}</span> : null}
{row?.franchise_percent ? <span>فرانشیز {formatNumber(row.franchise_percent)}٪</span> : null}
{row?.ceiling_rials ? <span>سقف {formatRial(row.ceiling_rials)}</span> : null}
{!row && <span className="muted">(ارث از قرارداد)</span>}
</span>