A resource carries its own working hours and holidays in the resource-first model, so it belongs on the same settings page as a doctor's schedule rather than on a page of its own. The page now has a scope switch — doctors or resources — with the per-item tab bar below it, and both scopes reuse the panels that already existed: ScheduleSection for a doctor, the working-hours and exceptions panels for a resource. The selection lives in the query string, so back and refresh return to the same tab. The screenshot of the finished tab caught two real defects, both fixed here: Dates in the resource panels and the holidays page read as year 57932. formatDate already multiplies seconds by 1000, and five call sites passed `x * 1000` on top of it. This predates the tab — the code was inherited from the old calendar page — but it was invisible until a two-week preview was put on screen. The working-hours panel still told the user their hours were intersected with the branch's. Branches are gone; the shift is the only source now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
4.7 KiB
TypeScript
104 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { screen, waitFor } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { renderWithProviders } from '../test/utils';
|
||
|
||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||
vi.mock('../hooks/usePermissions', () => ({ usePermissions: () => ({ can: () => true }) }));
|
||
|
||
// ScheduleSection یک درختِ سنگین با کوئریهای خودش است؛ اینجا فقط باید ثابت شود کدام
|
||
// scope رندر میشود، نه اینکه برنامهٔ پزشک درست کار میکند — آن تست خودش را دارد.
|
||
vi.mock('../components/schedule/ScheduleSection', () => ({
|
||
ScheduleSection: () => <div>برنامهٔ پزشک</div>,
|
||
}));
|
||
vi.mock('../components/FreeVisitPrice', () => ({ default: () => <div>قیمت ویزیت</div> }));
|
||
vi.mock('../components/resources/ResourceWorkingHoursPanel', () => ({
|
||
default: ({ resourceUuid }: { resourceUuid?: string }) => <div>ساعات کاری {resourceUuid}</div>,
|
||
DAY_LABELS: [],
|
||
}));
|
||
vi.mock('../components/resources/ResourceExceptionsPanel', () => ({
|
||
default: ({ resourceUuid }: { resourceUuid?: string }) => <div>تعطیلات {resourceUuid}</div>,
|
||
}));
|
||
|
||
let resources: Array<{ uuid: string; name: string; type_name: string }> = [];
|
||
vi.mock('../hooks/useResources', () => ({
|
||
useResources: () => ({ resources, loading: false }),
|
||
}));
|
||
|
||
vi.mock('../lib/api', () => ({
|
||
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||
ApiError: class extends Error {},
|
||
}));
|
||
|
||
import { api } from '../lib/api';
|
||
import { useAuthStore } from '../stores/authStore';
|
||
import ClinicAppointmentSettingsPage from './ClinicAppointmentSettingsPage';
|
||
|
||
const get = api.get as ReturnType<typeof vi.fn>;
|
||
|
||
const LASER = { uuid: 'r-1', name: 'لیزر دایود', type_name: 'دستگاه لیزر' };
|
||
const ROOM = { uuid: 'r-2', name: 'اتاق ۱', type_name: 'اتاق درمان' };
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
resources = [LASER, ROOM];
|
||
useAuthStore.setState({
|
||
primaryRole: 'clinic',
|
||
dbUuid: 'c-1',
|
||
context: { type: 'clinic', db_uuid: 'c-1' } as never,
|
||
availableContexts: [],
|
||
});
|
||
get.mockResolvedValue({ success: true, data: { data: [{ uuid: 'd-1', name: 'دکتر مرادی' }] } });
|
||
});
|
||
|
||
describe('ClinicAppointmentSettingsPage', () => {
|
||
it('پیشفرض روی پزشکان است', async () => {
|
||
renderWithProviders(<ClinicAppointmentSettingsPage />);
|
||
|
||
await waitFor(() => expect(screen.getByText('برنامهٔ پزشک')).toBeInTheDocument());
|
||
expect(screen.queryByText(/ساعات کاری r-/)).not.toBeInTheDocument();
|
||
});
|
||
|
||
/** منبع همانجایی مدیریت میشود که برنامهٔ پزشک — نه در یک صفحهٔ جدا. */
|
||
it('تب منابع، ساعت کاری و تعطیلات منبع را میآورد', async () => {
|
||
const user = userEvent.setup();
|
||
renderWithProviders(<ClinicAppointmentSettingsPage />);
|
||
|
||
await user.click(screen.getByRole('button', { name: 'منابع' }));
|
||
|
||
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
|
||
expect(screen.getByText('تعطیلات r-1')).toBeInTheDocument();
|
||
expect(screen.queryByText('برنامهٔ پزشک')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('بین منابع جابهجا میشود', async () => {
|
||
const user = userEvent.setup();
|
||
renderWithProviders(<ClinicAppointmentSettingsPage />);
|
||
|
||
await user.click(screen.getByRole('button', { name: 'منابع' }));
|
||
await user.click(await screen.findByRole('button', { name: 'اتاق ۱' }));
|
||
|
||
await waitFor(() => expect(screen.getByText('ساعات کاری r-2')).toBeInTheDocument());
|
||
});
|
||
|
||
/** scope در URL مینشیند، وگرنه «بازگشت» و رفرش کاربر را به تب پزشکان میپراند. */
|
||
it('scope را از URL میخواند', async () => {
|
||
renderWithProviders(<ClinicAppointmentSettingsPage />, {
|
||
route: '/admin/settings/appointment-settings?scope=resources',
|
||
});
|
||
|
||
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
|
||
});
|
||
|
||
it('کلینیک بدون منبع، حالت خالی با راهحل میدهد', async () => {
|
||
resources = [];
|
||
const user = userEvent.setup();
|
||
renderWithProviders(<ClinicAppointmentSettingsPage />);
|
||
|
||
await user.click(screen.getByRole('button', { name: 'منابع' }));
|
||
|
||
expect(await screen.findByText('هنوز منبعی تعریف نشده است')).toBeInTheDocument();
|
||
expect(screen.getByText('تنظیمات ← منابع')).toHaveAttribute('href', '/admin/resources');
|
||
});
|
||
});
|