A doctor now has one booking schedule per context — the personal practice plus one per clinic — and every booking endpoint takes an optional clinic_uuid where omitting it means the personal practice, not a wildcard. This site sent none, so a clinic-only doctor showed no availability at all and a doctor working in both places silently booked into the wrong one. - services/response.js: getBookingLocations + clinic_uuid on slots, service-slots, booking-services and month-availability. The manual query building is kept so the service_item_uuids[] serialisation does not change. - AppointmentPage owns the selected location; booking_mode and services are derived from it instead of a separate getBookingServices call, which drops a request. Changing location clears the selected service, slot and date, since a service from one location cannot be booked into another. - New LocationSelect step, shown only when there is more than one location. The list arrives sorted by earliest free slot, so the first item is the default and is not re-sorted here. - DatePicker drops its month cache when the location changes; otherwise the previous location's disabled days stayed on the calendar. - The slot address now comes from the selected location rather than doctor.address, which does not contain clinic addresses. - clinic_uuid rides through to the appointment payload, and /appointment/[doctorId]?clinic_uuid=… preselects a location. - Doctor page JSON-LD gains availableService from the bookable services. openingHoursSpecification still needs a public weekly-hours endpoint. Removed the dead locateVisit state, which was initialised true and never unset. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
231 lines
8.1 KiB
JavaScript
231 lines
8.1 KiB
JavaScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { request } from "@/services/response";
|
|
import Cookies from "js-cookie";
|
|
import Container from "./Container";
|
|
import { safeJsonParse } from "@/lib/sanitize";
|
|
|
|
const defaultData = {
|
|
phone: { value: "", isEdit: false },
|
|
national_code: { value: "", isEdit: true },
|
|
name: { value: "", isEdit: true },
|
|
family: { value: "", isEdit: true },
|
|
gender: { value: "", isEdit: true },
|
|
insurance_id: { value: "", isEdit: true },
|
|
basic_insurance: { value: "", isEdit: true },
|
|
supplementary_insurance: { value: "", isEdit: true },
|
|
};
|
|
|
|
function buildProfileData(profile, phone) {
|
|
return {
|
|
uuid: profile.uuid,
|
|
phone: { value: phone, isEdit: false },
|
|
national_code: {
|
|
value: profile.national_code || "",
|
|
isEdit: !profile.national_code_approved,
|
|
},
|
|
name: { value: profile.label || "", isEdit: true },
|
|
family: { value: profile.family || "", isEdit: true },
|
|
gender: { value: profile.gender || "", isEdit: true },
|
|
insurance_id: { value: profile.insurance_id || "", isEdit: true },
|
|
basic_insurance: {
|
|
value: profile.basic_insurance_id ? { id: profile.basic_insurance_id } : "",
|
|
isEdit: true,
|
|
},
|
|
supplementary_insurance: {
|
|
value: profile.supplementary_insurance_id
|
|
? { id: profile.supplementary_insurance_id }
|
|
: "",
|
|
isEdit: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid = null }) {
|
|
const [step, setStep] = useState(0);
|
|
const [isForAnother, setIsForAnother] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
const [prevData, setPrevData] = useState(defaultData);
|
|
const [data, setData] = useState(defaultData);
|
|
|
|
// Login states
|
|
const [num, setNum] = useState("");
|
|
const [uuid, setUuid] = useState("");
|
|
const [isSendMsg, setIsSendMsg] = useState(false);
|
|
|
|
// Appointment slot states
|
|
const [selectedSlot, setSelectedSlot] = useState(null);
|
|
const [selectedDate, setSelectedDate] = useState(null);
|
|
const [appointmentId, setAppointmentId] = useState(null);
|
|
const [appointmentExpiresAt, setAppointmentExpiresAt] = useState(null);
|
|
|
|
// Service-based booking (روش نوبتدهی سرویسی)
|
|
const [selectedServiceUuids, setSelectedServiceUuids] = useState([]);
|
|
|
|
// محل نوبتدهی: مطب شخصی یا یکی از کلینیکهایی که پزشک در آن برنامه دارد.
|
|
const [bookingLocations, setBookingLocations] = useState([]);
|
|
const [selectedLocation, setSelectedLocation] = useState(null);
|
|
const [locationConfirmed, setLocationConfirmed] = useState(false);
|
|
const [locationsLoading, setLocationsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!doctor?.uuid) return;
|
|
request
|
|
.getBookingLocations(doctor.uuid)
|
|
.then((res) => {
|
|
const d = res?.data?.data ?? res?.data ?? {};
|
|
const list = Array.isArray(d.booking_locations) ? d.booking_locations : [];
|
|
setBookingLocations(list);
|
|
|
|
// آرایه از قبل بر اساس زودترین نوبت آزاد مرتب است؛ دوباره مرتبش نکن.
|
|
const fromUrl = initialClinicUuid
|
|
? list.find((l) => l.clinic_uuid === initialClinicUuid)
|
|
: null;
|
|
setSelectedLocation(fromUrl ?? list[0] ?? null);
|
|
setLocationConfirmed(Boolean(fromUrl) || list.length <= 1);
|
|
})
|
|
.catch(() => setBookingLocations([]))
|
|
.finally(() => setLocationsLoading(false));
|
|
}, [doctor?.uuid, initialClinicUuid]);
|
|
|
|
// روش نوبتدهی و سرویسها per-location هستند: یک پزشک میتواند در مطب شخصی
|
|
// اسلاتی و در کلینیک سرویسی باشد.
|
|
const bookingMode = selectedLocation?.booking_mode === "service" ? "service" : "slot";
|
|
const bookingServices = selectedLocation?.services ?? [];
|
|
|
|
// تعویض محل، انتخابهای وابسته را باطل میکند؛ وگرنه ترکیب سرویسِ یک محل با
|
|
// اسلات محل دیگر ساخته میشود و ثبت نوبت با ۴۲۲ رد میشود.
|
|
const changeLocation = (location) => {
|
|
setSelectedLocation(location);
|
|
setLocationConfirmed(true);
|
|
setSelectedServiceUuids([]);
|
|
setSelectedSlot(null);
|
|
setSelectedDate(null);
|
|
};
|
|
|
|
// بازگشت به مرحلهٔ انتخاب محل
|
|
const reopenLocationChoice = () => setLocationConfirmed(false);
|
|
|
|
useEffect(() => {
|
|
const fetchUserData = async () => {
|
|
setIsLoading(true);
|
|
|
|
const userInfo = Cookies.get("userInfo");
|
|
const parsedData = safeJsonParse(userInfo);
|
|
const usernameFromCookie = parsedData?.username || "";
|
|
|
|
// ابتدا شماره موبایل را از Cookie ست میکنیم
|
|
if (usernameFromCookie) {
|
|
setData(prev => ({
|
|
...prev,
|
|
phone: { value: usernameFromCookie, isEdit: false },
|
|
}));
|
|
}
|
|
|
|
// سپس اگر uuid داریم، بقیه اطلاعات را از API میگیریم
|
|
if (userInfo && parsedData) {
|
|
try {
|
|
const res = await request.getUserProfile(parsedData.uuid);
|
|
const profile = res?.data?.data;
|
|
|
|
if (profile) {
|
|
const newData = buildProfileData(profile, usernameFromCookie);
|
|
setData(newData);
|
|
setPrevData(newData);
|
|
}
|
|
} catch (error) {
|
|
// در صورت خطا، شماره موبایل حفظ میشود و بقیه فیلدها قابل ویرایش میمانند
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
} else {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchUserData();
|
|
}, []); // فقط یک بار در mount اولیه اجرا میشه
|
|
|
|
// useEffect جداگانه برای زمانی که step تغییر میکنه
|
|
useEffect(() => {
|
|
const refetchUserData = async () => {
|
|
if (step === 3) {
|
|
const userInfo = Cookies.get("userInfo");
|
|
const parsedData = safeJsonParse(userInfo);
|
|
const usernameFromCookie = parsedData?.username || "";
|
|
|
|
if (userInfo && parsedData && !data.uuid) {
|
|
try {
|
|
const res = await request.getUserProfile(parsedData.uuid);
|
|
const profile = res?.data?.data;
|
|
|
|
if (profile) {
|
|
const newData = buildProfileData(profile, usernameFromCookie);
|
|
setData(newData);
|
|
setPrevData(newData);
|
|
}
|
|
} catch (error) {
|
|
// در صورت خطا، فرم خالی قابل ویرایش باقی میماند
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
refetchUserData();
|
|
}, [step]);
|
|
|
|
|
|
if (isLoading || locationsLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="w-12 h-12 border-4 border-[#5559CE] border-t-transparent rounded-full animate-spin"></div>
|
|
<p className="text-[#3B3B3B] text-[16px]">در حال بارگذاری...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Container
|
|
step={step}
|
|
data={data}
|
|
doctor={doctor}
|
|
setData={setData}
|
|
setStep={setStep}
|
|
prevData={prevData}
|
|
matchedCity={matchedCity}
|
|
isForAnother={isForAnother}
|
|
setIsForAnother={setIsForAnother}
|
|
disabledDates={disabledDates}
|
|
num={num}
|
|
setNum={setNum}
|
|
uuid={uuid}
|
|
setUuid={setUuid}
|
|
isSendMsg={isSendMsg}
|
|
setIsSendMsg={setIsSendMsg}
|
|
selectedSlot={selectedSlot}
|
|
setSelectedSlot={setSelectedSlot}
|
|
selectedDate={selectedDate}
|
|
setSelectedDate={setSelectedDate}
|
|
appointmentId={appointmentId}
|
|
setAppointmentId={setAppointmentId}
|
|
appointmentExpiresAt={appointmentExpiresAt}
|
|
setAppointmentExpiresAt={setAppointmentExpiresAt}
|
|
bookingMode={bookingMode}
|
|
bookingServices={bookingServices}
|
|
selectedServiceUuids={selectedServiceUuids}
|
|
setSelectedServiceUuids={setSelectedServiceUuids}
|
|
bookingLocations={bookingLocations}
|
|
selectedLocation={selectedLocation}
|
|
locationConfirmed={locationConfirmed}
|
|
changeLocation={changeLocation}
|
|
reopenLocationChoice={reopenLocationChoice}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export default AppointmentPage;
|