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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user