# نوبت‌دهی ادمین بر اساس کد ملی + موبایل (پرونده یکتا با کد ملی)

## پروژه

`clinicpro` (backend Symfony + پنل ادمین React). تک‌ریپو — نیازی به تغییر `nobat724_front` نیست.

> توجه: endpoint عمومی سایت (`POST /api/v1/appointment` → `AppointmentController::book`) **از قبل** کد ملی را الزامی و اعتبارسنجی می‌کند. این تسک فقط شکافِ مسیر **ادمین/کلینیک/منشی/پزشک** را می‌بندد که هنوز بیمار را فقط با موبایل resolve می‌کند.

## زمینه

پرونده‌ی بیمار (`PatientRecord`) روی `user_id` کلید می‌خورد (UniqueConstraint: `entity_type + entity_id + user_id`) و در `PatientService::autoCreateForEntity` از `$appointment->getUser()` ساخته می‌شود. یعنی هویت پرونده = رکورد `User`. اما رکورد `User` در مسیر ثبت نوبتِ ادمین فقط با **موبایل** پیدا/ساخته می‌شود:

- `src/Appointment/Controller/MyAppointmentsController.php` خط ۸۱: `findOneBy(['mobileNumber' => $mobile])`
- `src/Admin/Controller/AdminApiController.php` خط ۸۷۰: `findOneBy(['mobileNumber' => $mobile])`

نتیجه: یک شخص با دو موبایل مختلف → دو `User` مجزا → دو پرونده‌ی مجزا. در حالی که کد ملی یکتاست (`User.national_code` هم‌اکنون `unique: true, nullable: true`). پس هویت درستِ بیمار = **کد ملی**، و موبایل صرفاً یک راه تماس است.

## هدف

در ثبت نوبتِ ادمین، بیمار باید با **کد ملی + موبایل** شناسایی شود:

1. کد ملی در فرم و در هر دو endpoint ادمین **الزامی و معتبر** شود.
2. رکورد `User` بیمار **اول با کد ملی** resolve شود (نه صرفاً موبایل)، تا پرونده برای یک کد ملی یکتا بماند حتی اگر موبایل عوض شود.
3. `patient_national_code` روی `Appointment` ذخیره شود (فیلد و setter از قبل موجود است: `Appointment::setPatientNationalCode`).

## فایل‌های مرتبط

| فایل | نقش | تغییر |
|------|-----|-------|
| `src/Appointment/Controller/MyAppointmentsController.php` | endpoint `POST /api/v1/my/appointment` (doctor/clinic/secretary/admin) | الزام + resolve با کد ملی |
| `src/Admin/Controller/AdminApiController.php` | endpoint `POST /api/v1/admin/appointment` (فقط admin) | الزام + resolve با کد ملی |
| `src/Auth/Repository/UserRepository.php` | فقط `findByMobile` دارد | افزودن `findByNationalCode` |
| `src/Patient/Service/PatientService.php` | `resolvePatientUser` مشترک (اختیاری، ضدتکرار) | استخراج منطق resolve |
| `assets/admin/pages/AppointmentCreatePage.tsx` | فرم ثبت نوبت | افزودن فیلد کد ملی + ارسال در payload |
| `src/Shared/…/InputValidator.php` | `toEnglishDigits` + `isValidIranNationalCode` (استفاده‌شده در `book`) | فقط استفاده |
| `docs/api/appointment.md` + `docs/api/admin.md` | مستندات endpoint | به‌روزرسانی |

## وضعیت فعلی (کد واقعی)

### `book()` عمومی — الگوی درستِ موجود (کپی از `AppointmentController::book`, خط ۲۴۲–۲۶۳)

```php
$nationalCode = InputValidator::toEnglishDigits(trim((string) ($data['patient_national_code'] ?? '')));
if ($nationalCode === '') {
    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی بیمار الزامی است', 422, 'patient_national_code');
}
if (!InputValidator::isValidIranNationalCode($nationalCode)) {
    return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی نامعتبر است', 422, 'patient_national_code');
}
$appointment = new Appointment($doctor, $user, $slotStart, $slotEnd);
$appointment->setPatientNationalCode($nationalCode);
```

