# پرامپت: غیرفعال کردن دکتر توسط صاحب کلینیک

## هدف

صاحب کلینیک بتواند یک دکتر را از کلینیک خودش **غیرفعال** کند (نه حذف — تا سوابق حفظ شوند).  
از لحظه غیرفعال شدن، تمام نوبت‌های آینده (pending/confirmed) آن دکتر **لغو** می‌شوند.  
صاحب کلینیک می‌تواند در آینده دکتر را مجدداً فعال کند.

---

## زمینه فنی موجود

### جدول `clinic_doctors` (join table فعلی):
```sql
clinic_id INT  (PK)
doctor_id INT  (PK)
-- فاقد is_active یا deactivated_at
```

### API موجود:
- `GET /api/v1/clinic/doctor-list/{clinicUuid}` → لیست دکترهای کلینیک  
  پیاده‌سازی: `$clinic->getDoctors()->toArray()` → `$d->toListArray()`  
  فیلد `active` در response = `doctor.active_doctor_appointment` (**global** flag — نه clinic-specific)

### Frontend موجود:
- **`ClinicDetailPage.tsx`**: نمایش لیست دکترها با badge active/inactive  
  Interface: `{ id, uuid, name, gender, degree, img, specialties, active: boolean }`  
  Route: `/admin/clinics/:uuid`  
  دسترسی: admin + clinic owner (هر دو می‌توانند ببینند)

### احراز هویت clinic owner:
```php
// در ClinicController.update:
if ($clinic->getUser()->getId() !== $user->getId() && !$user->hasRole('ROLE_ADMIN')) {
    return $this->error(..., 403);
}
```
همین الگو را در endpoint های جدید استفاده کن.

### نوبت‌ها:
- جدول `appointments` فقط `doctor_id` دارد — **clinic_id و address_id ندارد**
- جدول `doctor_addresses` هم **clinic_id ندارد** — فقط `doctor_id`, `name`, `address`, `telephone`, `city_id`, `province_id`
- `weekly_schedules.setting` (JSON) هر session دارای `location_id` است که به `doctor_addresses.id` اشاره دارد — اما این مقدار در appointment ذخیره نمی‌شود
- **بنابراین در حال حاضر از طریق دیتابیس نمی‌توان فهمید هر نوبت در کدام کلینیک بوده**
- استاتوس‌ها: `pending`, `confirmed`, `completed`, `cancelled_by_doctor`, `cancelled_by_user`, `expired`, `no_show`  
- کنسل کردن = تغییر status به `cancelled_by_doctor`

---

## قابلیت‌ها

---

### قابلیت ۱ — Migration: سه تغییر schema

برای پیاده‌سازی لغو نوبت‌های مختص کلینیک، سه ستون در سه جدول مختلف لازم است:

#### ۱-الف: جدول `clinic_doctors`
```sql
is_active      TINYINT(1)  NOT NULL DEFAULT 1
deactivated_at INT(11)     NULL
```

#### ۱-ب: جدول `doctor_addresses`
```sql
clinic_id      INT(11)     NULL   -- FK به clinics.id (اگر این آدرس متعلق به کلینیکی باشد)
```

#### ۱-ج: جدول `appointments`
```sql
address_id     INT(11)     NULL   -- FK به doctor_addresses.id (آدرس محل نوبت)
```

**چرا این سه تغییر؟**
- `clinic_doctors.is_active` + `deactivated_at` → برای مدیریت وضعیت دکتر در کلینیک
- `doctor_addresses.clinic_id` → برای دانستن کدام آدرس‌ها به این کلینیک تعلق دارند
- `appointments.address_id` → برای دانستن نوبت در کدام آدرس (و در نتیجه کدام کلینیک) ثبت شده

> ⚠️ هر سه migration باید **دستی** نوشته شوند (join table و فیلدهای extra با Doctrine Entity آپدیت نمی‌شوند به صورت خودکار).

