feat: Implement resource-based appointment booking flow
- Updated DoctorPage component to accept bookingResources prop for appointment list. - Added serviceQuery function to serialize service item UUIDs for API requests. - Introduced new API endpoints for fetching booking resources and resource slots. - Enhanced tests for new resource-based booking functionality, including resource selection and service availability. - Created ResourceSelect component for selecting appointment types, including doctor and resource options. - Updated appointment submission logic to include resource_uuid in payload when applicable. - Ensured UI reflects changes in booking flow without disrupting existing doctor-centric experience.
This commit is contained in:
@@ -65,6 +65,12 @@ function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid
|
||||
// Service-based booking (روش نوبتدهی سرویسی)
|
||||
const [selectedServiceUuids, setSelectedServiceUuids] = useState([]);
|
||||
|
||||
// نوبتدهی منبعمحور: دستگاه/اتاق/یونیتِ همین پزشک در همین محل.
|
||||
// `selectedResource === null` یعنی «نوبت با پزشک» — همان جریان همیشگی.
|
||||
const [resources, setResources] = useState([]);
|
||||
const [selectedResource, setSelectedResource] = useState(null);
|
||||
const [resourceConfirmed, setResourceConfirmed] = useState(false);
|
||||
|
||||
// محل نوبتدهی: مطب شخصی یا یکی از کلینیکهایی که پزشک در آن برنامه دارد.
|
||||
const [bookingLocations, setBookingLocations] = useState([]);
|
||||
const [selectedLocation, setSelectedLocation] = useState(null);
|
||||
@@ -120,21 +126,81 @@ function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid
|
||||
.catch(() => {});
|
||||
}, [doctor?.uuid, browsingDate]);
|
||||
|
||||
// منابع **مستقل از محلها** گرفته میشوند و یکبار برای همهٔ محیطهای پزشک.
|
||||
//
|
||||
// وابستهکردنش به محل، پزشکی را که خودش برنامهٔ نوبتدهی ندارد ولی دستگاهش دارد از
|
||||
// دسترس خارج میکرد: «محل» از برنامهٔ هفتگیِ پزشک میآید و برای او خالی است، در حالی
|
||||
// که دستگاه تقویم و شعبهٔ خودش را دارد. هر منبع `clinic_uuid` خودش را همراه دارد.
|
||||
//
|
||||
// خطا عمداً بیصدا است — پزشکِ بدون منبع و خطای شبکه هر دو یعنی «مرحلهٔ منبع را نشان
|
||||
// نده»، و جریان پزشکمحور باید دستنخورده ادامه یابد.
|
||||
useEffect(() => {
|
||||
if (!doctor?.uuid) return;
|
||||
|
||||
let cancelled = false;
|
||||
request
|
||||
.getBookingResources(doctor.uuid)
|
||||
.then((res) => {
|
||||
if (cancelled) return;
|
||||
const d = res?.data?.data ?? res?.data ?? {};
|
||||
setResources(Array.isArray(d.resources) ? d.resources : []);
|
||||
})
|
||||
.catch(() => !cancelled && setResources([]));
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [doctor?.uuid]);
|
||||
|
||||
// منابعِ محلِ انتخابشده. وقتی محلی وجود ندارد (پزشکِ بدون برنامهٔ هفتگی) همهٔ منابع
|
||||
// نشان داده میشوند، چون خودشان محیطشان را همراه دارند.
|
||||
const locationResources =
|
||||
bookingLocations.length === 0
|
||||
? resources
|
||||
: resources.filter(
|
||||
(r) => (r.clinic_uuid ?? null) === (selectedLocation?.clinic_uuid ?? null)
|
||||
);
|
||||
|
||||
// روش نوبتدهی و سرویسها per-location هستند: یک پزشک میتواند در مطب شخصی
|
||||
// اسلاتی و در کلینیک سرویسی باشد.
|
||||
const bookingMode = selectedLocation?.booking_mode === "service" ? "service" : "slot";
|
||||
const bookingServices = selectedLocation?.services ?? [];
|
||||
//
|
||||
// منبع این را بازنویسی میکند: منبع اسلاتِ ثابت ندارد و زمانهایش همیشه از مدتِ
|
||||
// سرویسهای انتخابشده ساخته میشوند، حتی اگر برنامهٔ همان محل اسلاتی باشد.
|
||||
const bookingMode =
|
||||
selectedResource !== null || selectedLocation?.booking_mode === "service"
|
||||
? "service"
|
||||
: "slot";
|
||||
const bookingServices = selectedResource?.services ?? selectedLocation?.services ?? [];
|
||||
|
||||
// تعویض محل، انتخابهای وابسته را باطل میکند؛ وگرنه ترکیب سرویسِ یک محل با
|
||||
// اسلات محل دیگر ساخته میشود و ثبت نوبت با ۴۲۲ رد میشود.
|
||||
//
|
||||
// باطلسازی آبشاری است: محل ← منبع ← سرویس ← روز ← ساعت. هر سطح، همهٔ سطوح
|
||||
// پایینترش را پاک میکند.
|
||||
const changeLocation = (location) => {
|
||||
setSelectedLocation(location);
|
||||
setLocationConfirmed(true);
|
||||
setSelectedResource(null);
|
||||
setResourceConfirmed(false);
|
||||
setSelectedServiceUuids([]);
|
||||
setSelectedSlot(null);
|
||||
setSelectedDate(null);
|
||||
};
|
||||
|
||||
// انتخاب «نوبت با پزشک» (`resource === null`) هم یک انتخاب است و مرحله را میبندد.
|
||||
const changeResource = (resource) => {
|
||||
setSelectedResource(resource);
|
||||
setResourceConfirmed(true);
|
||||
setSelectedServiceUuids([]);
|
||||
setSelectedSlot(null);
|
||||
setSelectedDate(null);
|
||||
};
|
||||
|
||||
const reopenResourceChoice = () => setResourceConfirmed(false);
|
||||
|
||||
// منبعی وجود ندارد ⇒ مرحله اصلاً رندر نمیشود و تجربهٔ فعلی بدون تغییر میماند.
|
||||
const resourceStepNeeded = locationResources.length > 0 && !resourceConfirmed;
|
||||
|
||||
// بازگشت به مرحلهٔ انتخاب محل
|
||||
const reopenLocationChoice = () => setLocationConfirmed(false);
|
||||
|
||||
@@ -257,6 +323,11 @@ function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid
|
||||
locationConfirmed={locationConfirmed}
|
||||
changeLocation={changeLocation}
|
||||
reopenLocationChoice={reopenLocationChoice}
|
||||
resources={locationResources}
|
||||
selectedResource={selectedResource}
|
||||
resourceStepNeeded={resourceStepNeeded}
|
||||
changeResource={changeResource}
|
||||
reopenResourceChoice={reopenResourceChoice}
|
||||
onDateChange={setBrowsingDate}
|
||||
selectedClosedOnDate={selectedClosedOnDate}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user