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
@@ -11,7 +11,8 @@ import { useResources } from '../hooks/useResources';
import SettingsLayout from '../components/layout/SettingsLayout';
import { ScheduleSection } from '../components/schedule/ScheduleSection';
import ResourceWorkingHoursPanel from '../components/resources/ResourceWorkingHoursPanel';
import ResourceExceptionsPanel from '../components/resources/ResourceExceptionsPanel';
import ResourceExceptionsCard from '../components/resources/ResourceExceptionsCard';
import NationalHolidaysCard from '../components/holidays/NationalHolidaysCard';
import FreeVisitPrice from '../components/FreeVisitPrice';
import PageHeader from '../components/ui/PageHeader';
import type { ClinicDoctorItem } from '../components/ClinicDoctorsManager';
@@ -82,18 +83,14 @@ function ClinicAppointmentSettingsContent() {
return (
<div className="fade-in">
{/* سوییچر پزشک/منبع در خودِ ردیف عنوان می‌نشیند: بالاترین نقطهٔ محتوا و همیشه
در دید، بدون اسکرول. پایین‌تر از هدر، کاربر باید دنبالش می‌گشت. */}
<PageHeader
title="مدیریت نوبت‌دهی"
description={description}
backTo="/admin/settings-menu"
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
{/* دو انتخابگر پشت سر هم می‌مانند — «نما» بعد «مورد». در اسلات action هدر،
سوییچر به لبهٔ مقابلِ صفحه می‌افتاد و از تبی که کنترل می‌کند جدا می‌شد. */}
<div>
<span className="field-label" id="appt-scope-tabs">نما</span>
<div className="seg" role="group" aria-labelledby="appt-scope-tabs">
action={(
<div className="seg" role="group" aria-label="نمای تنظیمات">
{SCOPES.map((s) => (
<button
key={s.id}
@@ -105,8 +102,10 @@ function ClinicAppointmentSettingsContent() {
</button>
))}
</div>
</div>
)}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap)' }}>
{scope === 'doctors' ? (
<DoctorsScope
loading={doctorsQ.isLoading}
@@ -250,15 +249,54 @@ function ResourcesScope({ loading, resources, selected, canUpdate, onSelect }: {
{/* همان دلیل تب پزشک: بدون key، شیفتِ نیمه‌ویرایش‌شده به منبع بعدی می‌چسبد. */}
{selected && (
<div key={selected.uuid} style={{ display: 'grid', gap: 'var(--gap)' }}>
<ResourceWorkingHoursPanel resourceUuid={selected.uuid} canUpdate={canUpdate} />
<ResourceExceptionsPanel resourceUuid={selected.uuid} canUpdate={canUpdate} />
</div>
<ResourceCalendarTabs key={selected.uuid} resourceUuid={selected.uuid} canUpdate={canUpdate} />
)}
</>
);
}
const CALENDAR_TABS = [
{ id: 'shifts', label: 'شیفت هفتگی' },
{ id: 'holidays', label: 'تعطیلات رسمی' },
{ id: 'exceptions', label: 'مرخصی و سرویس' },
] as const;
type CalendarTab = typeof CALENDAR_TABS[number]['id'];
/**
* برنامهٔ کاری منبع در سه تب: شیفت هفتگی، تعطیلات رسمی، مرخصی و سرویس.
*
* هر سه یک چیز را می‌سازند («این منبع کِی باز است؟») پس یک‌جا می‌مانند؛ ولی هم‌زمان
* دیده‌شدنشان لازم نیست و پشت‌سرهم چیدنشان صفحه را سه برابر بلند می‌کرد. تب فعال در
* URL می‌نشیند تا «بازگشت» و رفرش همان نما را برگردانند.
*/
function ResourceCalendarTabs({ resourceUuid, canUpdate }: { resourceUuid: string; canUpdate: boolean }) {
const [urlState, setUrlState] = useUrlState({ calendarTab: 'shifts' });
const tab = (CALENDAR_TABS.some((t) => t.id === urlState.calendarTab)
? urlState.calendarTab
: 'shifts') as CalendarTab;
return (
<div style={{ display: 'grid', gap: 12 }}>
<div className="seg" style={{ alignSelf: 'flex-start' }} role="group" aria-label="برنامهٔ کاری منبع">
{CALENDAR_TABS.map((t) => (
<button
key={t.id}
className={tab === t.id ? 'active' : ''}
aria-pressed={tab === t.id}
onClick={() => setUrlState({ calendarTab: t.id })}
>
{t.label}
</button>
))}
</div>
{tab === 'shifts' && <ResourceWorkingHoursPanel resourceUuid={resourceUuid} canUpdate={canUpdate} />}
{tab === 'holidays' && <NationalHolidaysCard canUpdate={canUpdate} />}
{tab === 'exceptions' && <ResourceExceptionsCard resourceUuid={resourceUuid} canUpdate={canUpdate} />}
</div>
);
}
export default function ClinicAppointmentSettingsPage() {
return (
<SettingsLayout active="appointment">