feat: implement per-doctor insurance settings in multi-doctor clinics
- Updated InsuranceModal to include doctorUuid in the payload for insurance contracts. - Enhanced TenantInsuranceContracts to allow selection of doctors and pass doctorUuid in API requests. - Modified InsuranceController to handle doctorUuid for tenant insurance endpoints, ensuring contracts are stored per doctor. - Updated API documentation to reflect the new optional doctor_uuid parameter for tenant insurance endpoints. - Added tests to verify the functionality of per-doctor insurance contracts and ensure isolation of contracts between doctors.
This commit is contained in:
@@ -3,7 +3,11 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { PlusIcon, PencilIcon, MagnifyingGlassIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '../lib/api';
|
||||
import type { ApiResponse } from '../lib/api';
|
||||
import { formatRial, formatNumber, formatDate } from '../lib/utils';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import SearchableSelect from './ui/SearchableSelect';
|
||||
import type { ClinicDoctorItem } from './ClinicDoctorsManager';
|
||||
import InsuranceModal, { Contract, InsuranceOption, KIND_LABEL, buildInsurancePayload } from './InsuranceModal';
|
||||
|
||||
type Kind = 'basic' | 'supplementary';
|
||||
@@ -37,20 +41,52 @@ export function contractSummary(c: Contract): string {
|
||||
|
||||
export default function TenantInsuranceContracts() {
|
||||
const qc = useQueryClient();
|
||||
const { dbUuid, context, availableContexts } = useAuthStore();
|
||||
const [tab, setTab] = useState<Kind>('basic');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editContract, setEditContract] = useState<Contract | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
const [pickedDoctorUuid, setPickedDoctorUuid] = useState<string | null>(null);
|
||||
|
||||
// A user who is both a doctor and a clinic owner may have a doctor db_uuid active;
|
||||
// fall back to the clinic context so the roster query targets the clinic. Same
|
||||
// resolution as ClinicAppointmentSettingsPage.
|
||||
const clinicUuid = useMemo(() => {
|
||||
if (context?.type === 'clinic') return dbUuid;
|
||||
return availableContexts.find((c) => c.type === 'clinic')?.db_uuid ?? null;
|
||||
}, [context, dbUuid, availableContexts]);
|
||||
|
||||
const doctorsQuery = useQuery({
|
||||
queryKey: ['clinic-doctors', clinicUuid],
|
||||
queryFn: () => api.get<ApiResponse<{ data: ClinicDoctorItem[] }>>(`/api/v1/clinic/doctor-list/${clinicUuid}`),
|
||||
enabled: !!clinicUuid,
|
||||
});
|
||||
|
||||
const doctorList: ClinicDoctorItem[] = useMemo(() => {
|
||||
const raw = doctorsQuery.data?.data;
|
||||
return (raw as any)?.data ?? raw ?? [];
|
||||
}, [doctorsQuery.data]);
|
||||
|
||||
// In a clinic, insurance is per-doctor: default to the first doctor. Solo doctors /
|
||||
// personal offices have no clinic context → doctorUuid stays null → backend keeps the
|
||||
// legacy tenant-scoped behavior.
|
||||
const isClinic = !!clinicUuid;
|
||||
const doctorUuid = useMemo(
|
||||
() => (isClinic ? pickedDoctorUuid ?? doctorList[0]?.uuid ?? null : null),
|
||||
[isClinic, pickedDoctorUuid, doctorList],
|
||||
);
|
||||
const showDoctorPicker = isClinic && doctorList.length > 1;
|
||||
const dq = doctorUuid ? `?doctor_uuid=${encodeURIComponent(doctorUuid)}` : '';
|
||||
|
||||
const contractsQuery = useQuery({
|
||||
queryKey: ['tenant-insurances'],
|
||||
queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
|
||||
queryKey: ['tenant-insurances', doctorUuid],
|
||||
queryFn: () => api.get(`/api/v1/billing/tenant-insurances${dq}`),
|
||||
});
|
||||
|
||||
const pricingQuery = useQuery({
|
||||
queryKey: ['insurance-pricing'],
|
||||
queryFn: () => api.get('/api/v1/insurance-pricing'),
|
||||
queryKey: ['insurance-pricing', doctorUuid],
|
||||
queryFn: () => api.get(`/api/v1/insurance-pricing${dq}`),
|
||||
});
|
||||
|
||||
const contracts: Contract[] = (contractsQuery.data as any)?.data?.data ?? [];
|
||||
@@ -76,7 +112,7 @@ export default function TenantInsuranceContracts() {
|
||||
supplementary: contracts.filter((c) => kindOf(c) === 'supplementary').length,
|
||||
}), [contracts, catalogTypeById]);
|
||||
|
||||
const invalidate = () => qc.invalidateQueries({ queryKey: ['tenant-insurances'] });
|
||||
const invalidate = () => qc.invalidateQueries({ queryKey: ['tenant-insurances', doctorUuid] });
|
||||
const toggleRow = (uuid: string) => setExpanded((p) => (p === uuid ? null : uuid));
|
||||
|
||||
const saveMut = useMutation({
|
||||
@@ -94,7 +130,10 @@ export default function TenantInsuranceContracts() {
|
||||
|
||||
const toggleMut = useMutation({
|
||||
mutationFn: (c: Contract) =>
|
||||
api.patch(`/api/v1/billing/tenant-insurances/${c.uuid}`, { is_active: !c.is_active }),
|
||||
api.patch(`/api/v1/billing/tenant-insurances/${c.uuid}`, {
|
||||
is_active: !c.is_active,
|
||||
...(doctorUuid ? { doctor_uuid: doctorUuid } : {}),
|
||||
}),
|
||||
onSuccess: () => invalidate(),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
@@ -114,6 +153,21 @@ export default function TenantInsuranceContracts() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showDoctorPicker && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 16, maxWidth: 320 }}>
|
||||
<label style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-2)' }}>پزشک</label>
|
||||
<SearchableSelect
|
||||
options={doctorList.map((d) => ({ value: d.uuid, label: d.name }))}
|
||||
value={doctorUuid ?? ''}
|
||||
onChange={(v) => { setPickedDoctorUuid(v ? String(v) : null); setExpanded(null); }}
|
||||
placeholder="انتخاب پزشک..."
|
||||
/>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-3)' }}>
|
||||
تنظیمات بیمه برای هر پزشک جداگانه ذخیره میشود.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div role="tablist" style={{ display: 'flex', gap: 4, borderBottom: '1px solid var(--border)', marginBottom: 16 }}>
|
||||
{KINDS.map((k) => {
|
||||
const active = k.key === tab;
|
||||
@@ -181,6 +235,7 @@ export default function TenantInsuranceContracts() {
|
||||
editContract={editContract}
|
||||
options={editContract ? allInsurances : available}
|
||||
kind={editContract ? kindOf(editContract) : tab}
|
||||
doctorUuid={doctorUuid}
|
||||
onClose={closeModal}
|
||||
onSubmit={(payload) => saveMut.mutate(payload)}
|
||||
isPending={saveMut.isPending}
|
||||
|
||||
Reference in New Issue
Block a user