208 lines
9.2 KiB
TypeScript
208 lines
9.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { TrashIcon, PlusIcon, PencilIcon } from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import { formatRial } from '../lib/utils';
|
|
|
|
interface Contract {
|
|
uuid: string;
|
|
insurance_id: number;
|
|
insurance_name: string | null;
|
|
insurance_kind: string | null;
|
|
version: number;
|
|
coverage_percent: number;
|
|
franchise_rials: number;
|
|
annual_ceiling_rials: number | null;
|
|
}
|
|
|
|
interface InsuranceOption {
|
|
insurance_id: number;
|
|
insurance_name: string;
|
|
type: string;
|
|
}
|
|
|
|
const KIND_LABEL: Record<string, string> = {
|
|
basic: 'پایه',
|
|
supplementary: 'تکمیلی',
|
|
};
|
|
|
|
export default function TenantInsuranceContracts() {
|
|
const qc = useQueryClient();
|
|
const [addOpen, setAddOpen] = useState(false);
|
|
const [editUuid, setEditUuid] = useState<string | null>(null);
|
|
const [insuranceId, setInsuranceId] = useState('');
|
|
const [coverage, setCoverage] = useState('');
|
|
const [franchise, setFranchise] = useState('');
|
|
const [ceiling, setCeiling] = useState('');
|
|
|
|
const contractsQuery = useQuery<{ data: Contract[] }>({
|
|
queryKey: ['tenant-insurances'],
|
|
queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
|
|
});
|
|
|
|
const pricingQuery = useQuery<{ data: { insurances: InsuranceOption[] } }>({
|
|
queryKey: ['insurance-pricing'],
|
|
queryFn: () => api.get('/api/v1/insurance-pricing'),
|
|
});
|
|
|
|
const contracts = (contractsQuery.data as any)?.data?.data ?? [];
|
|
const allInsurances: InsuranceOption[] = (pricingQuery.data as any)?.data?.insurances ?? [];
|
|
const activeIds = new Set(contracts.map((c: Contract) => c.insurance_id));
|
|
const available = allInsurances.filter((i) => !activeIds.has(i.insurance_id));
|
|
|
|
const resetForm = () => {
|
|
setInsuranceId('');
|
|
setCoverage('');
|
|
setFranchise('');
|
|
setCeiling('');
|
|
setAddOpen(false);
|
|
setEditUuid(null);
|
|
};
|
|
|
|
const openEdit = (c: Contract) => {
|
|
setEditUuid(c.uuid);
|
|
setAddOpen(false);
|
|
setInsuranceId(String(c.insurance_id));
|
|
setCoverage(String(c.coverage_percent ?? ''));
|
|
setFranchise(String(c.franchise_rials ?? ''));
|
|
setCeiling(c.annual_ceiling_rials != null ? String(c.annual_ceiling_rials) : '');
|
|
};
|
|
|
|
const editMut = useMutation({
|
|
mutationFn: () =>
|
|
api.patch(`/api/v1/billing/tenant-insurances/${editUuid}`, {
|
|
coverage_percent: Number(coverage) || 0,
|
|
franchise_rials: Number(franchise) || 0,
|
|
annual_ceiling_rials: ceiling === '' ? null : Number(ceiling),
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success('قرارداد بیمه ویرایش شد');
|
|
resetForm();
|
|
qc.invalidateQueries({ queryKey: ['tenant-insurances'] });
|
|
},
|
|
onError: (e: Error) => toast.error(e.message),
|
|
});
|
|
|
|
const addMut = useMutation({
|
|
mutationFn: () =>
|
|
api.post('/api/v1/billing/tenant-insurances', {
|
|
insurance_id: Number(insuranceId),
|
|
coverage_percent: Number(coverage) || 0,
|
|
franchise_rials: Number(franchise) || 0,
|
|
annual_ceiling_rials: ceiling === '' ? null : Number(ceiling),
|
|
}),
|
|
onSuccess: () => {
|
|
toast.success('قرارداد بیمه فعال شد');
|
|
resetForm();
|
|
qc.invalidateQueries({ queryKey: ['tenant-insurances'] });
|
|
},
|
|
onError: (e: Error) => toast.error(e.message),
|
|
});
|
|
|
|
const delMut = useMutation({
|
|
mutationFn: (uuid: string) => api.delete(`/api/v1/billing/tenant-insurances/${uuid}`),
|
|
onSuccess: () => {
|
|
toast.success('قرارداد غیرفعال شد');
|
|
qc.invalidateQueries({ queryKey: ['tenant-insurances'] });
|
|
},
|
|
onError: (e: Error) => toast.error(e.message),
|
|
});
|
|
|
|
return (
|
|
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14 }}>
|
|
<div>
|
|
<h2 style={{ fontSize: 14, fontWeight: 700, margin: 0 }}>قراردادهای بیمه</h2>
|
|
<p style={{ fontSize: 12, color: 'var(--text-3)', marginTop: 4, lineHeight: 1.7 }}>
|
|
بیمههایی که با آنها قرارداد دارید. درصد پوشش، فرانشیز و سقف تعهد هر بیمه را تعیین کنید.
|
|
</p>
|
|
</div>
|
|
{!addOpen && available.length > 0 && (
|
|
<button className="btn primary sm" onClick={() => setAddOpen(true)}>
|
|
<PlusIcon style={{ width: 14 }} /> افزودن بیمه
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{(addOpen || editUuid) && (
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'flex-end', padding: 14, borderRadius: 10, border: '1px solid var(--border)', background: 'var(--surface)', marginBottom: 14 }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
<label style={{ fontSize: 11.5, fontWeight: 600 }}>بیمه</label>
|
|
<select className="input" style={{ minWidth: 160 }} value={insuranceId} disabled={!!editUuid} onChange={(e) => setInsuranceId(e.target.value)}>
|
|
<option value="">انتخاب...</option>
|
|
{(editUuid ? allInsurances : available).map((i) => (
|
|
<option key={i.insurance_id} value={i.insurance_id}>{i.insurance_name} ({KIND_LABEL[i.type] ?? i.type})</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
<label style={{ fontSize: 11.5, fontWeight: 600 }}>درصد پوشش</label>
|
|
<input type="number" min={0} max={100} dir="ltr" className="input" style={{ width: 100 }} value={coverage} onChange={(e) => setCoverage(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: 130 }} value={franchise} onChange={(e) => setFranchise(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: 130 }} placeholder="بینهایت" value={ceiling} onChange={(e) => setCeiling(e.target.value)} />
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
<button
|
|
className="btn primary sm"
|
|
disabled={!insuranceId || addMut.isPending || editMut.isPending}
|
|
onClick={() => (editUuid ? editMut.mutate() : addMut.mutate())}
|
|
>
|
|
{addMut.isPending || editMut.isPending ? '...' : 'ذخیره'}
|
|
</button>
|
|
<button className="btn ghost sm" onClick={resetForm}>لغو</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{contractsQuery.isLoading ? (
|
|
<div className="muted" style={{ fontSize: 13 }}>در حال بارگذاری...</div>
|
|
) : contracts.length === 0 ? (
|
|
<div style={{ fontSize: 12.5, color: 'var(--text-3)', padding: '12px 0' }}>
|
|
هنوز با هیچ بیمهای قرارداد فعال ندارید.
|
|
</div>
|
|
) : (
|
|
<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={{ 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>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|