feat: enhance resource calendar validation and UI

- Implement real-time validation for overlapping shifts in the ResourceWorkingHoursPanel.
- Remove the copy shift functionality to simplify the UI and prevent confusion.
- Introduce ResourceExceptionsCard to manage resource exceptions, including leave and maintenance.
- Update ClinicAppointmentSettingsPage to utilize new components and improve tab navigation for resource management.
- Add comprehensive validation tests for resource calendar to ensure overlapping shifts are correctly handled.
- Update API documentation to reflect new validation error messages and rules.
This commit is contained in:
hamed
2026-08-03 09:37:22 +03:30
parent c42679d98c
commit 348e1cf517
9 changed files with 535 additions and 289 deletions
@@ -16,8 +16,11 @@ 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>,
vi.mock('../components/resources/ResourceExceptionsCard', () => ({
default: ({ resourceUuid }: { resourceUuid?: string }) => <div>مرخصی {resourceUuid}</div>,
}));
vi.mock('../components/holidays/NationalHolidaysCard', () => ({
default: () => <div>تعطیلات رسمی سال</div>,
}));
let resources: Array<{ uuid: string; name: string; type_name: string }> = [];
@@ -60,17 +63,65 @@ describe('ClinicAppointmentSettingsPage', () => {
});
/** منبع همان‌جایی مدیریت می‌شود که برنامهٔ پزشک — نه در یک صفحهٔ جدا. */
it('تب منابع، ساعت کاری و تعطیلات منبع را می‌آورد', async () => {
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 />, {
route: '/admin/settings/appointment-settings?scope=resources',
});
// پیش‌فرض روی شیفت هفتگی است — کارِ اصلی صفحه.
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
expect(screen.queryByText('تعطیلات رسمی سال')).not.toBeInTheDocument();
expect(screen.queryByText('مرخصی r-1')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'تعطیلات رسمی' }));
expect(await screen.findByText('تعطیلات رسمی سال')).toBeInTheDocument();
expect(screen.queryByText('ساعات کاری r-1')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'مرخصی و سرویس' }));
expect(await screen.findByText('مرخصی r-1')).toBeInTheDocument();
expect(screen.queryByText('تعطیلات رسمی سال')).not.toBeInTheDocument();
});
/** تب از URL خوانده می‌شود، پس رفرش و «بازگشت» همان نما را نگه می‌دارند. */
it('تب برنامهٔ کاری را از URL می‌خواند', async () => {
renderWithProviders(<ClinicAppointmentSettingsPage />, {
route: '/admin/settings/appointment-settings?scope=resources&calendarTab=exceptions',
});
expect(await screen.findByText('مرخصی r-1')).toBeInTheDocument();
expect(screen.queryByText('ساعات کاری r-1')).not.toBeInTheDocument();
});
/** پیش‌نمایش دو هفته در این صفحه کاربردی ندارد و فقط ارتفاع اضافه می‌کرد. */
it('پیش‌نمایش دو هفته را نشان نمی‌دهد', async () => {
const user = userEvent.setup();
renderWithProviders(<ClinicAppointmentSettingsPage />, {
route: '/admin/settings/appointment-settings?scope=resources',
});
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
expect(screen.queryByText('پیش‌نمایش دو هفته')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'مرخصی و سرویس' }));
await screen.findByText('مرخصی r-1');
expect(screen.queryByText('پیش‌نمایش دو هفته')).not.toBeInTheDocument();
});
it('بین منابع جابه‌جا می‌شود', async () => {
const user = userEvent.setup();
renderWithProviders(<ClinicAppointmentSettingsPage />);