# باگفیکس صفحه نوبتها
## فهرست باگها
| # | باگ | فایلها |
|---|-----|---------|
| ۱ | کرش تقویم — `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={{ ... }}>
{/* فیلد نام */}