feat: redesign insurance pricing page with tabbed navigation for basic and supplementary insurance, expandable rows for contract details, and move free visit price card to appointment settings

This commit is contained in:
hamed
2026-07-15 14:55:16 +03:30
parent 3e5dee0ad5
commit 9f75aeedf5
8 changed files with 495 additions and 96 deletions
@@ -9,7 +9,7 @@ vi.mock('../lib/api', () => ({
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
import { api } from '../lib/api';
import TenantInsuranceContracts, { filterInsurances } from './TenantInsuranceContracts';
import TenantInsuranceContracts, { filterInsurances, contractSummary } from './TenantInsuranceContracts';
import type { Contract } from './InsuranceModal';
const get = api.get as ReturnType<typeof vi.fn>;
@@ -38,6 +38,20 @@ describe('filterInsurances', () => {
});
});
describe('contractSummary', () => {
it('shows coverage, franchise and ceiling inline', () => {
const s = contractSummary(mk({ coverage_percent: 90, franchise_rials: 500_000, annual_ceiling_rials: 20_000_000 }));
expect(s).toContain('پوشش');
expect(s).toContain('فرانشیز');
expect(s).toContain('سقف پوشش');
});
it('omits franchise when zero and marks unlimited ceiling', () => {
const s = contractSummary(mk({ franchise_rials: 0, annual_ceiling_rials: null }));
expect(s).not.toContain('فرانشیز');
expect(s).toContain('سقف پوشش نامحدود');
});
});
describe('TenantInsuranceContracts', () => {
beforeEach(() => {
get.mockReset();
@@ -46,8 +60,9 @@ describe('TenantInsuranceContracts', () => {
get.mockImplementation((path: string) => {
if (path.includes('tenant-insurances')) {
return Promise.resolve({ success: true, data: { data: [
mk({ uuid: 'u1', insurance_id: 3, insurance_name: 'بیمه ایران', is_active: true }),
mk({ uuid: 'u2', insurance_id: 5, insurance_name: 'بیمه آسیا', is_active: false }),
mk({ uuid: 'u1', insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic', is_active: true }),
mk({ uuid: 'u2', insurance_id: 5, insurance_name: 'بیمه آسیا', insurance_kind: 'basic', is_active: false }),
mk({ uuid: 'u3', insurance_id: 7, insurance_name: 'بیمه دانا', insurance_kind: 'supplementary', is_active: true }),
] } });
}
return Promise.resolve({ success: true, data: { insurances: [] } });
@@ -56,10 +71,42 @@ describe('TenantInsuranceContracts', () => {
// Desktop table and mobile cards both render in jsdom (CSS `hidden`/`md:` is inert),
// so each row's text appears twice — assertions use *AllBy* accordingly.
it('lists active and inactive contracts', async () => {
it('lists only the active tab (basic) contracts', async () => {
renderWithProviders(<TenantInsuranceContracts />);
expect((await screen.findAllByText('بیمه ایران')).length).toBeGreaterThan(0);
expect(screen.getAllByText('بیمه آسیا').length).toBeGreaterThan(0);
// Supplementary contract is hidden under the other tab.
expect(screen.queryByText('بیمه دانا')).not.toBeInTheDocument();
});
it('switching to the supplementary tab filters by kind', async () => {
renderWithProviders(<TenantInsuranceContracts />);
await screen.findAllByText('بیمه ایران');
fireEvent.click(screen.getByRole('tab', { name: /بیمه تکمیلی/ }));
expect((await screen.findAllByText('بیمه دانا')).length).toBeGreaterThan(0);
expect(screen.queryByText('بیمه ایران')).not.toBeInTheDocument();
});
it('classifies by catalog type, overriding a stale contract kind', async () => {
// آسیا is stored on the contract as basic (legacy manual pick) but the catalog
// marks it supplementary — catalog type wins, so it must leave the basic tab.
get.mockImplementation((path: string) => {
if (path.includes('tenant-insurances')) {
return Promise.resolve({ success: true, data: { data: [
mk({ uuid: 'u1', insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic' }),
mk({ uuid: 'u2', insurance_id: 5, insurance_name: 'بیمه آسیا', insurance_kind: 'basic' }),
] } });
}
return Promise.resolve({ success: true, data: { insurances: [
{ insurance_id: 3, insurance_name: 'بیمه ایران', type: 'basic' },
{ insurance_id: 5, insurance_name: 'بیمه آسیا', type: 'supplementary' },
] } });
});
renderWithProviders(<TenantInsuranceContracts />);
await screen.findByText('بیمه ایران');
expect(screen.queryByText('بیمه آسیا')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: /بیمه تکمیلی/ }));
expect(await screen.findByText('بیمه آسیا')).toBeInTheDocument();
});
it('search box filters rows by name', async () => {
@@ -70,12 +117,22 @@ describe('TenantInsuranceContracts', () => {
expect(screen.getAllByText('بیمه آسیا').length).toBeGreaterThan(0);
});
it('status toggle PATCHes is_active', async () => {
it('clicking a row expands its detail, clicking again collapses', async () => {
renderWithProviders(<TenantInsuranceContracts />);
const names = await screen.findAllByText('بیمه ایران');
expect(screen.queryByText('نسخه قرارداد')).not.toBeInTheDocument();
fireEvent.click(names[0]);
expect(screen.getAllByText('نسخه قرارداد').length).toBeGreaterThan(0);
fireEvent.click(names[0]);
expect(screen.queryByText('نسخه قرارداد')).not.toBeInTheDocument();
});
it('clicking the status toggle does not expand the row (stopPropagation)', async () => {
renderWithProviders(<TenantInsuranceContracts />);
await screen.findAllByText('بیمه ایران');
// The active row's toggle offers to deactivate it.
const toggles = screen.getAllByLabelText('غیرفعال کردن');
fireEvent.click(toggles[0]);
expect(screen.queryByText('نسخه قرارداد')).not.toBeInTheDocument();
await waitFor(() =>
expect(patch).toHaveBeenCalledWith('/api/v1/billing/tenant-insurances/u1', { is_active: false }),
);