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:
@@ -22,7 +22,8 @@ export interface Contract {
|
||||
version: number;
|
||||
is_active: boolean;
|
||||
coverage_percent: number;
|
||||
franchise_rials: number;
|
||||
/** درصد، نه مبلغ — سهم اجباری بیمار از مبلغ تحت پوشش تکمیلی. */
|
||||
franchise_percent: number;
|
||||
annual_ceiling_rials: number | null;
|
||||
kind: string | null;
|
||||
effective_from: number;
|
||||
@@ -40,7 +41,7 @@ export interface InsuranceFormValues {
|
||||
effectiveTo: string; // Y-m-d
|
||||
/** درصد پوشش به ازای هر نوع خدمت — کلید = key همان category. */
|
||||
categoryPercents: Record<string, string>;
|
||||
franchise: string; // toman
|
||||
franchise: string; // percent
|
||||
ceiling: string; // toman
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ export function contractToForm(c: Contract): InsuranceFormValues {
|
||||
effectiveFrom: unixToIso(c.effective_from),
|
||||
effectiveTo: unixToIso(c.effective_to),
|
||||
categoryPercents: percentsToStrings(c.category_coverages),
|
||||
franchise: c.franchise_rials != null ? String(rialToToman(c.franchise_rials)) : '',
|
||||
franchise: c.franchise_percent != null ? String(c.franchise_percent) : '',
|
||||
ceiling: c.annual_ceiling_rials != null ? String(rialToToman(c.annual_ceiling_rials)) : '',
|
||||
};
|
||||
}
|
||||
@@ -73,6 +74,12 @@ function percentsToStrings(map?: Record<string, number>): Record<string, string>
|
||||
return Object.fromEntries(Object.entries(map ?? {}).map(([k, v]) => [k, String(v)]));
|
||||
}
|
||||
|
||||
/** درصد پوششِ قابل ثبت: عددی بین ۱ تا ۱۰۰ — صفر یعنی قرارداد آن نوع خدمت را پوشش نمیدهد. */
|
||||
export function isValidPercent(raw?: string): boolean {
|
||||
const n = Number(raw);
|
||||
return raw !== undefined && raw !== '' && Number.isFinite(n) && n > 0 && n <= 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the API payload from form values (toman → rials, Y-m-d → unix).
|
||||
* When `doctorUuid` is set, the contract is targeted at that doctor (multi-doctor
|
||||
@@ -80,7 +87,7 @@ function percentsToStrings(map?: Record<string, number>): Record<string, string>
|
||||
*
|
||||
* `category_coverages` is only sent when the user may override percentages — the
|
||||
* backend rejects it otherwise, and omitting it keeps the contract on the central
|
||||
* admin defaults. فرانشیز فقط در قرارداد تکمیلی معنا دارد.
|
||||
* admin defaults. فرانشیز درصد است و فقط در قرارداد تکمیلی معنا دارد.
|
||||
*/
|
||||
export function buildInsurancePayload(
|
||||
v: InsuranceFormValues,
|
||||
@@ -95,7 +102,7 @@ export function buildInsurancePayload(
|
||||
kind: v.kind || null,
|
||||
// ستون قدیمی قرارداد؛ آخرین سطح fallback است و با درصد سرپایی همگام میماند.
|
||||
coverage_percent: Number(v.categoryPercents.outpatient ?? percents[0]?.[1] ?? 0) || 0,
|
||||
franchise_rials: isBasic ? 0 : tomanToRial(Number(v.franchise) || 0),
|
||||
franchise_percent: isBasic ? 0 : Number(v.franchise) || 0,
|
||||
annual_ceiling_rials: v.ceiling === '' ? null : tomanToRial(Number(v.ceiling)),
|
||||
effective_from: isoToUnix(v.effectiveFrom),
|
||||
effective_to: isoToUnix(v.effectiveTo),
|
||||
@@ -111,7 +118,7 @@ interface Props {
|
||||
editContract: Contract | null;
|
||||
/** Insurance catalog options; in edit mode all are shown, in add mode only the available ones. */
|
||||
options: InsuranceOption[];
|
||||
/** انواع خدمت از سرور — یک ورودی درصد به ازای هر نوع رندر میشود. */
|
||||
/** نوع خدمتهای *فعالِ* همین tenant — یک ورودی درصد به ازای هر نوع رندر میشود. */
|
||||
categories: ServiceCategoryOption[];
|
||||
/** Insurance kind of the active tab ('basic'|'supplementary'); assigned to new contracts, not user-editable. */
|
||||
kind: string;
|
||||
@@ -155,8 +162,15 @@ export default function InsuranceModal({
|
||||
const setPercent = (key: string, raw: string) =>
|
||||
setForm((f) => ({ ...f, categoryPercents: { ...f.categoryPercents, [key]: digitsOnly(raw, 3) } }));
|
||||
|
||||
// بدون مجوز update درصدها اصلاً ارسال نمیشوند، پس اجبارشان هم بیمعناست.
|
||||
const missingPercents = canUpdate
|
||||
? categories.filter((c) => !isValidPercent(form.categoryPercents[c.key])).map((c) => c.key)
|
||||
: [];
|
||||
const franchiseInvalid = form.kind === 'supplementary' && form.franchise !== '' && Number(form.franchise) > 100;
|
||||
const canSubmit = !!form.insuranceId && missingPercents.length === 0 && !franchiseInvalid;
|
||||
|
||||
const submit = () => {
|
||||
if (!form.insuranceId) return;
|
||||
if (!canSubmit) return;
|
||||
onSubmit(buildInsurancePayload(form, doctorUuid, canUpdate));
|
||||
};
|
||||
|
||||
@@ -177,7 +191,7 @@ export default function InsuranceModal({
|
||||
<button
|
||||
type="button"
|
||||
className="btn primary"
|
||||
disabled={!form.insuranceId || isPending}
|
||||
disabled={!canSubmit || isPending}
|
||||
onClick={submit}
|
||||
>
|
||||
{isPending ? '...' : 'ثبت بیمه'}
|
||||
@@ -230,10 +244,12 @@ export default function InsuranceModal({
|
||||
value={form.categoryPercents[c.key] ?? ''}
|
||||
onChange={(e) => setPercent(c.key, e.target.value)}
|
||||
/>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>
|
||||
<span style={{ fontSize: 11, color: missingPercents.includes(c.key) ? 'var(--danger)' : 'var(--text-3)' }}>
|
||||
{!canUpdate
|
||||
? 'مقدار پیشفرض تنظیمات مرکزی'
|
||||
: sourceOf(c.key) === SOURCE_ADMIN_DEFAULT
|
||||
: missingPercents.includes(c.key)
|
||||
? 'درصد پوشش الزامی است (۱ تا ۱۰۰)'
|
||||
: sourceOf(c.key) === SOURCE_ADMIN_DEFAULT
|
||||
? 'پیشفرض ادمین'
|
||||
: ' '}
|
||||
</span>
|
||||
@@ -244,8 +260,19 @@ export default function InsuranceModal({
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isBasic ? '1fr' : '1fr 1fr', gap: 12 }}>
|
||||
{!isBasic && (
|
||||
<div style={field}>
|
||||
<label style={label}>فرانشیز (تومان)</label>
|
||||
<input type="text" inputMode="numeric" dir="ltr" className="input" value={form.franchise} onChange={(e) => set({ franchise: digitsOnly(e.target.value) })} />
|
||||
<label style={label}>فرانشیز (درصد)</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
dir="ltr"
|
||||
className="input"
|
||||
aria-label="فرانشیز درصد"
|
||||
value={form.franchise}
|
||||
onChange={(e) => set({ franchise: digitsOnly(e.target.value, 3) })}
|
||||
/>
|
||||
<span style={{ fontSize: 11, color: franchiseInvalid ? 'var(--danger)' : 'var(--text-3)' }}>
|
||||
{franchiseInvalid ? 'فرانشیز نمیتواند بیش از ۱۰۰ باشد' : 'سهم بیمار از مبلغ تحت پوشش'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={field}>
|
||||
|
||||
Reference in New Issue
Block a user