# باگ‌فیکس صفحه نوبت‌ها ## فهرست باگ‌ها | # | باگ | فایل‌ها | |---|-----|---------| | ۱ | کرش تقویم — `RangeError: Invalid array length` | `PersianCalendar.tsx` | | ۲ | روز هفته در DateNavigator نمایش داده نمی‌شود | `AppointmentsPage.tsx` | | ۳ | پیام «slot نیست» باید «تعطیل است» باشد | `AppointmentsPage.tsx` | | ۴ | نوبت جدید — نام اجباری + find-or-create patient | `AppointmentsPage.tsx` + دو controller | | ۵ | در نمایش جدولی، موبایل پزشک به جای بیمار نشان داده می‌شود | `AppointmentsPage.tsx` + `AppointmentController.php` | | ۶ | نوبت‌های رزرو شده در نمایش زمانبندی نشان داده نمی‌شوند | `AppointmentsPage.tsx` | | ۷ | ستون عملیات («...») از نمایش جدولی حذف شود | `AppointmentsPage.tsx` | --- ## باگ ۱ — کرش تقویم ### علت ریشه‌ای **فایل:** `assets/admin/components/ui/PersianCalendar.tsx` ```ts // مشکل: locale 'fa-IR' → اعداد فارسی → parseInt('۱۴۰۴') = NaN → Array(NaN) کرش const parts = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { ... }).formatToParts(d); const get = (t: string) => parseInt(parts.find(...), 10); ``` `parseInt('۱۴۰۴', 10)` در مرورگر `NaN` می‌دهد → `viewYear = NaN` → `Array(NaN).fill(null)` → کرش. ### فیکس ```ts // locale را به 'en' عوض کن — تاریخ شمسی ولی اعداد ASCII const parts = new Intl.DateTimeFormat('en-u-ca-persian', { year: 'numeric', month: 'numeric', day: 'numeric', }).formatToParts(d); ``` --- ## باگ ۲ — روز هفته در DateNavigator ### وضعیت فعلی ```tsx // فقط تاریخ شمسی — بدون روز هفته
{formatDate(date)}
``` ### رفتار مورد انتظار ``` [ < ] ┌──────────────┐ [ > ] [ 📅 ] │ شنبه │ │ ۱۴۰۴/۰۳/۲۲ │ └──────────────┘ ``` ### الزامات UI - روز هفته فارسی: شنبه / یک‌شنبه / دوشنبه / سه‌شنبه / چهارشنبه / پنج‌شنبه / جمعه - روز هفته **پررنگ‌تر** از تاریخ - **جمعه** به رنگ قرمز (`#ef4444`) — تعطیل - واکنشی: مستقیم از prop `date` محاسبه شود، نه state جداگانه - height کمی بیشتر (۴۴px) — هم‌تراز با دکمه‌های toolbar ### پیاده‌سازی ```ts const WEEK_DAYS_FA = ['یک‌شنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنج‌شنبه', 'جمعه', 'شنبه']; // 0=Sun 1=Mon 2=Tue 3=Wed 4=Thu 5=Fri 6=Sat function getPersianWeekDay(gregorianDate: string): string { return WEEK_DAYS_FA[new Date(gregorianDate + 'T12:00:00').getDay()]; } ``` در `DateNavigator`: ```tsx const weekDay = getPersianWeekDay(date); const isFriday = new Date(date + 'T12:00:00').getDay() === 5; // جایگزین div تاریخ:
{weekDay} {formatDate(date)}
``` --- ## باگ ۳ — پیام «slot نیست» **فایل:** `AppointmentsPage.tsx` — تابع `ScheduleView` ```tsx // قبل: if (!slots.length) return
هیچ slot زمانی برای این روز تنظیم نشده است
; // بعد: if (!slots.length) return (
🏖️
این روز تعطیل است
هیچ برنامه زمانبندی برای این روز تنظیم نشده است
); ``` --- ## باگ ۴ — نوبت جدید: نام اجباری + find-or-create patient ### مشکل فعلی **Frontend:** `NewAppointmentModal` فقط موبایل دارد، نام ندارد. **Backend `POST /api/v1/admin/appointment`** (خط ۷۵۰ در `AdminApiController.php`): ```php $patient = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]); if (!$patient) return $this->error('USER_NOT_FOUND', 'بیمار با این شماره یافت نشد', 404); // ❌ اشتباه: به جای ایجاد کاربر جدید، خطا برمی‌گرداند ``` **Backend `POST /api/v1/appointment`** (برای دکتر/کلینیک): ```php // ❌ اشتباه: از کاربر لاگین‌شده (خود دکتر) به عنوان بیمار استفاده می‌کند $appointment = new Appointment($doctor, $user, $slotStart, $slotEnd); ``` ### فیکس Backend #### ۱. `POST /api/v1/admin/appointment` — در `AdminApiController.php` افزودن `patient_name` به body و find-or-create: ```php $mobile = trim($data['patient_mobile'] ?? ''); $patientName = trim($data['patient_name'] ?? ''); if (empty($doctorUuid) || $slotStart <= 0 || $slotEnd <= $slotStart || empty($mobile) || empty($patientName)) { return $this->error('VALIDATION', 'doctor_uuid، slot_start، slot_end، patient_mobile و patient_name الزامی است', 422); } // find-or-create: $patient = $this->em->getRepository(User::class)->findOneBy(['mobileNumber' => $mobile]); if (!$patient) { $patient = new User($mobile); $patient->setRealName($patientName); $patient->setRoles(['ROLE_USER']); $this->em->persist($patient); } ``` #### ۲. `POST /api/v1/my/appointment` — **endpoint جدید** در `MyAppointmentsController.php` این endpoint برای دکتر/کلینیک/منشی است که می‌خواهند برای بیمار نوبت بگیرند: ```php #[Route('/api/v1/my/appointment', methods: ['POST'])] #[IsGranted('IS_AUTHENTICATED_FULLY')] public function createAppointment(Request $request, #[CurrentUser] User $user): JsonResponse { $data = json_decode($request->getContent(), true) ?? []; $doctorUuid = trim($data['doctor_uuid'] ?? ''); $slotStart = (int) ($data['slot_start'] ?? 0); $slotEnd = (int) ($data['slot_end'] ?? 0); $mobile = trim($data['patient_mobile'] ?? ''); $patientName = trim($data['patient_name'] ?? ''); if (empty($doctorUuid) || $slotStart <= 0 || $slotEnd <= $slotStart || empty($mobile) || empty($patientName)) { return $this->error('VALIDATION', 'همه فیلدها الزامی است', 422); } // فقط برای نقش‌های مجاز $roles = $user->getRoles(); $allowedRoles = ['ROLE_DOCTOR', 'ROLE_CLINIC', 'ROLE_SECRETARY', 'ROLE_ADMIN']; if (!array_intersect($allowedRoles, $roles)) { return $this->error('FORBIDDEN', 'دسترسی ندارید', 403); } $doctor = $this->doctorRepo->findByUuid($doctorUuid); if (!$doctor) return $this->error('DOCTOR_NOT_FOUND', 'پزشک یافت نشد', 404); // find-or-create patient $patient = $this->em->getRepository(\App\Auth\Entity\User::class)->findOneBy(['mobileNumber' => $mobile]); if (!$patient) { $patient = new \App\Auth\Entity\User($mobile); $patient->setRealName($patientName); $patient->setRoles(['ROLE_USER']); $this->em->persist($patient); } // conflict check $conflict = $this->em->createQueryBuilder() ->select('COUNT(a.id)')->from(\App\Appointment\Entity\Appointment::class, 'a') ->where('a.doctor = :doctor') ->andWhere('a.slotStart < :end AND a.slotEnd > :start') ->andWhere("a.status NOT IN ('cancelled_by_doctor','cancelled_by_user','cancelled_by_admin','auto_cancel_unpaid')") ->setParameter('doctor', $doctor)->setParameter('start', $slotStart)->setParameter('end', $slotEnd) ->getQuery()->getSingleScalarResult(); if ($conflict > 0) return $this->error('SLOT_TAKEN', 'این نوبت قبلاً رزرو شده است', 409); $appointment = new \App\Appointment\Entity\Appointment($doctor, $patient, $slotStart, $slotEnd); $this->em->persist($appointment); $this->em->flush(); return $this->success(['uuid' => $appointment->getUuid(), 'status' => $appointment->getStatus()], 201); } ``` ### فیکس Frontend — `NewAppointmentModal` ```tsx function NewAppointmentModal({ slot, onClose, onSuccess }) { const [mobile, setMobile] = useState(''); const [patientName, setPatientName] = useState(''); const qc = useQueryClient(); const role = useAuthStore(s => s.primaryRole); // ادمین از admin endpoint، بقیه از my endpoint const createEndpoint = role === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/my/appointment'; const mutation = useMutation({ mutationFn: () => api.post(createEndpoint, { doctor_uuid: slot.doctor_uuid, slot_start: slot.start, slot_end: slot.end, patient_mobile: mobile, patient_name: patientName, }), // ... }); const isValid = mobile.length >= 10 && patientName.trim().length >= 2; return (
e.stopPropagation()} style={{ ... }}> {/* فیلد نام */}
setPatientName(e.target.value)} placeholder="مثال: علی محمدی" style={{ width: '100%', height: 38, ... }} autoFocus />
{/* فیلد موبایل */}
setMobile(e.target.value)} placeholder="مثال: 09123456789" style={{ width: '100%', height: 38, direction: 'ltr', ... }} />
); } ``` --- ## باگ ۵ — patient_mobile نشان می‌دهد موبایل پزشک ### علت ریشه‌ای **فایل:** `AppointmentController.php` — endpoint `POST /api/v1/appointment` ```php // ❌ $user = دکتر لاگین‌شده → نه بیمار $appointment = new Appointment($doctor, $user, $slotStart, $slotEnd); ``` وقتی دکتر یا کلینیک از `/api/v1/appointment` استفاده می‌کند، کاربر لاگین‌شده (خود دکتر) به عنوان بیمار ثبت می‌شود. پس `u.mobileNumber as patient_mobile` موبایل دکتر را برمی‌گرداند. ### فیکس بعد از فیکس باگ ۴ (ساخت endpoint جدید `POST /api/v1/my/appointment`)، frontend را آپدیت کن تا از این endpoint استفاده کند: ```tsx // قبل: const createEndpoint = role === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/appointment'; // بعد: const createEndpoint = role === 'admin' ? '/api/v1/admin/appointment' : '/api/v1/my/appointment'; ``` این تغییر باگ ۴ و ۵ را با هم رفع می‌کند. --- ## باگ ۶ — نوبت‌های رزرو شده در نمایش زمانبندی ### مشکل **فایل:** `AppointmentsPage.tsx` — `mergedSlots` useMemo نوبت‌های رزرو شده باید در زمانبندی با کارت رنگی نشان داده شوند. اگر نمایش داده نمی‌شوند، احتمالاً تطبیق timestamp ها مشکل دارد. ### بررسی و فیکس ```tsx const mergedSlots: SlotItem[] = React.useMemo(() => { if (viewMode !== 'schedule') return []; const rawSlots: any[] = (slotsQuery.data?.data as any)?.slots ?? []; const apptByStart = new Map(); // مطمئن شو slot_start عدد صحیح است appointments.forEach(a => { const key = typeof a.slot_start === 'number' ? a.slot_start : parseInt(String(a.slot_start), 10); apptByStart.set(key, a); }); return rawSlots.map((s: any) => { const slotStart = typeof s.start === 'number' ? s.start : parseInt(String(s.start), 10); const appt = apptByStart.get(slotStart) ?? null; return { start: slotStart, end: typeof s.end === 'number' ? s.end : parseInt(String(s.end), 10), start_time: s.start_time ?? new Date(slotStart * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }), end_time: s.end_time ?? new Date((typeof s.end === 'number' ? s.end : parseInt(String(s.end), 10)) * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }), is_available: !appt, appointment: appt, }; }); }, [viewMode, slotsQuery.data, appointments]); ``` همچنین مطمئن شو که بعد از ثبت نوبت جدید، هر دو query invalidate می‌شوند: ```tsx onSuccess={() => { qc.invalidateQueries({ queryKey: apptQueryKey }); qc.invalidateQueries({ queryKey: slotsQueryKey }); qc.invalidateQueries({ queryKey: ['appt-today-stats', selectedDate] }); }} ``` --- ## باگ ۷ — حذف ستون عملیات از نمایش جدولی **فایل:** `AppointmentsPage.tsx` — تابع `TableView` ```tsx // حذف شود — header: عملیات // حذف شود — در هر ردیف: ``` --- ## فایل‌های تأثیرپذیر | فایل | تغییر | |------|-------| | `assets/admin/components/ui/PersianCalendar.tsx` | باگ ۱ — locale `en-u-ca-persian` | | `assets/admin/pages/AppointmentsPage.tsx` | باگ ۲، ۳، ۶، ۷ — UI + merge fix + modal | | `src/Admin/Controller/AdminApiController.php` | باگ ۴ — find-or-create در `createAppointment()` | | `src/Appointment/Controller/MyAppointmentsController.php` | باگ ۴، ۵ — endpoint جدید `POST /api/v1/my/appointment` | --- ## ترتیب اجرا 1. **باگ ۱** — `PersianCalendar.tsx`: فیکس `toJalali` → تست: تقویم باز شود 2. **باگ ۲** — `AppointmentsPage.tsx`: روز هفته در DateNavigator 3. **باگ ۳** — `AppointmentsPage.tsx`: متن «تعطیل است» 4. **باگ ۴+۵** — backend: `AdminApiController.php` + endpoint جدید در `MyAppointmentsController.php` → `php -l` هر دو فایل 5. **باگ ۴+۵** — frontend: آپدیت `NewAppointmentModal` با فیلد نام + endpoint جدید 6. **باگ ۶** — `AppointmentsPage.tsx`: بررسی و فیکس merge timestamps 7. **باگ ۷** — `AppointmentsPage.tsx`: حذف ستون عملیات 8. تست TypeScript: `ddev exec npx tsc --noEmit --project tsconfig.json` 9. تست build: `ddev exec yarn dev`