Files
clinicpro/assets/admin/components/ServiceTariffModal.tsx
T
hamedandClaude Opus 4.8 89191eee57 feat: insurance & medical billing system (6 phases)
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>
2026-06-23 15:05:24 +03:30

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>
);
}