**Migration file (یک فایل، سه addSql):**
```php
$this->addSql("ALTER TABLE clinic_doctors ADD COLUMN is_active TINYINT(1) NOT NULL DEFAULT 1");
$this->addSql("ALTER TABLE clinic_doctors ADD COLUMN deactivated_at INT(11) NULL");
$this->addSql("ALTER TABLE doctor_addresses ADD COLUMN clinic_id INT(11) NULL");
$this->addSql("ALTER TABLE doctor_addresses ADD CONSTRAINT FK_doctor_addr_clinic FOREIGN KEY (clinic_id) REFERENCES clinics(id) ON DELETE SET NULL");
$this->addSql("ALTER TABLE appointments ADD COLUMN address_id INT(11) NULL");
$this->addSql("ALTER TABLE appointments ADD CONSTRAINT FK_appt_address FOREIGN KEY (address_id) REFERENCES doctor_addresses(id) ON DELETE SET NULL");
```

**آپدیت Entity:**
- در `DoctorAddress.php`: فیلد `private ?int $clinicId = null;` اضافه کن (با getter/setter)
- در `Appointment.php`: فیلد `private ?int $addressId = null;` اضافه کن (با getter/setter)

**اجرا:**
```bash
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
```

---

### قابلیت ۲ — Backend: endpoint غیرفعال کردن دکتر

**فایل:** `src/Clinic/Controller/ClinicController.php`

**Route:** `PATCH /api/v1/clinic/{clinicUuid}/doctor/{doctorUuid}/deactivate`

**Auth:** `IS_AUTHENTICATED_FULLY` + باید owner کلینیک باشد

**منطق:**
1. کلینیک را با `clinicUuid` پیدا کن — اگر نبود: 404
2. بررسی owner: `$clinic->getUser()->getId() !== $user->getId()` → 403
3. دکتر را با `doctorUuid` پیدا کن — اگر نبود: 404
4. بررسی عضویت: دکتر باید در `clinic_doctors` این کلینیک باشد → اگر نبود: 404
5. بررسی: اگر قبلاً غیرفعال بود (`is_active = 0`) → `ERR_CONFLICT_001` / 409
6. با native SQL جدول join را آپدیت کن:
   ```sql
   UPDATE clinic_doctors SET is_active = 0, deactivated_at = :now
   WHERE clinic_id = :clinicId AND doctor_id = :doctorId
   ```
7. **کنسل کردن نوبت‌های آینده مختص این کلینیک:**
   ```php
   // فقط نوبت‌هایی که address_id آن‌ها به آدرس‌های این کلینیک اشاره می‌کند
   $future = $appointmentRepo->findFutureActiveByDoctorAndClinic($doctor, $clinic, time());
   foreach ($future as $appt) {
       $appt->transitionTo(Appointment::STATUS_CANCELLED_BY_DOCTOR);
   }
   $this->em->flush();
   ```
8. Response: `{ message: 'دکتر غیرفعال شد', cancelled_appointments: count }`

**Repository method لازم (`AppointmentRepository`):**
```php
public function findFutureActiveByDoctorAndClinic(Doctor $doctor, Clinic $clinic, int $now): array
{
    // نوبت‌های آینده این دکتر که در آدرس‌های متعلق به این کلینیک رزرو شده‌اند
    return $this->createQueryBuilder('a')
        ->join(DoctorAddress::class, 'addr', 'WITH', 'a.addressId = addr.id')
        ->where('a.doctor = :doctor')
        ->andWhere('addr.clinicId = :clinicId')
        ->andWhere('a.slotStart > :now')
        ->andWhere('a.status IN (:statuses)')
        ->setParameter('doctor', $doctor)
        ->setParameter('clinicId', $clinic->getId())
        ->setParameter('now', $now)
        ->setParameter('statuses', [Appointment::STATUS_PENDING, Appointment::STATUS_CONFIRMED])
        ->getQuery()
        ->getResult();
}
```

> ⚠️ چون `address_id` یک integer column است (نه ORM relation)، از native query استفاده کن اگر DQL مشکل داشت:
> ```php
> return $this->getEntityManager()->createNativeQuery(
>     "SELECT a.* FROM appointments a
>      JOIN doctor_addresses da ON da.id = a.address_id
>      WHERE a.doctor_id = :doctorId
>        AND da.clinic_id = :clinicId
>        AND a.slot_start > :now
>        AND a.status IN ('pending', 'confirmed')",
>     (new ResultSetMappingBuilder($this->getEntityManager()))->addRootEntityFromClassMetadata(Appointment::class, 'a')
> )->setParameters(['doctorId' => $doctor->getId(), 'clinicId' => $clinic->getId(), 'now' => $now])
>  ->getResult();
> ```