### مسیر ادمین — بیمار فقط با موبایل (کپی از `MyAppointmentsController::createAppointment`, خط ۸۱–۸۷)

```php
$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);
}
$appointment = new Appointment($doctor, $patient, $slotStart, $slotEnd);
```
(`AdminApiController::createAppointment` خط ۸۷۰–۸۷۶ دقیقاً همین است.)

### فرم — بدون فیلد کد ملی (کپی از `AppointmentCreatePage.tsx`)

```tsx
const [name, setName] = useState('');
const [mobile, setMobile] = useState('');
// ...
const effectiveName = picked?.user_name || name.trim();
const effectiveMobile = picked?.user_mobile || mobile.trim();
const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10 && !!start && !!end;
// payload:
patient_name: effectiveName,
patient_mobile: effectiveMobile,
```
> نکته: ردیف‌های جستجوی بیمار (`PatientRow`) فقط `user_name` و `user_mobile` دارند؛ برای پرکردن خودکارِ کد ملیِ بیمارِ انتخاب‌شده باید `user_national_code` هم از endpoint جستجو (`GET /api/v1/patient`) بیاید — بررسی کن آیا برمی‌گردد؛ اگر نه، آن را هم به خروجی اضافه کن (این فیلد در `PatientRecord::toArray` خط ۱۱۶ موجود است).

## وظایف

### ۱. `UserRepository::findByNationalCode`

در `src/Auth/Repository/UserRepository.php` کنار `findByMobile` اضافه کن:

```php
public function findByNationalCode(string $nationalCode): ?User
{
    return $this->findOneBy(['nationalCode' => $nationalCode]);
}
```

### ۲. منطق resolve بیمار با کد ملی (اولویت با کد ملی، سپس موبایل)

یک متد مشترک بساز تا در هر دو endpoint استفاده شود (DRY + SOLID). مکان پیشنهادی: `PatientService::resolvePatientUser` (یا یک سرویس کوچک اختصاصی اگر تزریق `PatientService` سنگین بود — تصمیم را در کد بنویس).

قاعده‌ی resolve:

```
nationalCode معتبر ورودی + mobile + name داریم:
1) user = userRepo.findByNationalCode(nationalCode)
2) اگر نبود: user = userRepo.findByMobile(mobile)
     - اگر پیدا شد و nationalCode او خالی است → user.setNationalCode(nationalCode)
     - اگر پیدا شد و nationalCode او با ورودی فرق دارد → خطای 422
       «این شماره موبایل به کد ملی دیگری تعلق دارد» (تعارض هویت)
3) اگر هیچ‌کدام نبود: user جدید با mobile، setRealName(name)، setNationalCode(nationalCode)، ROLE_USER، persist
4) اگر user با کد ملی پیدا شد ولی mobileنش با ورودی فرق دارد → موبایل را به‌روز نکن
   (کد ملی مرجع است؛ یک کد ملی می‌تواند چند موبایل داشته باشد — فقط پرونده یکتا بماند).
   نامِ خالیِ user را با name پر کن.
```

> چرا اولویت با کد ملی: خواسته‌ی صریح — «یک کاربر ممکن است با چند موبایل باشد و پرونده برای یک کد ملی یکتا». چون `PatientRecord` روی `user_id` است، تا وقتی برای یک کد ملی همان `User` برگردد، پرونده یکتا می‌ماند.

### ۳. الزام + اعتبارسنجی کد ملی در دو endpoint ادمین

در **هر دو** `MyAppointmentsController::createAppointment` و `AdminApiController::createAppointment`:

- بعد از خواندن `$mobile`/`$patientName`، `patient_national_code` را با همان الگوی `book()` بخوان، `toEnglishDigits` کن، خالی‌بودن و `isValidIranNationalCode` را چک کن (خطای 422 با فیلد `patient_national_code`).
- `$patient` را با متد resolve وظیفه‌ی ۲ بگیر (به‌جای `findOneBy(['mobileNumber' => $mobile])`).
- `$appointment->setPatientNationalCode($nationalCode)` را ست کن (مثل `book`).
- `patient_gender` را **الزامی نکن** مگر اینکه قبلاً در این مسیر الزامی بوده باشد — `book` عمومی جنسیت را الزامی می‌کند ولی مسیر ادمین تاکنون نمی‌کرده؛ رفتار فعلی را حفظ کن و فقط کد ملی را اضافه کن (اسکوپ حداقلی).
- به `ErrorCodes` مسیر ادمین دقت کن: این کنترلرها از ثابت‌های کوتاه (`ErrorCodes::VALIDATION`, `ErrorCodes::DOCTOR_NOT_FOUND` …) استفاده می‌کنند، نه `ERR_VALIDATION_001`. از همان سبکِ همان فایل استفاده کن.

