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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user