diff --git a/.claude/prompt/fix-public-booking-state-and-admin-context-slots.md b/.claude/prompt/fix-public-booking-state-and-admin-context-slots.md new file mode 100644 index 00000000..0bf5a65a --- /dev/null +++ b/.claude/prompt/fix-public-booking-state-and-admin-context-slots.md @@ -0,0 +1,220 @@ +# وضعیت نوبت‌دهی عمومی از همهٔ برنامه‌ها + context درست اسلات‌ها در پنل + +## پروژه + +`clinicpro` (Backend + پنل ادمین React) + +پرامپت همتا در سایت عمومی: `nobat724_front/.claude/prompt/doctor-profile-booking-state-from-locations.md` +(**backend اول اجرا شود** — فیلدهای `active` / `free_turn` پاسخ عمومی تغییر می‌کنند.) + +## زمینه — نتیجهٔ عیب‌یابی واقعی (curl + دیتابیس) + +سه علامت گزارش‌شده دوباره بررسی شد. **موتور اسلات سالم است** — هر سه علامت از این است که +«چه کسی، با چه contextی می‌پرسد». شواهد: + +``` +# دادهٔ دیتابیس — دکتر تست (doctor_id=3341, uuid bcabb3a8-…)، کلینیک 1003 (41e325c4-…): +weekly_schedules: + 2505 clinic_id=NULL setting=[{"sessions":[{"active":false,…}]}] ← شخصی، غیرفعال، فرمت لیستِ legacy + 2509 clinic_id=1003 روزهای 0..4 فعال 09:00–13:00، location_id=2631، meta.online_booking_enabled=true + +doctor_addresses: 2631 → type=clinic, clinic_id=1003 ✓ + +# تست مستقیم API (1405/04/27 = 2026-07-18): +GET /api/v1/appointment-slots?doctor_uuid=bcabb3a8…&date=2026-07-18&clinic_uuid=41e325c4… + → sessions پر ✓ +GET /api/v1/appointment-slots?doctor_uuid=bcabb3a8…&date=2026-07-18 (بدون clinic_uuid) + → sessions=[] , empty_reason="day_off" ← برنامهٔ شخصیِ 2505 خوانده می‌شود +GET /api/v1/appointment-booking-locations/bcabb3a8… + → یک محل کلینیکی معتبر با opening_hours و next_available_at ✓ +GET /api/v1/appointment-slots?doctor_uuid=&… + → 404 «دکتر یافت نشد» +``` + +## مشکل / هدف + +### علامت ۱ — سایت عمومی: «نوبت‌دهی غیرفعال است» برای پزشکی که نوبت‌دهی فعال دارد + +`GET /api/v1/doctor/{uuid}` فیلدهای `active` / `free_turn` / `hours_of_work` را از +`scheduleRepo->findByDoctor($doctor)` می‌سازد که **فقط برنامهٔ شخصی** (`clinic_id IS NULL`) +است. دکتر تست برنامهٔ شخصیِ غیرفعال دارد و برنامهٔ کلینیکش دیده نمی‌شود → +`active=false` → سایت «نوبت‌دهی غیرفعال است» نشان می‌دهد. + +### علامت ۲ — پنل با کاربر ادمین: «این روز شیفت کاری ندارد» + +`useClinicContext()` برای `primaryRole === 'admin'` مقدار `null` برمی‌گرداند (ادمین context +کلینیکی ندارد) → اسلات‌ها بدون `clinic_uuid` گرفته می‌شوند → برنامهٔ شخصیِ 2505 → `day_off`. + +### علامت ۳ — پزشک دعوت‌شده در محیط کلینیک: همان پیام + +`AppointmentsPage.tsx:341` مقدار اولیهٔ پزشکِ انتخاب‌شده را از `dbUuid` می‌گیرد؛ برای پزشک +دعوت‌شده در محیط کلینیک، `dbUuid` **uuid کلینیک** است نه پزشک → درخواست +`appointment-slots?doctor_uuid=` → 404 «دکتر یافت نشد» → و چون `TurnsTimeline` +هر حالت ناشناخته/خطا را به `day_off` ترجمه می‌کند، پیام «این روز شیفت کاری ندارد» دیده می‌شود. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/Doctor/Entity/Doctor.php:417-432` | `computeScheduleFields(?WeeklySchedule)` — تک‌برنامه‌ای | +| `src/Doctor/Entity/Doctor.php:512-533` | `toListArray` / `toDetailArray` مصرف‌کننده | +| `src/Doctor/Controller/DoctorController.php:126,179,204,351` | `findByDoctor` (فقط شخصی) | +| `src/Doctor/Controller/DoctorController.php:259-265` | `/api/v1/doctors` — map با overwrite دلبخواهی | +| `src/Appointment/Repository/WeeklyScheduleRepository.php:40` | `findAllByDoctor` (همهٔ contextها) | +| `src/Appointment/Service/SlotCalculatorService.php:182` | `findNextAvailableStart` per-context | +| `assets/admin/pages/AppointmentsPage.tsx:341` | `selectedDoctorUuid` از `dbUuid` | +| `assets/admin/pages/AppointmentsPage.tsx:425-433` | slots query (خودش درست است) | +| `assets/admin/hooks/useClinicContext.ts` | برای admin مقدار null | +| `assets/admin/components/appointments/TurnsTimeline.tsx:149-185` | fallback به `day_off` | +| `assets/admin/stores/authStore.ts` | `doctorUuid` (از `context.doctor_uuid` پر می‌شود) | + +## وضعیت فعلی + +### ۱. فیلدهای عمومی فقط از برنامهٔ شخصی + +`src/Doctor/Controller/DoctorController.php:179` (GET عمومی) و `:351` (PATCH): + +```php +$schedule = $this->scheduleRepo->findByDoctor($doctor); // @deprecated — فقط clinic IS NULL +return $this->success(['data' => array_merge($doctor->toDetailArray($schedule), [... +``` + +`/api/v1/doctors` (`:259-265`) — `findByDoctors` **همهٔ** برنامه‌ها (شخصی + کلینیک) را +برمی‌گرداند و map با overwrite، برنامهٔ «آخری» را نگه می‌دارد — نتیجه دلبخواهی است: + +```php +$scheduleMap = []; +foreach ($this->scheduleRepo->findByDoctors($result['items']) as $schedule) { + $scheduleMap[$schedule->getDoctor()->getId()] = $schedule; // آخری برنده می‌شود +} +``` + +### ۲. انتخاب پزشک در پنل از dbUuid + +`assets/admin/pages/AppointmentsPage.tsx:341`: + +```tsx +const [selectedDoctorUuid, setSelectedDoctorUuid] = useState(isDoctor && dbUuid ? dbUuid : ''); +``` + +برای پزشک دعوت‌شده در محیط کلینیک (`context = {type:'clinic', role:'doctor', scope:'clinic'}`)، +`primaryRole='doctor'` و `dbUuid` = uuid **کلینیک** است. `authStore.doctorUuid` +(از `context.doctor_uuid`) uuid درستِ پزشک را دارد و استفاده نمی‌شود. + +### ۳. TurnsTimeline خطا را «روز بدون شیفت» نشان می‌دهد + +`assets/admin/components/appointments/TurnsTimeline.tsx:180`: + +```tsx +if (!slots.length) { + const reason = EMPTY_REASON_TEXT[emptyReason ?? ''] ?? EMPTY_REASON_TEXT.day_off; +``` + +پاسخ 404، خطای شبکه، یا هر `empty_reason` ناشناخته → همیشه «این روز شیفت کاری ندارد». + +## وظایف + +### ۱. تجمیع وضعیت نوبت‌دهی عمومی از همهٔ برنامه‌ها + +`Doctor::computeScheduleFields` آرایه‌ای از برنامه‌ها بگیرد (امضای جدید: +`computeScheduleFields(WeeklySchedule[] $schedules)`؛ null-tolerant برای سازگاری): + +قواعد تجمیع: + +- **`has_schedule` / `active`**: حداقل یک برنامه (در هر context) که هم روز فعال دارد و هم + `meta.online_booking_enabled === true` → true. برنامهٔ شخصیِ خاموش نباید برنامهٔ کلینیکی + روشن را بپوشاند. +- **`free_turn`**: نزدیک‌ترین روز/ساعت در بین **همهٔ** برنامه‌های فعال (همان حلقهٔ فعلی + `computeScheduleParts`، اجراشده روی هر برنامه، سپس min بر اساس فاصلهٔ روز ایرانی). +- **`hours_of_work`**: از همان برنامه‌ای که `free_turn` را داد ساخته شود (ترکیب ساعت‌های دو + محل در یک رشته گمراه‌کننده است). اگر تصمیم دیگری گرفتی در PR توضیح بده. + +سپس چهار call site در `DoctorController` (`:126`، `:179`، `:204`، `:351`) از +`findAllByDoctor($doctor)` استفاده کنند و `/api/v1/doctors` (`:259-265`) map را به +`array` تبدیل کند (`findByDoctors` از قبل همه را می‌آورد — +فقط دیگر overwrite نکن). + +**نکته:** `APPOINTMENT_DISABLED_LABEL` وقتی برگردد که **همهٔ** برنامه‌ها +`online_booking_enabled=false` باشند، نه فقط اولین برنامه (`Doctor.php:423`). + +### ۲. uuid درست پزشک در AppointmentsPage + +```tsx +const doctorUuid = useAuthStore(s => s.doctorUuid); // از context.doctor_uuid +const [selectedDoctorUuid, setSelectedDoctorUuid] = useState( + isDoctor ? (doctorUuid ?? '') : '' +); +``` + +`dbUuid` فقط وقتی uuid پزشک است که `context.type === 'doctor'`؛ به آن اتکا نکن. بررسی کن +`NewAppointmentDrawer` و بقیهٔ مصرف‌کننده‌های `selectedDoctorUuid` هم از همین مقدار +تغذیه می‌شوند (prop می‌گیرند، پس با همین فیکس درست می‌شوند). + +### ۳. TurnsTimeline: خطا ≠ روز بدون شیفت + +- `AppointmentsPage` باید `slotsQuery.isError` و پیام خطای API (`errors[0].message`) را به + `TurnsTimeline` بدهد (prop جدید `errorMessage?: string | null`). +- در `TurnsTimeline`: اول خطا (`errorMessage` → همان پیام + ظاهر خطا)، بعد + `EMPTY_REASON_TEXT[emptyReason]`، و برای reason ناشناخته/غایب یک پیام خنثی: + «برنامهٔ این روز در دسترس نیست» — **هرگز** پیش‌فرض `day_off` نگذار؛ آن پیام یعنی + «backend صریحاً گفت این روز شیفت ندارد». +- دقت: پاسخ خطای API با `success:false` می‌آید؛ `lib/api.ts` را ببین که آیا آن را throw + می‌کند یا resolve — مسیر درست را بر همان اساس بنویس. + +### ۴. انتخاب محل برای ادمین (و هر بیننده‌ای بدون context کلینیک) + +ادمین context کلینیکی ندارد و نباید هم `useClinicContext` برایش چیزی جعل کند. راه درست: +همان منبع سایت عمومی — `GET /api/v1/appointment-booking-locations/{doctorUuid}`: + +- در `AppointmentsPage`، وقتی `isAdmin` و پزشکی انتخاب شده، این endpoint را بگیر + (query key شامل `selectedDoctorUuid`). +- اگر بیش از یک محل بود، یک `SearchableSelect` (قانون پروژه — نه `