` دوم با ``) وقتی `expanded === c.uuid` رندر شود و همهی فیلدهای کامل قرارداد را نشان دهد:
+ - درصد پوشش، فرانشیز، سقف تعهد سالانه
+ - تاریخ شروع و پایان قرارداد (`effective_from` / `effective_to`) با `formatDate` شمسی (`assets/admin/lib/utils.ts`)؛ اگر `effective_to == null` → «بدون تاریخ پایان»
+ - نسخهی قرارداد (`version`) و کد بیمه (`insurance_id`)
+ - وضعیت فعال/غیرفعال
+- انیمیشن باز/بستهشدن نرم باشد (از `--ease` استفاده کن یا یک transition ساده روی ارتفاع/opacity).
+
+**۴.۳ نسخهی موبایل (`md:hidden`):** همان الگوی Expandable روی کارتها — کارت جمعشده خلاصه را نشان دهد و با کلیک جزئیات کامل باز شود.
+
+**۴.۴ Empty state هر Tab:** اگر قراردادی برای Tab فعال نبود، پیام مناسب همان نوع: «هنوز بیمهی پایهای اضافه نکردهاید.» / «هنوز بیمهی تکمیلیای اضافه نکردهاید.» (بهجای پیام عمومی فعلی).
+
+## نکات مهم
+
+- **بدون تغییر backend و بدون migration.** فقط frontend. اگر حین کار حس کردی endpoint کم دارد، اول دوباره بگرد — احتمالاً داده در همان `insurance-pricing` / `tenant-insurances` هست.
+- **SOLID / تکمسئولیتی (قاعده ۱):** ردیف Expandable و کارت موبایل را به کامپوننتهای کوچک جدا کن (مثلاً `ContractRow`, `ContractCard`, `ContractDetails`) تا `TenantInsuranceContracts` متورم نشود. `StatusToggle` و `Row` فعلی را نگهدار/بازاستفاده کن.
+- **توکنهای طراحی:** هیچ رنگ hex هاردکد نکن؛ از `var(--...)` استفاده کن (`--primary`, `--surface-2`, `--border`, `--text-2/3`, `--success`, `--r`, `--ease`). منبع توکنها `assets/admin/styles.css` است.
+- **RTL و فارسی:** همهی رشتهها فارسی، اعداد و مبالغ فارسی از طریق `formatRial`/`formatNumber`/`formatDate`. از `insetInlineStart`/`paddingInlineStart` (نه left/right) مثل کد فعلی.
+- **`insurance_kind` ممکن است `null` باشد** (قراردادهای قدیمی که `kind` نداشتند و `type` کاتالوگشان هم null بوده) — در فیلتر Tab آن را به `'basic'` fallback بده تا گم نشود.
+- **حالت ویرایش:** نوع بیمه در ویرایش تغییر نمیکند؛ مودال در ویرایش `kind` قرارداد را حفظ میکند و لیست کامل `allInsurances` را میدهد (چون `insuranceId` قفل/`isDisabled` است).
+- **تستها (قاعده ۴):** فایل تست موجود `assets/admin/components/TenantInsuranceContracts.test.tsx` و `InsuranceModal.test.tsx` را بهروزرسانی/گسترش بده — سناریوها: (الف) فیلتر Tab قراردادها را درست جدا میکند، (ب) لیست انتخاب افزودن فقط بیمههای همان نوع را دارد، (ج) کلیک روی ردیف جزئیات را باز/بسته میکند، (د) کلیک روی دکمهی ویرایش/سوییچ ردیف را toggle نمیکند (`stopPropagation`)، (ه) `AppointmentSettingsPage.test.tsx`: `FreeVisitPrice` رندر میشود حتی وقتی `uuid` نیست. تستها را با `yarn test` سبز کن.
+- **type check:** `npx tsc --noEmit --project tsconfig.json` بدون خطا.
+- **مصرفکنندهی دیگر `FreeVisitPrice`:** مطمئن شو جایی جز `InsurancePricingPage` آن را import نمیکند (grep) تا انتقال چیزی را نشکند.
diff --git a/assets/admin/components/InsuranceModal.test.tsx b/assets/admin/components/InsuranceModal.test.tsx
index ca3c342b..99ba1a3d 100644
--- a/assets/admin/components/InsuranceModal.test.tsx
+++ b/assets/admin/components/InsuranceModal.test.tsx
@@ -51,13 +51,15 @@ describe('contractToForm', () => {
});
describe('InsuranceModal', () => {
- it('renders all seven fields in add mode', () => {
+ it('renders the fields in add mode with no manual kind select', () => {
renderWithProviders(
- {}} onSubmit={() => {}} />,
+ {}} onSubmit={() => {}} />,
);
expect(screen.getByText('افزودن بیمه')).toBeInTheDocument();
expect(screen.getByText('نام بیمه')).toBeInTheDocument();
- expect(screen.getByText('نوع بیمه')).toBeInTheDocument();
+ // Manual "نوع بیمه" select is gone; kind is shown as a read-only chip from the tab.
+ expect(screen.queryByText('نوع بیمه')).not.toBeInTheDocument();
+ expect(screen.getByText('پایه')).toBeInTheDocument();
expect(screen.getByText('تاریخ شروع قرارداد')).toBeInTheDocument();
expect(screen.getByText('تاریخ پایان قرارداد')).toBeInTheDocument();
expect(screen.getByText('درصد پوشش')).toBeInTheDocument();
@@ -66,10 +68,18 @@ describe('InsuranceModal', () => {
expect(screen.getByText('ثبت بیمه')).toBeInTheDocument();
});
+ it('shows the tab kind chip and carries it into a new payload', () => {
+ const onSubmit = vi.fn();
+ renderWithProviders(
+ {}} onSubmit={onSubmit} />,
+ );
+ expect(screen.getByText('تکمیلی')).toBeInTheDocument();
+ });
+
it('submits the built payload for an edited contract', () => {
const onSubmit = vi.fn();
renderWithProviders(
- {}} onSubmit={onSubmit} />,
+ {}} onSubmit={onSubmit} />,
);
fireEvent.click(screen.getByText('ثبت بیمه'));
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
diff --git a/assets/admin/components/InsuranceModal.tsx b/assets/admin/components/InsuranceModal.tsx
index 5c2baa87..692b41a4 100644
--- a/assets/admin/components/InsuranceModal.tsx
+++ b/assets/admin/components/InsuranceModal.tsx
@@ -76,6 +76,8 @@ interface Props {
editContract: Contract | null;
/** Insurance catalog options; in edit mode all are shown, in add mode only the available ones. */
options: InsuranceOption[];
+ /** Insurance kind of the active tab ('basic'|'supplementary'); assigned to new contracts, not user-editable. */
+ kind: string;
onClose: () => void;
onSubmit: (payload: ReturnType) => void;
isPending?: boolean;
@@ -86,13 +88,13 @@ interface Props {
* state, emits the built payload via onSubmit. Fields mirror the Figma "افزودن بیمه"
* modal plus the injected coverage/franchise/ceiling controls.
*/
-export default function InsuranceModal({ open, editContract, options, onClose, onSubmit, isPending }: Props) {
+export default function InsuranceModal({ open, editContract, options, kind, onClose, onSubmit, isPending }: Props) {
const [form, setForm] = useState(EMPTY_FORM);
useEffect(() => {
if (!open) return;
- setForm(editContract ? contractToForm(editContract) : EMPTY_FORM);
- }, [open, editContract]);
+ setForm(editContract ? contractToForm(editContract) : { ...EMPTY_FORM, kind });
+ }, [open, editContract, kind]);
const set = (patch: Partial) => setForm((f) => ({ ...f, ...patch }));
const isEdit = editContract !== null;
@@ -127,7 +129,15 @@ export default function InsuranceModal({ open, editContract, options, onClose, o
>
-
نام بیمه
+
+ نام بیمه
+
+ {KIND_LABEL[form.kind] ?? form.kind}
+
+
({ value: String(i.insurance_id), label: i.insurance_name }))}
value={form.insuranceId}
@@ -137,14 +147,6 @@ export default function InsuranceModal({ open, editContract, options, onClose, o
/>
-
- نوع بیمه
- set({ kind: e.target.value })}>
- پایه
- تکمیلی
-
-
-
تاریخ شروع قرارداد
diff --git a/assets/admin/components/TenantInsuranceContracts.test.tsx b/assets/admin/components/TenantInsuranceContracts.test.tsx
index ca473765..db32c38f 100644
--- a/assets/admin/components/TenantInsuranceContracts.test.tsx
+++ b/assets/admin/components/TenantInsuranceContracts.test.tsx
@@ -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
;
@@ -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( );
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( );
+ 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( );
+ 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( );
+ 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( );
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 }),
);
diff --git a/assets/admin/components/TenantInsuranceContracts.tsx b/assets/admin/components/TenantInsuranceContracts.tsx
index fc60b961..37ac030e 100644
--- a/assets/admin/components/TenantInsuranceContracts.tsx
+++ b/assets/admin/components/TenantInsuranceContracts.tsx
@@ -1,11 +1,22 @@
import { useMemo, useState, type ReactNode } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { PlusIcon, PencilIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
+import { PlusIcon, PencilIcon, MagnifyingGlassIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
import { toast } from 'sonner';
import { api } from '../lib/api';
-import { formatRial } from '../lib/utils';
+import { formatRial, formatNumber, formatDate } from '../lib/utils';
import InsuranceModal, { Contract, InsuranceOption, KIND_LABEL, buildInsurancePayload } from './InsuranceModal';
+type Kind = 'basic' | 'supplementary';
+
+const KINDS: { key: Kind; label: string; addLabel: string; emptyLabel: string }[] = [
+ { key: 'basic', label: 'بیمه پایه', addLabel: 'افزودن بیمه پایه', emptyLabel: 'هنوز بیمهی پایهای اضافه نکردهاید.' },
+ { key: 'supplementary', label: 'بیمه تکمیلی', addLabel: 'افزودن بیمه تکمیلی', emptyLabel: 'هنوز بیمهی تکمیلیای اضافه نکردهاید.' },
+];
+
+/** Contract's effective kind, falling back to 'basic' for legacy rows with no kind/type. */
+const contractKind = (c: Contract): Kind =>
+ (c.insurance_kind === 'supplementary' ? 'supplementary' : 'basic');
+
/** Case-insensitive filter over insurance name (and code) — the "جستجو در بیمه ها..." box. */
export function filterInsurances(list: Contract[], query: string): Contract[] {
const q = query.trim().toLowerCase();
@@ -16,11 +27,21 @@ export function filterInsurances(list: Contract[], query: string): Contract[] {
);
}
+/** One-line readable summary shown on the collapsed row: پوشش ۹۰٪ · فرانشیز … · سقف پوشش … */
+export function contractSummary(c: Contract): string {
+ const parts = [`پوشش ${formatNumber(c.coverage_percent)}٪`];
+ if (c.franchise_rials > 0) parts.push(`فرانشیز ${formatRial(c.franchise_rials)}`);
+ parts.push(c.annual_ceiling_rials != null ? `سقف پوشش ${formatRial(c.annual_ceiling_rials)}` : 'سقف پوشش نامحدود');
+ return parts.join(' · ');
+}
+
export default function TenantInsuranceContracts() {
const qc = useQueryClient();
+ const [tab, setTab] = useState('basic');
const [modalOpen, setModalOpen] = useState(false);
const [editContract, setEditContract] = useState(null);
const [search, setSearch] = useState('');
+ const [expanded, setExpanded] = useState(null);
const contractsQuery = useQuery({
queryKey: ['tenant-insurances'],
@@ -35,11 +56,28 @@ export default function TenantInsuranceContracts() {
const contracts: Contract[] = (contractsQuery.data as any)?.data?.data ?? [];
const allInsurances: InsuranceOption[] = (pricingQuery.data as any)?.data?.insurances ?? [];
const activeIds = new Set(contracts.map((c) => c.insurance_id));
- const available = allInsurances.filter((i) => !activeIds.has(i.insurance_id));
+ // Add-mode options: only the active tab's kind, excluding already-contracted insurances.
+ const available = allInsurances.filter((i) => i.type === tab).filter((i) => !activeIds.has(i.insurance_id));
- const rows = useMemo(() => filterInsurances(contracts, search), [contracts, search]);
+ // The insurance's real type comes from the catalog, not the contract's stored kind
+ // (legacy contracts carry a stale manually-picked kind). Catalog type is authoritative.
+ const catalogTypeById = useMemo(() => {
+ const m: Record = {};
+ for (const i of allInsurances) m[i.insurance_id] = i.type === 'supplementary' ? 'supplementary' : 'basic';
+ return m;
+ }, [allInsurances]);
+ const kindOf = (c: Contract): Kind =>
+ catalogTypeById[c.insurance_id] ?? contractKind(c);
+
+ const byKind = useMemo(() => contracts.filter((c) => kindOf(c) === tab), [contracts, tab, catalogTypeById]);
+ const rows = useMemo(() => filterInsurances(byKind, search), [byKind, search]);
+ const counts = useMemo(() => ({
+ basic: contracts.filter((c) => kindOf(c) === 'basic').length,
+ supplementary: contracts.filter((c) => kindOf(c) === 'supplementary').length,
+ }), [contracts, catalogTypeById]);
const invalidate = () => qc.invalidateQueries({ queryKey: ['tenant-insurances'] });
+ const toggleRow = (uuid: string) => setExpanded((p) => (p === uuid ? null : uuid));
const saveMut = useMutation({
mutationFn: (payload: ReturnType) =>
@@ -65,15 +103,46 @@ export default function TenantInsuranceContracts() {
const openEdit = (c: Contract) => { setEditContract(c); setModalOpen(true); };
const closeModal = () => { setModalOpen(false); setEditContract(null); };
+ const activeKind = KINDS.find((k) => k.key === tab)!;
+
return (
مدیریت بیمه
- افزودن بیمه
+ {activeKind.addLabel}
+
+ {KINDS.map((k) => {
+ const active = k.key === tab;
+ return (
+ { setTab(k.key); setExpanded(null); }}
+ style={{
+ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 14px',
+ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, fontWeight: 600,
+ color: active ? 'var(--primary)' : 'var(--text-3)',
+ borderBottom: `2px solid ${active ? 'var(--primary)' : 'transparent'}`,
+ marginBottom: -1, transition: 'color .2s, border-color .2s',
+ }}
+ >
+ {k.label}
+
+ {formatNumber(counts[k.key])}
+
+
+ );
+ })}
+
+
در حال بارگذاری...
) : rows.length === 0 ? (
- {search ? 'بیمهای یافت نشد.' : 'هنوز با هیچ بیمهای قرارداد ندارید.'}
+ {search ? 'بیمهای یافت نشد.' : activeKind.emptyLabel}
) : (
- <>
- {/* Desktop table */}
-
-
-
-
- ردیف
- نام بیمه
- کد
- نوع بیمه
- وضعیت
- عملیات
-
-
-
- {rows.map((c, i) => (
-
- {i + 1}
-
- {c.insurance_name ?? `#${c.insurance_id}`}
-
- پوشش {c.coverage_percent}٪
- {c.franchise_rials > 0 && ` · فرانشیز ${formatRial(c.franchise_rials)}`}
- {c.annual_ceiling_rials != null && ` · سقف ${formatRial(c.annual_ceiling_rials)}`}
-
-
- {c.insurance_id}
- {KIND_LABEL[c.insurance_kind ?? ''] ?? c.insurance_kind ?? '—'}
-
- toggleMut.mutate(c)} disabled={toggleMut.isPending} />
-
-
- openEdit(c)}>
-
-
-
-
- ))}
-
-
-
-
- {/* Mobile cards */}
-
- {rows.map((c) => (
-
-
-
{c.insurance_name ?? `#${c.insurance_id}`}
-
openEdit(c)}>
-
-
-
-
{c.insurance_id}} />
-
- toggleMut.mutate(c)} disabled={toggleMut.isPending} />} />
-
- ))}
-
- >
+
+ {rows.map((c) => (
+ toggleRow(c.uuid)}
+ onEdit={() => openEdit(c)}
+ onToggleStatus={() => toggleMut.mutate(c)}
+ statusPending={toggleMut.isPending}
+ />
+ ))}
+
)}
saveMut.mutate(payload)}
isPending={saveMut.isPending}
@@ -165,16 +189,74 @@ export default function TenantInsuranceContracts() {
);
}
-function Row({ label, value }: { label: string; value: ReactNode }) {
+interface RowProps {
+ contract: Contract;
+ open: boolean;
+ onToggleRow: () => void;
+ onEdit: () => void;
+ onToggleStatus: () => void;
+ statusPending?: boolean;
+}
+
+function ContractCard({ contract: c, open, onToggleRow, onEdit, onToggleStatus, statusPending }: RowProps) {
+ const stop = (fn: () => void) => (e: React.MouseEvent) => { e.stopPropagation(); fn(); };
return (
-
-
{label}:
-
{value}
+
+
+
+
+
{c.insurance_name ?? `#${c.insurance_id}`}
+
+
+
+
+
+
{contractSummary(c)}
+
{})}>
+
+
+ {open &&
}
);
}
-function StatusToggle({ contract, onToggle, disabled }: { contract: Contract; onToggle: () => void; disabled?: boolean }) {
+/** Expanded full detail of a contract (all fields), shown when its card is open. */
+function ContractDetails({ contract: c }: { contract: Contract }) {
+ return (
+
+
+ 0 ? formatRial(c.franchise_rials) : '—'} />
+
+
+
+ {formatNumber(c.version)}} />
+ {c.insurance_id}} />
+
+ {c.is_active ? 'فعال' : 'غیرفعال'}
+
+ }
+ />
+
+ );
+}
+
+function DetailCell({ label, value }: { label: string; value: ReactNode }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+function StatusToggle({ contract, onToggle, disabled }: { contract: Contract; onToggle: (e: React.MouseEvent) => void; disabled?: boolean }) {
return (
{
expect((await screen.findAllByText('مدیریت نوبت دهی')).length).toBeGreaterThan(1);
expect(screen.getByText('خرید اشتراک')).toBeInTheDocument(); // shell menu
});
+
+ it('renders the free-visit price card (moved here from insurance page)', async () => {
+ renderWithProviders( , { route: '/admin/appointment-settings' });
+ expect(await screen.findByText('قیمت ویزیت آزاد')).toBeInTheDocument();
+ });
+
+ it('shows the free-visit price card even when no doctor uuid is present', async () => {
+ useAuthStore.setState({ primaryRole: 'clinic', doctorUuid: null, dbUuid: null });
+ renderWithProviders( , { route: '/admin/appointment-settings' });
+ expect(await screen.findByText('قیمت ویزیت آزاد')).toBeInTheDocument();
+ expect(screen.getByText('این بخش فقط برای پزشک در دسترس است.')).toBeInTheDocument();
+ });
});
diff --git a/assets/admin/pages/AppointmentSettingsPage.tsx b/assets/admin/pages/AppointmentSettingsPage.tsx
index 51fd031e..f9247a93 100644
--- a/assets/admin/pages/AppointmentSettingsPage.tsx
+++ b/assets/admin/pages/AppointmentSettingsPage.tsx
@@ -3,6 +3,7 @@ import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
import { useAuthStore } from '../stores/authStore';
import SettingsLayout from '../components/layout/SettingsLayout';
+import FreeVisitPrice from '../components/FreeVisitPrice';
import { WeeklyScheduleTab, type AddressData } from './DoctorDetailPage';
/**
@@ -29,6 +30,8 @@ export default function AppointmentSettingsPage() {
مدیریت نوبت دهی
+
+
{!uuid ? (
این بخش فقط برای پزشک در دسترس است.
diff --git a/assets/admin/pages/InsurancePricingPage.tsx b/assets/admin/pages/InsurancePricingPage.tsx
index 0f2233f7..df477a37 100644
--- a/assets/admin/pages/InsurancePricingPage.tsx
+++ b/assets/admin/pages/InsurancePricingPage.tsx
@@ -1,6 +1,5 @@
import PageHeader from '../components/ui/PageHeader';
import TenantInsuranceContracts from '../components/TenantInsuranceContracts';
-import FreeVisitPrice from '../components/FreeVisitPrice';
import FeatureGate from '../components/ui/FeatureGate';
import SettingsLayout from '../components/layout/SettingsLayout';
@@ -10,10 +9,9 @@ export default function InsurancePricingPage() {