Files
nobat724_front/app/component/date/datePicker/index.js
T
hamed 54bebbd41a 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.
2026-08-09 10:45:52 +03:30

156 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef, useState } from "react";
import { useParams } from "next/navigation";
import moment from "moment-jalaali";
import "moment-timezone";
import { dateToTimestamp } from "@/helper";
import { request } from "@/services/response";
import InlineJalaliMonth from "@/components/common/InlineJalaliMonth";
function gregorianMonthsOf(jalaliMonth) {
const first = moment(jalaliMonth).startOf("jMonth");
const last = moment(jalaliMonth).endOf("jMonth");
const keys = new Set();
for (const d of [first, last]) {
keys.add(`${d.year()}-${d.month() + 1}`);
}
return [...keys].map((k) => {
const [year, month] = k.split("-").map(Number);
return { year, month };
});
}
function DatePicker({ setDate, clinicUuid = null, resourceUuid = null, selectedServiceUuids = [] }) {
const params = useParams();
const doctorUuid = params?.doctorId;
const [baseMonth, setBaseMonth] = useState(moment().tz("Asia/Tehran"));
const [selectedDate, setSelectedDate] = useState(null);
const [autoSelected, setAutoSelected] = useState(false);
const [disabledSet, setDisabledSet] = useState(() => new Set());
const [onlineEnabled, setOnlineEnabled] = useState(true);
const loadedMonths = useRef(new Set());
const secondMonth = moment(baseMonth).add(1, "jMonth");
// هر محل، روزهای غیرفعال و وضعیت نوبت‌دهی آنلاین خودش را دارد؛ کش ماه‌ها باید
// با تعویض محل دور ریخته شود وگرنه تقویم محل قبلی باقی می‌ماند.
//
// منبع هم همین‌طور: تقویم هر دستگاه مالِ خودش است، و مدتِ سرویس‌های انتخاب‌شده
// تعیین می‌کند کدام روز جا دارد — پس تغییر هرکدام یعنی کشِ باطل.
const serviceKey = selectedServiceUuids.join(",");
useEffect(() => {
loadedMonths.current = new Set();
setDisabledSet(new Set());
setOnlineEnabled(true);
setAutoSelected(false);
}, [clinicUuid, resourceUuid, serviceKey]);
useEffect(() => {
if (!doctorUuid && !resourceUuid) return;
const targets = [
...gregorianMonthsOf(baseMonth),
...gregorianMonthsOf(secondMonth),
].filter(({ year, month }) => !loadedMonths.current.has(`${year}-${month}`));
if (targets.length === 0) return;
targets.forEach(({ year, month }) => {
loadedMonths.current.add(`${year}-${month}`);
// منبع پرچمِ `online_booking_enabled` ندارد — آن روی برنامهٔ هفتگیِ پزشک است.
// نبودِ فیلد یعنی «روشن»، و شرطِ پایین‌تر فقط با `false` صریح واکنش نشان می‌دهد.
const req = resourceUuid
? request.getResourceMonthAvailability(resourceUuid, year, month, selectedServiceUuids)
: request.getMonthAvailability(doctorUuid, year, month, clinicUuid);
req
.then((res) => {
const data = res?.data ?? {};
if (data.online_booking_enabled === false) setOnlineEnabled(false);
const dates = Array.isArray(data.disabled_dates) ? data.disabled_dates : [];
if (dates.length) {
setDisabledSet((prev) => {
const next = new Set(prev);
dates.forEach((d) => next.add(d));
return next;
});
}
})
.catch(() => {
loadedMonths.current.delete(`${year}-${month}`);
});
});
}, [doctorUuid, baseMonth, clinicUuid, resourceUuid, serviceKey]);
const isDisabled = (date) => {
const day = moment(date).startOf("day");
if (day.isBefore(moment().startOf("day"))) return true;
return disabledSet.has(day.format("YYYY-MM-DD"));
};
const selectDay = (date) => {
if (isDisabled(date)) return;
setSelectedDate(date);
setDate(dateToTimestamp(date));
};
useEffect(() => {
if (autoSelected) return;
const today = moment().tz("Asia/Tehran");
for (let i = 0; i <= 60; i++) {
const candidate = today.clone().add(i, "days");
if (!isDisabled(candidate)) {
setSelectedDate(candidate);
setBaseMonth(candidate.clone());
setDate(candidate.clone().startOf("day").unix());
setAutoSelected(true);
break;
}
}
}, [disabledSet, autoSelected, setDate]);
if (!onlineEnabled) {
return (
<div className="w-full py-[40px] text-center text-[#525252] text-[14px] md:text-[16px] font-medium">
نوبتدهی آنلاین این پزشک در حال حاضر غیرفعال است.
</div>
);
}
return (
<div dir="rtl" className="datePickerApt flex items-start justify-evenly gap-[24px] w-full">
<div className="w-full lg:w-1/2">
<InlineJalaliMonth
displayDate={baseMonth}
selectedDate={selectedDate}
onSelectDay={selectDay}
isDisabled={isDisabled}
// وقتی تقویم ماه بعد در کنار این نمایش داده می‌شود (md و xl) دکمهٔ «بعد»
// این تقویم پنهان می‌ماند تا دوتا نشود؛ در موبایل و lg که تنها همین تقویم
// دیده می‌شود، دکمهٔ «بعد» فعال است تا کاربر بتواند به ماه بعد برود.
nextBtnClass="md:invisible lg:visible xl:invisible"
onPrev={() => setBaseMonth(moment(baseMonth).subtract(1, "jMonth"))}
onNext={() => setBaseMonth(moment(baseMonth).add(1, "jMonth"))}
/>
</div>
<div className="hidden md:block lg:hidden xl:block w-1/2">
<InlineJalaliMonth
displayDate={secondMonth}
selectedDate={selectedDate}
onSelectDay={selectDay}
isDisabled={isDisabled}
showPrev={false}
onNext={() => setBaseMonth(moment(baseMonth).add(1, "jMonth"))}
onPrev={() => setBaseMonth(moment(baseMonth).subtract(1, "jMonth"))}
/>
</div>
</div>
);
}
export default DatePicker;