fix(doctor): derive booking state from booking_locations

The profile decided "نوبت‌دهی غیرفعال است" from `doctor.active` alone, while the
page already had `booking_locations` — the more precise source, since the backend
only returns locations that are genuinely bookable. The two could disagree, and
for a doctor bookable only at a clinic they did.

A shared `bookingState` helper now drives both the desktop card and the mobile
bar: any location means bookable, the label comes from the earliest
`next_available_at`, and locations with no capacity yet read as "فعلاً نوبت خالی
ندارد" rather than disabled. With no locations at all it falls back to the
previous `doctor.active` / `free_turn` fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-18 16:25:37 +03:30
co-authored by Claude Fable 5
parent dfcf62735b
commit 36816eded2
6 changed files with 204 additions and 13 deletions
@@ -5,9 +5,11 @@ import GreenCalendarTurn from "@/components/icons/GreenCalendarTurn";
import { Button } from "@mui/material";
import Link from "next/link";
import DoctorAvatar from "@/app/component/DoctorAvatar";
import { bookingState } from "./bookingState";
function ItemAppointment({ turn, loading, doctorSlug }) {
const bookingDisabled = !loading && turn?.active === false;
function ItemAppointment({ turn, loading, doctorSlug, booking }) {
const state = booking ?? bookingState(turn, []);
const bookingDisabled = !loading && !state.enabled;
return (
<li
key={turn.id}
@@ -39,9 +41,7 @@ function ItemAppointment({ turn, loading, doctorSlug }) {
<GreenCalendarTurn />
<TextLoading width={100} height={15} loading={loading}>
<p className="text-[#05BA58] text-[12px] font-medium">
{bookingDisabled || !turn?.free_turn
? "نوبت‌دهی غیرفعال است"
: `اولین نوبت آزاد: ${turn.free_turn}`}
{state.label}
</p>
</TextLoading>
</div>
@@ -0,0 +1,53 @@
import moment from "moment-jalaali";
const DAY_NAMES = [
"یکشنبه",
"دوشنبه",
"سه‌شنبه",
"چهارشنبه",
"پنجشنبه",
"جمعه",
"شنبه",
];
const formatJalali = (ts) => {
const m = moment.unix(ts);
return `${DAY_NAMES[m.day()]} ${m.format("HH:mm")}`;
};
/**
* قاعدهٔ واحد فعال/غیرفعال بودن نوبت‌دهی در پروفایل پزشک.
*
* booking_locations دقیق‌تر از doctor.active است: backend فقط محل‌های واقعاً
* قابل‌رزرو را برمی‌گرداند. تا وقتی حداقل یک محل هست، نوبت‌دهی فعال است — حتی
* اگر فیلد قدیمی active به هر دلیل false باشد. بدون محل، همان فیلدهای doctor
* ملاک می‌مانند (سازگاری با پزشک بدون برنامه).
*/
export function bookingState(doctor, bookingLocations) {
const hasLocations =
Array.isArray(bookingLocations) && bookingLocations.length > 0;
if (!hasLocations) {
const enabled = doctor?.active !== false && !!doctor?.free_turn;
return {
enabled,
label: enabled
? `اولین نوبت آزاد: ${doctor.free_turn}`
: "نوبت‌دهی غیرفعال است",
};
}
const earliest =
bookingLocations
.map((l) => l.next_available_at)
.filter((t) => t != null)
.sort((a, b) => a - b)[0] ?? null;
return {
enabled: true,
label:
earliest != null
? `اولین نوبت آزاد: ${formatJalali(earliest)}`
: "فعلاً نوبت خالی ندارد",
};
}
+12 -6
View File
@@ -4,24 +4,30 @@ import { Button } from "@mui/material";
import ItemAppointment from "./ItemAppointment";
import ArrowLeftD from "@/components/icons/ArrowLeftD";
import Link from "next/link";
import { bookingState } from "./bookingState";
function AppointmentList({ doctor, loading, doctorSlug }) {
function AppointmentList({ doctor, loading, doctorSlug, bookingLocations = [] }) {
const booking = bookingState(doctor, bookingLocations);
return (
<>
<ul className="hidden sticky top-[122px] sm:top-[150px] mt:top-[178px] lg:top-[206px] lg:flex w-[38%] flex-col justify-start items-center gap-8">
{doctor && (
<ItemAppointment loading={loading} turn={doctor} key={doctor.id} doctorSlug={doctorSlug} />
<ItemAppointment
loading={loading}
turn={doctor}
key={doctor.id}
doctorSlug={doctorSlug}
booking={booking}
/>
)}
</ul>
<div
className="fixed lg:hidden bg-[#FFF] border-t border-transparent border-t-[#D7D7D7] border-solid bottom-0 right-0 w-full p-4 flex items-center justify-between"
>
<p className="text-[#05BA58] text-[11px] font-normal">
{doctor?.active === false || !doctor?.free_turn
? "نوبت‌دهی غیرفعال است"
: `اولین نوبت آزاد: ${doctor.free_turn}`}
{booking.label}
</p>
{doctor?.active === false ? (
{!booking.enabled ? (
<Button
variant="contained"
className="!py-2 !px-4 gap-1 !text-[14px] !font-medium"
+2 -2
View File
@@ -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, bookableAddressUuids = [], slug }) {
function DoctorPage({ doctor, comments, rateAggregate, addresses, bookableAddressUuids = [], bookingLocations = [], 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">
@@ -41,7 +41,7 @@ function DoctorPage({ doctor, comments, rateAggregate, addresses, bookableAddres
addresses={addresses}
bookableAddressUuids={bookableAddressUuids}
/>
<AppointmentList doctor={doctor} doctorSlug={slug} />
<AppointmentList doctor={doctor} doctorSlug={slug} bookingLocations={bookingLocations} />
</div>
<ClaimProfileSection doctor={doctor} />
</div>