---

### قابلیت ۲-ب — Backend: ذخیره `address_id` هنگام ثبت نوبت

**فایل:** `src/Appointment/Controller/AppointmentController.php`

برای اینکه بعداً بتوان نوبت‌ها را بر اساس کلینیک فیلتر کرد، باید `address_id` در هنگام booking ذخیره شود.

**تغییر لازم در booking endpoint:**
- Request body باید `address_id` (nullable integer) را بپذیرد
- این مقدار از `location_id` در slot response می‌آید (فرانت‌اند آن را دارد)
- هنگام ایجاد `Appointment`: `$appointment->setAddressId($request->get('address_id'))`

> ⚠️ این تغییر باید با frontend booking flow هماهنگ باشد — `location_id` از هر slot به عنوان `address_id` ارسال شود.

---

### قابلیت ۳ — Backend: endpoint فعال کردن مجدد دکتر

**Route:** `PATCH /api/v1/clinic/{clinicUuid}/doctor/{doctorUuid}/reactivate`

**Auth:** `IS_AUTHENTICATED_FULLY` + owner

**منطق:**
1. کلینیک + owner check + دکتر + عضویت (همان قابلیت ۲)
2. بررسی: اگر قبلاً فعال بود (`is_active = 1`) → `ERR_CONFLICT_001` / 409
3. آپدیت:
   ```sql
   UPDATE clinic_doctors SET is_active = 1, deactivated_at = NULL
   WHERE clinic_id = :clinicId AND doctor_id = :doctorId
   ```
4. Response: `{ message: 'دکتر مجدداً فعال شد' }`

---

### قابلیت ۴ — Backend: آپدیت endpoint `doctor-list`

**فایل:** `src/Clinic/Controller/ClinicController.php` — متد `doctorList`

**مشکل فعلی:** `active` field از `doctor.active_doctor_appointment` می‌آید (global) — باید از `clinic_doctors.is_active` بیاید (clinic-specific).

**راه‌حل:** Query را به native SQL تغییر بده:

```php
$rows = $conn->fetchAllAssociative(
    "SELECT d.uuid, d.name, d.gender, d.degree, d.images,
            cd.is_active, cd.deactivated_at,
            GROUP_CONCAT(DISTINCT cs.name SEPARATOR '||') as specialty_names,
            GROUP_CONCAT(DISTINCT cs.id   SEPARATOR '||') as specialty_ids
     FROM clinic_doctors cd
     JOIN doctors d ON d.id = cd.doctor_id
     JOIN clinics c  ON c.id = cd.clinic_id
     LEFT JOIN doctor_specialties ds ON ds.doctor_id = d.id
     LEFT JOIN categories cs ON cs.id = ds.category_id
     WHERE c.uuid = :uuid
     GROUP BY d.id, cd.is_active, cd.deactivated_at",
    ['uuid' => $clinicUuid]
);
```

Response format (همان ساختار قبلی + فیلدهای جدید):
```json
{
  "uuid": "...",
  "name": "دکتر احمدی",
  "gender": "male",
  "degree": "متخصص",
  "img": [],
  "specialties": [{ "id": "1", "name": "قلب" }],
  "active": true,
  "deactivated_at": null
}
```

---

### قابلیت ۵ — Frontend: دکمه غیرفعال/فعال در `ClinicDetailPage.tsx`

**فایل:** `assets/admin/pages/ClinicDetailPage.tsx`

**موارد لازم:**

#### آپدیت interface:
```ts
interface ClinicDoctorItem {
  id: string; uuid: string; name: string;
  gender: string | null; degree: string | null;
  img: { url: string }[];
  specialties: { id: string; name: string }[];
  active: boolean;
  deactivated_at: number | null; // جدید
}
```

#### منطق نمایش دکمه:
- دکمه فقط برای **clinic owner** نمایش داده شود:
  ```ts
  const authStore = useAuthStore();
  const isOwner = authStore.context?.type === 'clinic' && authStore.dbUuid === uuid; // uuid از params
  ```
- اگر `isOwner = true`: کنار هر دکتر دکمه نمایش بده

