feat(booking): show only locations that can actually be booked that day
The site offered a "personal practice" for a doctor who has no personal address at all — the schedule existed but its shifts pointed at the clinic's address, so there was nowhere to go. The backend now filters those out; this consumes the filtered contract and adds the per-day dimension. - getBookingLocations takes an optional date and the appointment page refetches on it, merging available_on_date into the existing list rather than replacing it, so browsing the calendar never resets the user's choice. - The browsed day had to be lifted out of the Date step: selectedDate is only set once a slot is confirmed, far too late to drive availability. - A location closed on the chosen day renders disabled with «در این روز نوبت ندارد», and when every location is closed the step says so instead of showing an empty slot list. If the already-selected location closes, a notice appears with a link back to the picker — silently showing nothing was the failure mode worth avoiding. - Doctor profile: workLocation in the Physician JSON-LD is limited to addresses that appear in booking_locations, since schema.org presents them as places a patient can attend. The address card still lists the others — they are real practice details — tagged «بدون نوبتدهی آنلاین». Verified end-to-end with a temporary unused address on the test doctor: the visible card listed both and tagged the unused one, while workLocation carried only the bookable one. The row was removed afterwards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# نمایش محلهای نوبتدهی فقط بر اساس برنامهٔ همان روز
|
||||
|
||||
## پروژه
|
||||
|
||||
`nobat724_front`
|
||||
|
||||
پرامپت همتای backend که **باید اول اجرا شود**:
|
||||
`clinicpro/.claude/prompt/fix-booking-context-slots-and-phantom-locations.md`
|
||||
|
||||
## زمینه
|
||||
|
||||
صفحهٔ رزرو حالا محل نوبتدهی (مطب شخصی / کلینیک) را از
|
||||
`GET /api/v1/appointment-booking-locations/{doctorUuid}` میگیرد و کاربر یکی را انتخاب میکند.
|
||||
|
||||
اما این فهرست **وضعیت واقعی رزرو در یک روز مشخص** را نشان نمیدهد. برای «دکتر تست»
|
||||
(`bcabb3a8-cae3-45ec-876c-548f9c1e1569`) گزینهٔ «مطب شخصی» نمایش داده میشود، در حالی که در
|
||||
دیتابیس این پزشک **هیچ آدرس شخصی ثبتشدهای ندارد** و شیفت برنامهٔ شخصیاش هم
|
||||
`location_id = NULL` است. یعنی محلی که اصلاً قابل رزرو نیست، به بیمار پیشنهاد میشود.
|
||||
|
||||
## مشکل / هدف
|
||||
|
||||
قاعدهٔ درست نمایش یک محل:
|
||||
|
||||
1. برای آن محل حداقل یک **آدرس ثبتشده** وجود داشته باشد، **و**
|
||||
2. در برنامهٔ کاری، همان آدرس برای شیفتهای آن محل **انتخاب شده** باشد، **و**
|
||||
3. برای **روز انتخابشده** حداقل یک اسلات آزاد داشته باشد.
|
||||
|
||||
اگر هر کدام برقرار نباشد، آن محل نباید بهعنوان گزینهٔ قابل انتخاب نمایش داده شود — نه در
|
||||
`/appointment/[doctorId]` و نه در پروفایل پزشک `/doctor/[slug]`.
|
||||
|
||||
شرطهای ۱ و ۲ در backend اعمال میشوند (پرامپت همتا). این پرامپت شرط ۳ و مصرف درست فهرست را
|
||||
پوشش میدهد.
|
||||
|
||||
## قرارداد API بعد از تغییر backend
|
||||
|
||||
```
|
||||
GET /api/v1/appointment-booking-locations/{doctorUuid}
|
||||
GET /api/v1/appointment-booking-locations/{doctorUuid}?date=YYYY-MM-DD
|
||||
```
|
||||
|
||||
- بدون `date`: فقط محلهای **معتبر** (آدرس دارند و شیفت روی آدرس دارند). محل بدون آدرس دیگر
|
||||
اصلاً برنمیگردد.
|
||||
- با `date`: هر آیتم فیلد `available_on_date` (بولین) میگیرد و پاسخ `date` را echo میکند.
|
||||
- `opening_hours` هر آیتم حالا `day_index` و `location_id` هم دارد.
|
||||
|
||||
بقیهٔ قرارداد بدون تغییر: مرتب بر اساس `next_available_at` صعودی، پاسخ double-nested
|
||||
(`json.data.data`).
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|------|-----|
|
||||
| `services/response.js` | `getBookingLocations` — باید `date` بگیرد |
|
||||
| `components/appointment/index.js` | `AppointmentPage` — نگهدارندهٔ `bookingLocations` و `selectedLocation` |
|
||||
| `components/appointment/Container.js` | مرحلهٔ انتخاب محل / سرویس / تاریخ |
|
||||
| `components/appointment/location/LocationSelect.js` | کارتهای انتخاب محل |
|
||||
| `components/appointment/date/index.js` | مرحلهٔ تاریخ + دکمهٔ «تغییر محل» |
|
||||
| `app/component/date/datePicker/index.js` | تقویم ماه (`getMonthAvailability`) |
|
||||
| `app/doctor/[slug]/page.js` | پروفایل پزشک + JSON-LD |
|
||||
| `components/doctor/detailDoctor/cards/locations/index.js` | کارت «موقعیت مکانی» در پروفایل |
|
||||
|
||||
## وضعیت فعلی
|
||||
|
||||
### فهرست محلها وابسته به روز نیست
|
||||
|
||||
`components/appointment/index.js` — یکبار در mount گرفته میشود و تا آخر ثابت میماند:
|
||||
|
||||
```js
|
||||
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);
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
`selectedDate` بعداً در همین کامپوننت ست میشود ولی هیچوقت به این فراخوانی برنمیگردد.
|
||||
|
||||
### پروفایل پزشک همهٔ آدرسها را نشان میدهد
|
||||
|
||||
`app/doctor/[slug]/page.js` — `workLocation` در JSON-LD و کارت «موقعیت مکانی» از
|
||||
`getDoctorAddresses(doctor.id)` میآیند که **همهٔ** آدرسها را برمیگرداند، مستقل از اینکه در
|
||||
برنامهٔ نوبتدهی استفاده شده باشند یا نه.
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. `date` در لایهٔ سرویس
|
||||
|
||||
`services/response.js`:
|
||||
|
||||
```js
|
||||
getBookingLocations: (doctor_uuid, date = null) =>
|
||||
api.get(`api/v1/appointment-booking-locations/${doctor_uuid}`, {
|
||||
params: date ? { date } : {},
|
||||
...removeTokenHead,
|
||||
}),
|
||||
```
|
||||
|
||||
### ۲. واکشی دوباره با تغییر روز
|
||||
|
||||
در `components/appointment/index.js`:
|
||||
|
||||
- فهرست اولیه بدون `date` گرفته شود (برای مرحلهٔ انتخاب محل، قبل از انتخاب روز).
|
||||
- بعد از انتخاب روز، دوباره با `date` گرفته شود و `available_on_date` روی کارتها اعمال شود.
|
||||
|
||||
`selectedDate` در این کامپوننت **timestamp** است؛ برای پارامتر API باید به `YYYY-MM-DD` تبدیل شود
|
||||
(از `moment-jalaali` که در پروژه هست استفاده کن، همان الگوی
|
||||
`app/component/date/dateTime/index.js` که `moment.unix(date).format("YYYY-MM-DD")` میزند).
|
||||
|
||||
**حالت مرزی مهم:** اگر روزِ انتخابشده محلِ انتخابشده را غیرفعال کند
|
||||
(`available_on_date === false`)، نباید بیصدا اسلات خالی نشان دهی. یا کاربر را به مرحلهٔ انتخاب
|
||||
محل برگردان با پیام روشن، یا خودکار به اولین محلِ باز در آن روز سوییچ کن و این جابهجایی را
|
||||
اطلاع بده. **بیصدا نگهداشتن محلِ بسته = صفحهٔ خالی بدون توضیح.**
|
||||
|
||||
### ۳. UI کارتهای محل
|
||||
|
||||
`components/appointment/location/LocationSelect.js`:
|
||||
|
||||
- وقتی `available_on_date === false`، کارت غیرفعال شود (`disabled`، `cursor-not-allowed`،
|
||||
کمرنگ) با برچسب «در این روز نوبت ندارد».
|
||||
- کارت غیرفعال قابل کلیک نباشد.
|
||||
- اگر **هیچ** محلی در آن روز باز نبود، پیام روشن بده و کاربر را به انتخاب روز دیگر هدایت کن.
|
||||
|
||||
از تم و کامپوننتهای موجود استفاده کن (MUI v5 + Tailwind، RTL، فونت Vazir) — طراحی جدید نساز.
|
||||
|
||||
### ۴. تقویم ماه هم per-location است
|
||||
|
||||
`app/component/date/datePicker/index.js` الان `clinicUuid` میگیرد و `getMonthAvailability` را با
|
||||
آن صدا میزند — این درست است و نیازی به تغییر ندارد. فقط مطمئن شو بعد از سوییچ محل، کش ماهها
|
||||
پاک میشود (همان effect موجود روی `clinicUuid`).
|
||||
|
||||
### ۵. پروفایل پزشک — فقط محلهای قابل رزرو
|
||||
|
||||
در `app/doctor/[slug]/page.js`:
|
||||
|
||||
- `getBookingLocations` (بدون `date`) را server-side بگیر — همین الان برای `availableService` و
|
||||
`openingHoursSpecification` گرفته میشود.
|
||||
- کارت «موقعیت مکانی» (`components/doctor/detailDoctor/cards/locations/`) و `workLocation` در
|
||||
JSON-LD باید **فقط** آدرسهایی را نشان دهند که `location_uuid` آنها در `booking_locations`
|
||||
آمده است.
|
||||
|
||||
```js
|
||||
const bookableUuids = new Set(
|
||||
bookingLocations.map((l) => l.location_uuid).filter(Boolean)
|
||||
);
|
||||
const bookableAddresses = addresses.filter((a) => bookableUuids.has(a.uuid));
|
||||
```
|
||||
|
||||
- **تصمیم لازم:** آدرسی که ثبت شده ولی در هیچ برنامهای استفاده نشده، اطلاعات واقعی مطب است و
|
||||
حذف کاملش از پروفایل ممکن است خواسته نباشد. پیشنهاد: در کارت «موقعیت مکانی» نمایش داده شود
|
||||
ولی بدون دکمهٔ «دریافت نوبت»، و در JSON-LD **نیاید** (چون schema.org آن را قابل مراجعه اعلام
|
||||
میکند). اگر تصمیم دیگری گرفتی، در PR بنویس.
|
||||
|
||||
### ۶. لینک CTA
|
||||
|
||||
`components/doctor/appointmentList/index.js` — اگر پروفایل محل مشخصی را برجسته کرد، CTA همان را
|
||||
حمل کند: `/appointment/${doctorSlug}?clinic_uuid=${clinicUuid}`. اگر محلی برجسته نشده، پارامتر
|
||||
ندهد تا صفحهٔ رزرو خودش انتخابگر را نشان دهد (رفتار فعلی، درست است).
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **backend اول.** تا وقتی فیلتر سمت سرور اعمال نشده، «مطب شخصی» جعلی همچنان برمیگردد و کار
|
||||
فرانت قابل تست نیست.
|
||||
- پاسخ double-nested: `json?.data?.data ?? json?.data`.
|
||||
- endpoint عمومی است و `removeTokenHead` میگیرد؛ برای رزرو نهایی `{ requireAuth: true }`.
|
||||
- تاریخها شمسی با `jalali-moment` / `moment-jalaali`؛ رشتههای جدید فارسی؛ RTL.
|
||||
- slug پزشک = `uuid`.
|
||||
- هر صفحه `generateMetadata` صادر کند و `params` همیشه `await` شود.
|
||||
- تست دستی: «دکتر تست» (`bcabb3a8-cae3-45ec-876c-548f9c1e1569`) — بعد از فیلتر backend نباید هیچ
|
||||
گزینهٔ «مطب شخصی» ببیند، فقط کلینیک «علی بهروزی». روزی که کلینیک شیفت ندارد هم باید محل را
|
||||
غیرفعال نشان دهد.
|
||||
- بعد از تغییرات: `npm run lint` و `npm run build` هر دو سبز.
|
||||
@@ -129,6 +129,14 @@ async function Doctor({ params }) {
|
||||
? await Promise.all([getDoctorAddresses(doctor.id), getBookingLocations(doctor.uuid)])
|
||||
: [[], []];
|
||||
|
||||
// آدرسی که در هیچ برنامهٔ نوبتدهی استفاده نشده، محل مراجعه نیست. در کارت
|
||||
// «موقعیت مکانی» میماند (اطلاعات واقعی مطب است) ولی به JSON-LD نمیرود، چون
|
||||
// schema.org آن را محلی قابلمراجعه اعلام میکند.
|
||||
const bookableAddressUuids = new Set(
|
||||
bookingLocations.map((l) => l.location_uuid).filter(Boolean)
|
||||
);
|
||||
const bookableAddresses = addresses.filter((a) => bookableAddressUuids.has(a.uuid));
|
||||
|
||||
// ساعات کاری per-location است؛ با uuid آدرس به هر محل وصل میشود.
|
||||
const openingHoursByLocation = bookingLocations.reduce((acc, location) => {
|
||||
if (location.location_uuid && location.opening_hours?.length) {
|
||||
@@ -191,8 +199,8 @@ async function Doctor({ params }) {
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
...(addresses.length > 0 && {
|
||||
workLocation: addresses.map((addr) => ({
|
||||
...(bookableAddresses.length > 0 && {
|
||||
workLocation: bookableAddresses.map((addr) => ({
|
||||
"@type": "MedicalClinic",
|
||||
name: addr.clinic_name || addr.name || `مطب دکتر ${doctor.name}`,
|
||||
address: {
|
||||
@@ -258,6 +266,7 @@ async function Doctor({ params }) {
|
||||
comments={comments}
|
||||
rateAggregate={rateAggregate}
|
||||
addresses={addresses}
|
||||
bookableAddressUuids={[...bookableAddressUuids]}
|
||||
slug={slug}
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
@@ -51,6 +51,8 @@ function Container({
|
||||
locationConfirmed,
|
||||
changeLocation,
|
||||
reopenLocationChoice,
|
||||
onDateChange,
|
||||
selectedClosedOnDate,
|
||||
}) {
|
||||
const serviceMode = bookingMode === "service";
|
||||
const clinicUuid = selectedLocation?.clinic_uuid ?? null;
|
||||
@@ -92,6 +94,8 @@ function Container({
|
||||
selectedLocation={selectedLocation}
|
||||
clinicUuid={clinicUuid}
|
||||
onChangeLocation={multiLocation ? reopenLocationChoice : null}
|
||||
onDateChange={onDateChange}
|
||||
closedOnDate={selectedClosedOnDate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,16 @@ function Date({
|
||||
selectedLocation = null,
|
||||
clinicUuid = null,
|
||||
onChangeLocation,
|
||||
onDateChange,
|
||||
closedOnDate = false,
|
||||
}) {
|
||||
const [date, setDate] = useState();
|
||||
|
||||
const pickDate = (value) => {
|
||||
setDate(value);
|
||||
onDateChange?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full lg:w-[61%]">
|
||||
<div
|
||||
@@ -44,8 +51,24 @@ function Date({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{closedOnDate && (
|
||||
<div className="mb-3 rounded-[8px] border border-solid border-[#F17732] bg-[rgba(241,119,50,0.08)] p-[12px]">
|
||||
<p className="text-[13px] text-[#B4541F] leading-relaxed">
|
||||
«{selectedLocation?.title}» در روز انتخابشده نوبتدهی ندارد.
|
||||
{onChangeLocation && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChangeLocation}
|
||||
className="mr-1 font-medium text-[#5559CE] hover:underline"
|
||||
>
|
||||
انتخاب محل دیگر
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Time
|
||||
setDate={setDate}
|
||||
setDate={pickDate}
|
||||
isStep
|
||||
disabledDates={disabledDates}
|
||||
clinicUuid={clinicUuid}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import moment from "moment-jalaali";
|
||||
import { request } from "@/services/response";
|
||||
import Cookies from "js-cookie";
|
||||
import Container from "./Container";
|
||||
@@ -69,6 +70,9 @@ function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid
|
||||
const [selectedLocation, setSelectedLocation] = useState(null);
|
||||
const [locationConfirmed, setLocationConfirmed] = useState(false);
|
||||
const [locationsLoading, setLocationsLoading] = useState(true);
|
||||
// روزی که کاربر در تقویم مرور میکند (timestamp) — با selectedDate فرق دارد،
|
||||
// که فقط پس از تأیید اسلات ست میشود.
|
||||
const [browsingDate, setBrowsingDate] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!doctor?.uuid) return;
|
||||
@@ -90,6 +94,32 @@ function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid
|
||||
.finally(() => setLocationsLoading(false));
|
||||
}, [doctor?.uuid, initialClinicUuid]);
|
||||
|
||||
// با تغییر روز، فقط پرچم available_on_date تازه میشود — انتخاب کاربر دستنخورده
|
||||
// میماند، وگرنه هر بار جابهجایی در تقویم مرحله را ریست میکرد.
|
||||
useEffect(() => {
|
||||
if (!doctor?.uuid || !browsingDate) return;
|
||||
|
||||
const date = moment.unix(browsingDate).format("YYYY-MM-DD");
|
||||
request
|
||||
.getBookingLocations(doctor.uuid, date)
|
||||
.then((res) => {
|
||||
const d = res?.data?.data ?? res?.data ?? {};
|
||||
const byKey = new Map(
|
||||
(Array.isArray(d.booking_locations) ? d.booking_locations : []).map((l) => [
|
||||
l.clinic_uuid ?? "personal",
|
||||
l.available_on_date,
|
||||
])
|
||||
);
|
||||
setBookingLocations((prev) =>
|
||||
prev.map((l) => ({
|
||||
...l,
|
||||
available_on_date: byKey.get(l.clinic_uuid ?? "personal") ?? false,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [doctor?.uuid, browsingDate]);
|
||||
|
||||
// روش نوبتدهی و سرویسها per-location هستند: یک پزشک میتواند در مطب شخصی
|
||||
// اسلاتی و در کلینیک سرویسی باشد.
|
||||
const bookingMode = selectedLocation?.booking_mode === "service" ? "service" : "slot";
|
||||
@@ -108,6 +138,10 @@ function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid
|
||||
// بازگشت به مرحلهٔ انتخاب محل
|
||||
const reopenLocationChoice = () => setLocationConfirmed(false);
|
||||
|
||||
// محلِ انتخابشده در روزِ در حال مرور بسته است. نباید بیصدا فهرست خالی نشان داد.
|
||||
const selectedClosedOnDate =
|
||||
Boolean(browsingDate) && selectedLocation?.available_on_date === false;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserData = async () => {
|
||||
setIsLoading(true);
|
||||
@@ -223,6 +257,8 @@ function AppointmentPage({ doctor, disabledDates, matchedCity, initialClinicUuid
|
||||
locationConfirmed={locationConfirmed}
|
||||
changeLocation={changeLocation}
|
||||
reopenLocationChoice={reopenLocationChoice}
|
||||
onDateChange={setBrowsingDate}
|
||||
selectedClosedOnDate={selectedClosedOnDate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,26 +14,39 @@ function nextAvailableLabel(timestamp) {
|
||||
* درست است و اینجا دوباره مرتب نمیشود.
|
||||
*/
|
||||
function LocationSelect({ locations = [], selected, onSelect }) {
|
||||
const anyOpen = locations.some((l) => l.available_on_date !== false);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[520px] mx-auto">
|
||||
<h2 className="text-[16px] font-bold text-[#3B3B3B] mb-4">۱. انتخاب محل نوبتدهی</h2>
|
||||
|
||||
{!anyOpen && locations.length > 0 && (
|
||||
<p className="mb-3 text-[13px] text-[#B4541F]">
|
||||
در روز انتخابشده هیچکدام از محلها نوبتدهی ندارند. روز دیگری را انتخاب کنید.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{locations.map((location) => {
|
||||
const key = location.clinic_uuid ?? location.location_uuid ?? "personal";
|
||||
const active =
|
||||
(selected?.clinic_uuid ?? null) === (location.clinic_uuid ?? null);
|
||||
const nextLabel = nextAvailableLabel(location.next_available_at);
|
||||
// null یعنی روزی انتخاب نشده؛ فقط false به معنی «این روز بسته است».
|
||||
const closed = location.available_on_date === false;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => onSelect(location)}
|
||||
disabled={closed}
|
||||
onClick={() => !closed && onSelect(location)}
|
||||
className={`w-full text-right p-[16px] rounded-[8px] border border-solid transition-colors ${
|
||||
active
|
||||
? "border-[#5559CE] bg-[rgba(85,89,206,0.06)]"
|
||||
: "border-[#EFEFEF] bg-[#FFF] hover:border-[#C7C9EC]"
|
||||
closed
|
||||
? "border-[#EFEFEF] bg-[#FAFAFA] opacity-60 cursor-not-allowed"
|
||||
: active
|
||||
? "border-[#5559CE] bg-[rgba(85,89,206,0.06)]"
|
||||
: "border-[#EFEFEF] bg-[#FFF] hover:border-[#C7C9EC]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-[8px]">
|
||||
@@ -55,10 +68,12 @@ function LocationSelect({ locations = [], selected, onSelect }) {
|
||||
|
||||
<p
|
||||
className={`mt-[8px] text-[12px] ${
|
||||
nextLabel ? "text-[#009D79]" : "text-[#A1A1A1]"
|
||||
closed ? "text-[#B4541F]" : nextLabel ? "text-[#009D79]" : "text-[#A1A1A1]"
|
||||
}`}
|
||||
>
|
||||
{nextLabel ?? "فعلاً نوبت خالی ندارد"}
|
||||
{closed
|
||||
? "در این روز نوبت ندارد"
|
||||
: nextLabel ?? "فعلاً نوبت خالی ندارد"}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import RoutingC from "@/components/icons/RoutingC";
|
||||
// Leaflet به window نیاز دارد → فقط کلاینت
|
||||
const MapView = dynamic(() => import("./MapView"), { ssr: false });
|
||||
|
||||
function Item({ data }) {
|
||||
function Item({ data, bookable = true }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -29,7 +29,14 @@ function Item({ data }) {
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="!px-[2px] !py-[4px] !flex !items-center !justify-between !w-full"
|
||||
>
|
||||
<p className="text-[#616161] text-[16px] font-medium">{data.name}</p>
|
||||
<p className="text-[#616161] text-[16px] font-medium">
|
||||
{data.name}
|
||||
{!bookable && (
|
||||
<span className="mr-2 text-[11px] font-normal text-[#A1A1A1]">
|
||||
(بدون نوبتدهی آنلاین)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<div
|
||||
className={` transition-all duration-500 ${isOpen ? "rotate-180" : "rotate-0"} `}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import CustomLoading from "@/app/component/loading/Custom";
|
||||
import Item from "./Item";
|
||||
|
||||
function Locations({ addresses }) {
|
||||
function Locations({ addresses, bookableAddressUuids = [] }) {
|
||||
if (!addresses?.length) return null;
|
||||
|
||||
const bookable = new Set(bookableAddressUuids);
|
||||
return (
|
||||
<div>
|
||||
<p className="text-[#525252] text-[16px] md:text-[18px] lg:text-[20px] font-bold">
|
||||
@@ -12,7 +14,7 @@ function Locations({ addresses }) {
|
||||
{addresses.map((item, idx) => (
|
||||
<CustomLoading key={idx} width="full" height={40}>
|
||||
<li className="w-full">
|
||||
<Item data={item} />
|
||||
<Item data={item} bookable={bookable.has(item.uuid)} />
|
||||
{addresses.length > idx + 1 && (
|
||||
<span className="w-full h-px bg-[#EFEFEF] block my-[12px] md:my-[14px] lg:my-[16px]"></span>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import AboutDcotor from "./cards/AboutDcotor";
|
||||
import Comments from "./cards/comments";
|
||||
import Locations from "./cards/locations";
|
||||
|
||||
function DetailDoctor({ doctor, comments, rateAggregate, addresses }) {
|
||||
function DetailDoctor({ doctor, comments, rateAggregate, addresses, bookableAddressUuids = [] }) {
|
||||
return (
|
||||
<div className="w-full lg:w-[59%]">
|
||||
<div className="hidden lg:flex">
|
||||
@@ -14,7 +14,7 @@ function DetailDoctor({ doctor, comments, rateAggregate, addresses }) {
|
||||
<Title doctor={doctor} />
|
||||
<Link />
|
||||
<AboutDcotor doctor={doctor} />
|
||||
<Locations addresses={addresses} />
|
||||
<Locations addresses={addresses} bookableAddressUuids={bookableAddressUuids} />
|
||||
<Comments
|
||||
doctor={doctor}
|
||||
comments={comments}
|
||||
|
||||
@@ -5,7 +5,7 @@ import DetailDoctor from "./detailDoctor";
|
||||
import Share from "./detailDoctor/Share";
|
||||
import CustomLoading from "@/app/component/loading/Custom";
|
||||
|
||||
function DoctorPage({ doctor, comments, rateAggregate, addresses, slug }) {
|
||||
function DoctorPage({ doctor, comments, rateAggregate, addresses, bookableAddressUuids = [], slug }) {
|
||||
return (
|
||||
<div className="pt-[92px] sm:pt-[120px] mt:pt-[148px] lg:pt-[176px] mt-[px] padding-responsive">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -39,6 +39,7 @@ function DoctorPage({ doctor, comments, rateAggregate, addresses, slug }) {
|
||||
comments={comments}
|
||||
rateAggregate={rateAggregate}
|
||||
addresses={addresses}
|
||||
bookableAddressUuids={bookableAddressUuids}
|
||||
/>
|
||||
<AppointmentList doctor={doctor} doctorSlug={slug} />
|
||||
</div>
|
||||
|
||||
@@ -57,8 +57,11 @@ export const request = {
|
||||
api.patch(`api/v1/appointment-settings/weekly-schedule/${uuid}`),
|
||||
deleteAppointmentWeeklySchedule: (uuid) =>
|
||||
api.delete(`api/v1/appointment-settings/weekly-schedule/${uuid}`),
|
||||
getBookingLocations: (doctor_uuid) =>
|
||||
api.get(`api/v1/appointment-booking-locations/${doctor_uuid}`, removeTokenHead),
|
||||
getBookingLocations: (doctor_uuid, date = null) =>
|
||||
api.get(`api/v1/appointment-booking-locations/${doctor_uuid}`, {
|
||||
params: date ? { date } : {},
|
||||
...removeTokenHead,
|
||||
}),
|
||||
getAppointmentSlots: (doctor_uuid, date, clinic_uuid = null) =>
|
||||
api.get(
|
||||
`api/v1/appointment-slots?doctor_uuid=${doctor_uuid}&date=${date}` +
|
||||
|
||||
Reference in New Issue
Block a user