### ۴. فرم `AppointmentCreatePage.tsx`

- state جدید: `const [nationalCode, setNationalCode] = useState('')`.
- در بلوک «مراجعه کننده جدید» (`picked === null`) یک فیلد ورودی کد ملی اضافه کن (کنار نام/موبایل). ورودی فارسی/انگلیسی را بپذیر ولی فقط رقم؛ maxLength=10، `dir="ltr"`.
- `effectiveNationalCode = picked?.user_national_code || nationalCode.trim()`.
- `valid` را گسترش بده: کد ملی باید ۱۰ رقم باشد (اعتبارسنجی کاملِ کد ملی سمت بک‌اند است؛ سمت فرانت فقط طول/رقم).
- در `payload`: `patient_national_code: effectiveNationalCode`.
- `PatientRow` را با `user_national_code?: string` گسترش بده و اگر endpoint جستجو آن را برنگرداند، در وظیفه‌ی مرتبط بک‌اند اضافه‌اش کن تا انتخاب بیمارِ موجود، فیلد را پر کند.

### ۵. مستندات

`docs/api/appointment.md` (برای `/api/v1/my/appointment`) و `docs/api/admin.md` (برای `/api/v1/admin/appointment`) را به‌روز کن: افزوده‌شدن فیلد الزامی `patient_national_code`، خطای 422 تعارض موبایل/کد ملی، و رفتار «resolve با کد ملی».

## نکات مهم

- **SOLID/DRY:** منطق resolve بیمار را یک‌جا بنویس؛ در دو کنترلر کپی‌پیست نکن. دلیلِ محلِ قرارگیری را در کامنت بنویس.
- **یکتایی DB:** `User.national_code` هم‌اکنون `unique: true` است — نیازی به migration نیست مگر تغییری در entity بدهی. اگر تغییری ندادی، migration نساز.
- **تعارض هویت (edge مهم):** موبایلی که قبلاً با کد ملیِ X ثبت شده، حالا با کد ملیِ Y بیاید → باید خطای روشن بدهی، نه اینکه کد ملی را عوض کنی (چون verify قبلی را باطل و داده را خراب می‌کند؛ `User::setNationalCode` خط ۹۵ خودش `nationalCodeVerified=false` می‌کند).
- **`for_self` نداریم اینجا:** مسیر ادمین همیشه برای «دیگری» است؛ برخلاف `book`، `$user` جاری پزشک/منشی است نه بیمار. بیمار همیشه از موبایل/کد ملیِ ورودی resolve می‌شود.
- **ارقام فارسی:** همیشه `InputValidator::toEnglishDigits` روی کد ملی و موبایل قبل از جستجو/ذخیره (منشی معمولاً فارسی تایپ می‌کند).
- **تست (الزامی — موفق/خطا/مرزی):**
  - موفق: بیمار جدید با کد ملی → `User` با `national_code` ساخته شد + نوبت ثبت شد.
  - موفق (یکتایی پرونده): همان کد ملی با موبایلِ متفاوت در نوبت دوم → همان `User` برگردد (نه User جدید)؛ پس از confirm، `PatientRecord` یکتا بماند.
  - خطا: کد ملی خالی → 422 `patient_national_code`.
  - خطا: کد ملی نامعتبر (checksum) → 422.
  - مرزی/تعارض: موبایلِ موجود با کد ملیِ متفاوت → 422 تعارض هویت.
- تست‌ها را با `ddev exec php bin/phpunit` و type-check فرانت را با `npx tsc --noEmit` اجرا کن. بدون سبز شدن، تسک تمام نیست.
- بعد از تغییر کد: `graphify update .` (اول commit طبق قاعده‌ی پروژه).
