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.
This commit is contained in:
hamed
2026-08-09 10:45:52 +03:30
parent 1668ef8890
commit 54bebbd41a
23 changed files with 1094 additions and 32 deletions
+84
View File
@@ -0,0 +1,84 @@
"use client";
const MAX_PREVIEW_SERVICES = 3;
/**
* زیرنویس کارت: نام سرویس‌ها تا سه‌تا و بعد «+n».
*
* شمارِ خالی («۳ سرویس») به بیمار نمی‌گوید این دستگاه به کارش می‌آید یا نه، و او را
* مجبور می‌کند کورکورانه کلیک کند.
*/
function servicesLabel(services = []) {
if (services.length === 0) return null;
const shown = services.slice(0, MAX_PREVIEW_SERVICES).map((s) => s.name);
const rest = services.length - shown.length;
return rest > 0 ? `${shown.join("، ")} +${rest}` : shown.join("، ");
}
/**
* انتخاب نوع نوبت — پیش از انتخاب سرویس و روز، و فقط وقتی این پزشک در این محل
* دست‌کم یک منبعِ قابل رزرو دارد.
*
* «منبع» واژهٔ داخلی است و به بیمار نشان داده نمی‌شود؛ کارت‌ها نام واقعی دستگاه/اتاق
* و نوعشان را نشان می‌دهند. کارت اول همیشه جریان همیشگی است: نوبت با خودِ پزشک.
*/
function ResourceSelect({ resources = [], selected = null, onSelect, allowDoctorOption = true }) {
const cardClass = (active) =>
`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]"
}`;
return (
<div className="w-full max-w-[520px] mx-auto">
<h2 className="text-[16px] font-bold text-[#3B3B3B] mb-4">۱. انتخاب نوع نوبت</h2>
<div className="flex flex-col gap-2">
{/* پزشکی که برنامهٔ هفتگی ندارد، ویزیتِ مستقیم هم ندارد؛ نشان‌دادن این کارت
کاربر را به فهرست ساعتِ همیشه‌خالی می‌برد. */}
{allowDoctorOption && (
<button type="button" onClick={() => onSelect(null)} className={cardClass(selected === null)}>
<p className="text-[14px] md:text-[16px] font-bold text-[#3B3B3B]">نوبت با پزشک</p>
<p className="mt-[6px] text-[13px] text-[#616161] font-normal">
ویزیت معمول در ساعتهای کاری پزشک
</p>
</button>
)}
{resources.map((resource) => {
const active = selected?.uuid === resource.uuid;
const label = servicesLabel(resource.services);
return (
<button
key={resource.uuid}
type="button"
onClick={() => onSelect(resource)}
className={cardClass(active)}
>
<div className="flex items-center justify-between gap-[8px]">
<p className="text-[14px] md:text-[16px] font-bold text-[#3B3B3B]">
{resource.name}
</p>
{resource.type?.name && (
<span className="text-[11px] text-[#5559CE] bg-[rgba(85,89,206,0.10)] rounded-[4px] px-[6px] py-[2px] shrink-0">
{resource.type.name}
</span>
)}
</div>
{label && (
<p className="mt-[6px] text-[13px] text-[#616161] font-normal">{label}</p>
)}
</button>
);
})}
</div>
</div>
);
}
export default ResourceSelect;