Multi-tenant insurance contracts, service coverage, versioned tariffs, invoice calculation, and insurance claims with debt reporting. - TenantInsurance: per-tenant insurance contracts (coverage/franchise/ceiling, versioning, soft-deactivate) + active guard - ServiceItem.insuranceCovered + TenantServiceCoverage per-service overrides - Tariff: versioned yearly tariffs with fallback to ServiceItem price - Billing domain: Money/ShareBreakdown VOs, BillingCalculator (unit-tested), Invoice/InvoiceItem aggregate, InvoiceService.createFromSession - Claim/ClaimItem with state machine (pending->submitted->approved/rejected->paid), ClaimService, insurance-debt report - ClaimSubmitterInterface + ManualClaimSubmitter (future insurance API ready) - Admin UI: insurance-pricing page, claims page, service tariff modal, service insurance toggle; routes + sidebar entries - Architecture doc + billing/insurance/clinic-services API docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
4.2 KiB
TypeScript
95 lines
4.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import { formatRial, formatNumber } from '../lib/utils';
|
|
import Modal from './ui/Modal';
|
|
import type { ServiceItem } from '../types';
|
|
|
|
interface TariffRow {
|
|
uuid: string;
|
|
year: number;
|
|
price_rials: number;
|
|
is_active: boolean;
|
|
}
|
|
|
|
interface TariffResponse {
|
|
current_year: number;
|
|
default_price_rials: number;
|
|
data: TariffRow[];
|
|
}
|
|
|
|
export default function ServiceTariffModal({ item, onClose }: { item: ServiceItem | null; onClose: () => void }) {
|
|
const qc = useQueryClient();
|
|
const [year, setYear] = useState('');
|
|
const [price, setPrice] = useState('');
|
|
|
|
const { data, isLoading } = useQuery<{ data: TariffResponse }>({
|
|
queryKey: ['service-tariffs', item?.uuid],
|
|
queryFn: () => api.get(`/api/v1/service-items/${item!.uuid}/tariffs`),
|
|
enabled: !!item,
|
|
});
|
|
|
|
const resp = (data as any)?.data as TariffResponse | undefined;
|
|
const tariffs = resp?.data ?? [];
|
|
const currentYear = resp?.current_year;
|
|
|
|
const saveMut = useMutation({
|
|
mutationFn: () =>
|
|
api.put(`/api/v1/service-items/${item!.uuid}/tariffs/${Number(year)}`, {
|
|
price_rials: Number(price) || 0,
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success('تعرفه ذخیره شد');
|
|
setYear('');
|
|
setPrice('');
|
|
qc.invalidateQueries({ queryKey: ['service-tariffs', item?.uuid] });
|
|
},
|
|
onError: (e: Error) => toast.error(e.message),
|
|
});
|
|
|
|
return (
|
|
<Modal open={!!item} onClose={onClose} title={`تعرفههای سالانه — ${item?.name ?? ''}`}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
{resp && (
|
|
<div style={{ fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.7 }}>
|
|
قیمت پیشفرض خدمت: <b>{formatRial(resp.default_price_rials)}</b>. اگر تعرفهی سالی ثبت نشود، همین قیمت اعمال میشود.
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'flex-end', padding: 12, borderRadius: 10, border: '1px solid var(--border)', background: 'var(--surface)' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
<label style={{ fontSize: 11.5, fontWeight: 600 }}>سال (شمسی)</label>
|
|
<input type="number" dir="ltr" className="input" style={{ width: 100 }} placeholder={currentYear ? String(currentYear) : '۱۴۰۴'} value={year} onChange={(e) => setYear(e.target.value)} />
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
<label style={{ fontSize: 11.5, fontWeight: 600 }}>تعرفه (ریال)</label>
|
|
<input type="number" min={0} dir="ltr" className="input" style={{ width: 150 }} value={price} onChange={(e) => setPrice(e.target.value)} />
|
|
</div>
|
|
<button className="btn primary sm" disabled={!year || saveMut.isPending} onClick={() => saveMut.mutate()}>
|
|
{saveMut.isPending ? '...' : 'ذخیره'}
|
|
</button>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="muted" style={{ fontSize: 13 }}>در حال بارگذاری...</div>
|
|
) : tariffs.length === 0 ? (
|
|
<div style={{ fontSize: 12.5, color: 'var(--text-3)' }}>هنوز تعرفهی سالانهای ثبت نشده است.</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
{tariffs.map((t) => (
|
|
<div key={t.uuid} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', borderRadius: 8, border: '1px solid var(--border)' }}>
|
|
<span style={{ fontWeight: 600, fontSize: 13 }}>
|
|
سال {formatNumber(t.year)}
|
|
{t.year === currentYear && <span className="badge green" style={{ fontSize: 10, marginInlineStart: 6 }}><span className="bdot" />جاری</span>}
|
|
</span>
|
|
<span style={{ color: 'var(--primary)', fontWeight: 600, fontSize: 13 }}>{formatRial(t.price_rials)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|