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
+47 -7
View File
@@ -171,20 +171,60 @@ describe('ResourceDetailPage', () => {
expect(screen.getAllByText('بدون شیفت — این روز بسته است')).toHaveLength(7);
});
/** پرتکرارترین حالت یک ساعتِ یکسان برای کل هفته است؛ بدون کپی، ۱۵ تعامل لازم بود. */
it('شیفت یک روز را به همهٔ روزهای هفته کپی می‌کند', async () => {
/**
* دو شیفتِ متداخل در یک روز نباید ذخیره شود. سرور هم همین را رد می‌کند، ولی خطا
* باید حین ویرایش دیده شود نه بعد از رفت‌وبرگشت شبکه.
*/
it('شیفت‌های متداخل را رد می‌کند و ذخیره را می‌بندد', async () => {
const days = emptyDays();
days['0'] = [{ sequence: 0, start_minute: 540, end_minute: 1020, start_time: '09:00', end_time: '17:00', active: true }];
days['0'] = [
{ sequence: 0, start_minute: 540, end_minute: 780, start_time: '09:00', end_time: '13:00', active: true },
{ sequence: 1, start_minute: 1020, end_minute: 1140, start_time: '17:00', end_time: '19:00', active: true },
];
const user = userEvent.setup();
mockApi(days);
renderPage('/admin/resources/r1?tab=hours');
await waitFor(() => expect(screen.getByDisplayValue('17:00')).toBeInTheDocument());
expect(screen.queryByText(/تداخل دارند/)).not.toBeInTheDocument();
// ۱۷:۰۰–۱۹:۰۰ به ۱۲:۰۰ کشیده می‌شود و با ۰۹:۰۰–۱۳:۰۰ تداخل پیدا می‌کند.
await user.clear(screen.getByLabelText('ساعت شروع شیفت ۲ روز شنبه'));
await user.type(screen.getByLabelText('ساعت شروع شیفت ۲ روز شنبه'), '12:00');
// پیام دو جا می‌آید و باید بیاید: کنار خودِ روز، و در نوار ذخیره که دلیل قفل را می‌گوید.
expect(await screen.findAllByText(/شیفت‌های روز شنبه با هم تداخل دارند/)).toHaveLength(2);
expect(screen.getByRole('button', { name: 'ذخیرهٔ شیفت‌ها' })).toBeDisabled();
});
/** بازهٔ بدون تداخل نباید قربانی اعتبارسنجی شود — مرز چسبیده مجاز است. */
it('شیفت‌های چسبیده اما بدون هم‌پوشانی را می‌پذیرد', async () => {
const days = emptyDays();
days['0'] = [
{ sequence: 0, start_minute: 540, end_minute: 780, start_time: '09:00', end_time: '13:00', active: true },
{ sequence: 1, start_minute: 780, end_minute: 1020, start_time: '13:00', end_time: '17:00', active: true },
];
const user = userEvent.setup();
mockApi(days);
renderPage('/admin/resources/r1?tab=hours');
await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument());
await user.click(screen.getByRole('button', { name: 'کپی شیفت‌های شنبه به همهٔ روزهای هفته' }));
expect(screen.queryByText(/تداخل دارند/)).not.toBeInTheDocument();
await waitFor(() => expect(screen.getAllByDisplayValue('09:00')).toHaveLength(7));
expect(screen.getAllByDisplayValue('17:00')).toHaveLength(7);
expect(screen.queryByText('بدون شیفت — این روز بسته است')).not.toBeInTheDocument();
await user.click(screen.getAllByRole('button', { name: 'شیفت' })[1]);
expect(await screen.findByRole('button', { name: 'ذخیرهٔ شیفت‌ها' })).toBeEnabled();
});
/** آیکون کپی گمراه‌کننده بود و حذف شد. */
it('دکمهٔ کپی شیفت ندارد', async () => {
const days = emptyDays();
days['0'] = [{ sequence: 0, start_minute: 540, end_minute: 1020, start_time: '09:00', end_time: '17:00', active: true }];
mockApi(days);
renderPage('/admin/resources/r1?tab=hours');
await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument());
expect(screen.queryByRole('button', { name: /کپی/ })).not.toBeInTheDocument();
});
/** پیش‌نمایش نباید «وقت قابل رزرو» خوانده شود — نوبت‌ها هنوز کسر نشده‌اند. */