#### دکمه‌ها:
- اگر `doc.active = true`:  
  دکمه «غیرفعال کردن» (className: `btn sm soft`) با آیکون `NoSymbolIcon`  
  با `ConfirmDialog` که متن هشدار داشته باشد: **«تمام نوبت‌های آینده این دکتر لغو خواهند شد»**

- اگر `doc.active = false`:  
  دکمه «فعال کردن» (className: `btn sm primary`) با آیکون `CheckCircleIcon`  
  بدون confirm dialog

#### API calls (با `useMutation`):
```ts
const deactivateMutation = useMutation({
  mutationFn: (doctorUuid: string) =>
    api.patch(`/api/v1/clinic/${uuid}/doctor/${doctorUuid}/deactivate`, {}),
  onSuccess: (res) => {
    const count = res?.data?.cancelled_appointments ?? 0;
    toast.success(`دکتر غیرفعال شد${count ? ` — ${count} نوبت لغو شد` : ''}`);
    queryClient.invalidateQueries({ queryKey: ['clinic-doctors', uuid] });
  },
  onError: (err) => toast.error(err.message ?? 'خطا'),
});

const reactivateMutation = useMutation({
  mutationFn: (doctorUuid: string) =>
    api.patch(`/api/v1/clinic/${uuid}/doctor/${doctorUuid}/reactivate`, {}),
  onSuccess: () => {
    toast.success('دکتر مجدداً فعال شد');
    queryClient.invalidateQueries({ queryKey: ['clinic-doctors', uuid] });
  },
  onError: (err) => toast.error(err.message ?? 'خطا'),
});
```

#### UX نکات:
- `ConfirmDialog` برای deactivate: عنوان «غیرفعال کردن دکتر»، متن «تمام نوبت‌های آینده این دکتر لغو می‌شوند. آیا مطمئن هستید؟»
- badge دکتر غیرفعال: `className="badge gray"` + تاریخ غیرفعالی با `formatDate(String(doc.deactivated_at))`
- import لازم: `NoSymbolIcon`, `CheckCircleIcon` از `@heroicons/react/24/outline`
- `ConfirmDialog` از `../components/ui/ConfirmDialog`

---

## ترتیب اجرا

1. Migration دستی (یک فایل): `clinic_doctors.is_active/deactivated_at` + `doctor_addresses.clinic_id` + `appointments.address_id`
2. آپدیت Entity: `DoctorAddress.clinicId` و `Appointment.addressId` (getter/setter)
3. Backend: آپدیت booking endpoint تا `address_id` را بگیرد و ذخیره کند
4. Backend: `findFutureActiveByDoctorAndClinic` در `AppointmentRepository`
5. Backend: `deactivate` endpoint
6. Backend: `reactivate` endpoint
7. Backend: آپدیت `doctorList` به native SQL (شامل `is_active` از `clinic_doctors`)
8. تست backend
9. Frontend: آپدیت `ClinicDetailPage.tsx` (دکمه deactivate/reactivate)
10. تست frontend
11. مستندسازی `docs/api/clinic.md`

---

## تست هر مرحله

```bash
# migration (دستی ساخته می‌شود — نه از طریق diff)
ddev exec php bin/console doctrine:migrations:migrate --no-interaction

# syntax
ddev exec php -l src/Clinic/Controller/ClinicController.php
ddev exec php -l src/Appointment/Repository/AppointmentRepository.php
ddev exec php -l src/Doctor/Entity/DoctorAddress.php
ddev exec php -l src/Appointment/Entity/Appointment.php
ddev exec php -l src/Appointment/Controller/AppointmentController.php

# cache + routes
ddev exec php bin/console cache:clear
ddev exec php bin/console debug:router | grep -E "deactivate|reactivate"

# frontend
ddev exec yarn dev
ddev exec npx tsc --noEmit --project tsconfig.json 2>&1 | head -20
```

---

## مستندسازی

به `docs/api/clinic.md` اضافه کن:

### `PATCH /api/v1/clinic/{clinicUuid}/doctor/{doctorUuid}/deactivate`
### `PATCH /api/v1/clinic/{clinicUuid}/doctor/{doctorUuid}/reactivate`
### آپدیت `GET /api/v1/clinic/doctor-list/{clinicUuid}` (فیلدهای جدید)
