# بازطراحی کامل صفحه نوبت‌ها (AppointmentsPage)

## هدف
صفحه `/admin/appointments` باید کاملاً بازطراحی شود تا شبیه به تصاویر مرجع باشد.
دو حالت نمایش وجود دارد: **جدولی** و **زمانبندی**. هر دو حالت date-based هستند (نه pagination کلی).

---

## یک کامپوننت برای همه نقش‌ها

**همه پنل‌ها — دکتر، کلینیک، ادمین — دقیقاً همین صفحه را می‌بینند.** یک کامپوننت `AppointmentsPage.tsx` برای هر سه نقش. تفاوت‌ها فقط در رفتار داده‌ها است، نه UI:

| ویژگی | ادمین | کلینیک | دکتر |
|-------|-------|--------|------|
| API لیست نوبت‌ها | `/api/v1/admin/appointments` | `/api/v1/my/appointments` | `/api/v1/my/appointments` |
| API slot‌های خالی | `/api/v1/appointment-slots` | `/api/v1/appointment-slots` | `/api/v1/appointment-slots` |
| dropdown پرسنل | همه دکترها | فقط دکترهای کلینیک | فقط خودش (auto-select) |
| Doctor Tabs | نمایش داده نشود | ✅ نمایش (اگر ≥۲ دکتر) | نمایش داده نشود |
| ثبت نوبت | با patient_mobile | با patient_mobile | با patient_mobile |
| تغییر وضعیت | ✅ | ✅ | ✅ |

**نکته**: وقتی `primaryRole === 'doctor'`، دکتر UUID از `useAuthStore().dbUuid` گرفته می‌شود و dropdown پرسنل auto-select + read-only است.

**نکته**: وقتی `primaryRole === 'clinic'`، لیست دکترهای قابل انتخاب از نوبت‌های برگشتی استخراج می‌شود (unique `doctor_uuid` + `doctor_name`).

---

## ساختار کلی صفحه

### ۱. نوار آمار بالا (Stats Bar)
چهار کارت آمار **امروز** (نه کل):
- **کل نوبت‌های امروز** — icon: شبکه نقطه‌ای بنفش
- **نوبت‌های انجام شده** — icon: آدم سبز
- **مراجعین در انتظار** — icon: ساعت نارنجی
- **نوبت‌های لغو شده** — icon: ضربدر قرمز

هر کارت: عدد بزرگ + برچسب + آیکون گرافیکی رنگی.
داده از API: `GET /api/v1/admin/appointments/today-stats?date=YYYY-MM-DD`
پاسخ: `{ total, completed, waiting, cancelled }`

---

### ۲. نوار کنترل (Toolbar)

```
[+ نوبت جدید]  [آیکون جدول]  [نمایش جدولی] [زمانبندی]  [پرسنل را انتخاب کنید ▼]  [←] [۱۴۰۳/۰۶/۰۵] [→] [📅]
```

**اجزا:**
- دکمه **"+ نوبت جدید"**: primary، باز می‌کند modal ثبت نوبت
- **تاگل نما**: دو تب inline — "نمایش جدولی" (active=رنگی) و "زمانبندی"
- **انتخاب پرسنل**: dropdown با لیست پزشکان — برای ادمین همه، برای کلینیک پزشکان خودش
- **ناوبری تاریخ**: دکمه‌های `←` `→` برای روز قبلی/بعدی + تاریخ شمسی + آیکون تقویم که PersianCalendar popup باز می‌کند
- **تقویم popup شمسی**: ماه/سال فارسی، روزهای هفته فارسی (ش ی د س چ پ ج)، امروز highlight خاکستری، روز انتخابی circle آبی، دکمه‌های ماه قبل/بعد

**نکته مهم**: نما date-based است — هر بار یک روز خاص نمایش داده می‌شود. پیش‌فرض = امروز.

---

### ۳. سیستم وضعیت‌ها

**وضعیت‌های واقعی backend** (entity `Appointment.php`):

| نام نمایشی       | backend value (`STATUS_*`)  | رنگ        |
|-----------------|----------------------------|------------|
| رزرو شده        | `pending`                  | آبی        |
| تأیید شده       | `confirmed`                | سبز        |
| تکمیل شده       | `completed`                | سبز تیره   |
| لغو پزشک        | `cancelled_by_doctor`      | قرمز       |
| لغو بیمار       | `cancelled_by_user`        | قرمز       |
| غیبت            | `no_show`                  | خاکستری    |
| منقضی شده       | `expired`                  | خاکستری    |

**ماشین حالت (ALLOWED_TRANSITIONS)**:
- `pending` → `confirmed` | `cancelled_by_doctor` | `cancelled_by_user` | `expired`
- `confirmed` → `completed` | `cancelled_by_doctor` | `cancelled_by_user` | `no_show`

**PATCH status موجود** در `AppointmentController.php`:
```
PATCH /api/v1/appointment/{uuid}/status
Body: { "status": "confirmed", "version": 1 }
```
دارای optimistic lock (version field). دکتر و ادمین هر دو مجاز هستند.

**نکته مهم**: وضعیت‌هایی مثل `waiting_for_payment`, `checked_in`, `waiting`, `in_progress`, `visited` که در frontend قدیمی بودند در entity وجود ندارند. فقط از وضعیت‌های بالا استفاده شود.

**تغییر inline وضعیت**: کلیک روی badge → dropdown با دایره‌های رنگی → انتخاب → PATCH:
```
PATCH /api/v1/admin/appointment/{uuid}/status
Body: { "status": "visited" }
```
پس از موفقیت: فقط cache همان query را invalidate کند (بدون reload صفحه).

---

### ۴. نمای جدولی (Table View)

**ستون‌ها**:
`ردیف` | `نام بیمار` | `شماره تماس` | `شروع` | `پایان` | `وضعیت` | `عملیات`

- **ردیف**: شماره ردیف ساده
- **شروع / پایان**: ساعت HH:MM
- **وضعیت**: badge قابل کلیک با `▼` → dropdown تغییر وضعیت
- **عملیات**: دکمه `...` → منو: مشاهده جزئیات، لغو نوبت

**API**: `GET /api/v1/admin/appointments?date=YYYY-MM-DD&doctor_uuid=...&limit=100`
(در این نما limit بالا، بدون pagination — همه نوبت‌های یک روز)

---

### ۵. نمای زمانبندی (Schedule View)

**بالا**: Doctor Tabs — تب برای هر پزشک، کلیک = سوئیچ به نوبت‌های آن دکتر

**Layout** (RTL):
```
[ کارت‌های نوبت — ستون عریض چپ ]    [ ● ۰۸:۰۰ — محور زمان راست ]
```

**محور زمان (Time Axis)**:
- خط عمودی نقطه‌ای (dashed) از بالا به پایین
- نقطه ● در زمان شروع هر slot
- label زمان فارسی کنار نقطه (۰۸:۰۰، ۰۸:۴۰)

**کارت نوبت پر**:
```
┌────────────────────────────────────────────────────────────┐
│  ▼ ویزیت شده                       ۰۸:۰۰ ● ─ ─ ─ ─ ─ ─  │  ← header
│                                     ۰۸:۳۵ ●               │
├────────────────────────────────────────────────────────────┤
│  👤 ساغر صابری نژاد   📞 ۰۹۳۵۶۶۱۹۴۳۸                     │  ← body
│  سرویس: [نام سرویس]            [+ اضافه]                   │
├────────────────────────────────────────────────────────────┤
│  [عملیات ...]                                              │  ← footer
└────────────────────────────────────────────────────────────┘
```
رنگ‌بندی: border + background-tint متناسب با وضعیت:
- ویزیت شده: border سبز، background سبز ۵٪
- لغو شده: border قرمز، background قرمز ۵٪
- در حال پیگیری: border نارنجی، background نارنجی ۵٪
- سالن: border بنفش، background بنفش ۵٪
- ثبت شده/قطعی: border آبی، background آبی ۵٪

**کارت slot خالی**:
```
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│                    ⊕ نوبت جدید                           │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
```
- border dashed آبی کمرنگ
- کلیک → modal ثبت نوبت با doctor_uuid + slot_start/end پیش‌پر شده

**API برای نمای زمانبندی**:
`GET /api/v1/appointment-slots?doctor_uuid=...&date=YYYY-MM-DD`
باید slot‌های پر را هم برگرداند با اطلاعات نوبت:
```json
{
  "slots": [
    {
      "start": 1234567890,
      "end":   1234568490,
      "label": "09:10",
      "is_available": false,
      "appointment": {
        "uuid": "...",
        "patient_name": "...",
        "patient_mobile": "...",
        "status": "visited",
        "service_name": "..."
      }
    },
    {
      "start": 1234569000,
      "end":   1234569600,
      "label": "09:45",
      "is_available": true,
      "appointment": null
    }
  ]
}
```

---

## وضعیت Backend موجود (بررسی شده)

### ✅ موجود و قابل استفاده
| Endpoint | وضعیت |
|----------|-------|
| `GET /api/v1/appointment-slots?doctor_uuid=...&date=YYYY-MM-DD` | ✅ موجود — **فقط slot‌های خالی** برمی‌گرداند |
| `POST /api/v1/appointment` | ✅ موجود — برای کاربر لاگین‌شده |
| `POST /api/v1/admin/appointment` | ✅ موجود — برای ادمین با patient_mobile |
| `PATCH /api/v1/appointment/{uuid}/status` | ✅ موجود — با optimistic lock و transition validation |
| `GET /api/v1/admin/appointments?date=...` | باید بررسی شود آیا date filter دارد |

### 🔴 ندارد — باید اضافه شود

**۱. آمار امروز** — در `AdminApiController`:
```php
#[Route('/api/v1/admin/appointments/today-stats', methods: ['GET'])]
// پارامتر: ?date=YYYY-MM-DD
// منطق: COUNT GROUP BY status با DATE(FROM_UNIXTIME(slot_start)) = :date
// پاسخ: { total, completed, waiting, cancelled }
```

**۲. Slot‌های کامل (پر + خالی)** — برای نمای زمانبندی:

**روش پیشنهادی (بدون backend جدید)**:
- Frontend دو query موازی می‌زند:
  1. `GET /api/v1/appointment-slots?doctor_uuid=...&date=...` → لیست slot‌های خالی
  2. `GET /api/v1/admin/appointments?date=...&doctor_uuid=...` → لیست نوبت‌های پر
- Merge در frontend: هر slot یا `{ is_available: true }` یا `{ is_available: false, appointment: {...} }`

اگر merge پیچیده شد، گزینه دوم اضافه کردن param به slot endpoint:
```
GET /api/v1/appointment-slots?doctor_uuid=...&date=...&include_booked=1
```
که slot‌های رزرو‌شده را هم با فیلد `appointment` برمی‌گرداند.

---

---

## ⭐ مهم‌ترین Feature: ثبت نوبت از نمای زمانبندی

### پیش‌شرط: دکتر باید برنامه هفتگی داشته باشد

**جریان بررسی**:
1. وقتی دکتری انتخاب می‌شود، ابتدا `GET /api/v1/appointment-settings/weekly-schedule/{doctor_uuid}` فراخوانی شود
2. اگر `404` برگشت → نوار هشدار نمایش داده شود:
   ```
   ⚠️ دکتر [نام] هنوز برنامه هفتگی تنظیم نکرده است.
   [تنظیم برنامه] ← لینک به صفحه تنظیمات دکتر
   ```
3. اگر schedule موجود بود → نوبت‌ها بر اساس آن محاسبه شوند

**Slot Format خروجی `SlotCalculatorService`**:
```json
{
  "start": 1718438400,
  "end":   1718439600,
  "start_time": "09:00",
  "end_time": "09:20",
  "location_id": 1973
}
```
فیلد `label` برای نمایش: `"09:00"` (از `start_time`)

### جریان ثبت نوبت از slot خالی (اصلی‌ترین UX)

کلیک روی کارت `+ نوبت جدید` در نمای زمانبندی:

```
┌──────────────────────────────┐
│  ثبت نوبت — ۰۹:۱۰ تا ۰۹:۳۰  │
│  دکتر: [نام دکتر]            │
│  ─────────────────────────── │
│  موبایل بیمار: [___________] │ ← فقط این فیلد!
│  [ثبت نوبت]  [انصراف]        │
└──────────────────────────────┘
```

- **تنها یک فیلد ورودی**: شماره موبایل بیمار
- doctor_uuid + slot_start + slot_end از slot کلیک‌شده پیش‌پر هستند
- Submit → `POST /api/v1/admin/appointment` با `{ doctor_uuid, slot_start, slot_end, patient_mobile }`
- موفقیت → slot خالی تبدیل به کارت نوبت `pending` می‌شود (بدون reload کل صفحه)

**برای نقش دکتر/منشی** (نه ادمین):
- بیمار را با موبایل پیدا کند
- اگر کاربر یافت نشد → خطا: "بیمار با این شماره در سیستم یافت نشد"
- (نمی‌توان کاربر جدید از اینجا ساخت)

### Doctor Tabs در داشبورد کلینیک

وقتی `primaryRole === 'clinic'`:
- تب‌های دکتر بالای نمای زمانبندی نمایش داده شوند
- هر تب: نام دکتر
- کلیک روی تب → نمایش نوبت‌های آن دکتر برای تاریخ انتخابی
- داده‌ها از `GET /api/v1/admin/appointments?date=...` فیلتر شده با `doctor_uuid`
- برای گرفتن لیست دکترهای کلینیک: از نوبت‌های برگشتی unique doctor_name/uuid استخراج شود

---

## کامپوننت‌های جدید Frontend

### `PersianCalendar.tsx`
```tsx
interface Props {
  value: string;           // YYYY-MM-DD
  onChange: (v: string) => void;
  onClose: () => void;
}
```
- محاسبه ماه/سال شمسی از `Intl.DateTimeFormat('fa-IR-u-ca-persian')`
- گرید ۷ ستونه با padding برای روز اول ماه
- تبدیل روز انتخابی شمسی → Gregorian: از `new Date()` با تاریخ ISO ساخته‌شده

### `AppointmentStatusDropdown.tsx`
```tsx
interface Props {
  uuid: string;
  currentStatus: string;
  onChanged: () => void;
}
```
- دکمه badge با `▼`
- dropdown با لیست وضعیت‌ها و دایره رنگی
- useMutation برای PATCH
- بعد از موفقیت: invalidate query

---

## فایل‌های تأثیرپذیر

| فایل | تغییر |
|------|-------|
| `assets/admin/pages/AppointmentsPage.tsx` | بازنویسی کامل |
| `assets/admin/components/ui/PersianCalendar.tsx` | جدید |
| `assets/admin/components/ui/PersianDateInput.tsx` | افزودن PersianCalendar popup |
| `assets/admin/components/ui/AppointmentStatusDropdown.tsx` | جدید |
| `assets/admin/styles.css` | CSS schedule view + status colors |
| `src/Admin/Controller/AdminApiController.php` | today-stats + PATCH status |
| `src/Appointment/Controller/AppointmentController.php` | بهبود slots |
| `docs/api/admin.md` | مستندسازی endpoint‌های جدید |
| `docs/api/appointment.md` | مستندسازی بهبود slots |

---

## منطق محاسبه Slot‌ها (SlotCalculatorService)

اولویت‌بندی:
1. **Holiday** (تعطیلات) → هیچ slot‌ای نیست
2. **Date Override** با `active: false` → هیچ slot‌ای نیست
3. **Date Override** با `active: true` → از `custom_slots` آن روز استفاده شود
4. **Weekly Schedule** → از session‌های روز هفته مربوطه (ایندکس: شنبه=0 ... جمعه=6)

خروجی slot بعد از محاسبه و حذف slot‌های رزرو‌شده:
```json
{ "start": 1718438400, "end": 1718439600, "start_time": "09:00", "end_time": "09:20", "location_id": 1973 }
```

این endpoint **فقط slot‌های خالی** برمی‌گرداند (booked slots حذف شده‌اند).

---

## نکات پیاده‌سازی

1. **Date-based**: queryKey شامل `selectedDate` — پیش‌فرض امروز (Gregorian)
2. **Doctor state**: یک state مشترک `selectedDoctorUuid` برای هر دو نما
3. **Schedule view — دو query موازی**:
   - `useQuery(['slots', doctorUuid, date])` → slot‌های خالی از `/api/v1/appointment-slots`
   - `useQuery(['appointments', date, doctorUuid])` → نوبت‌های پر از `/api/v1/admin/appointments`
   - Merge در frontend: آرایه کامل با `is_available` flag
4. **Slot کلیک**: `onSlotClick(slot)` → `setBookingSlot(slot)` → mini modal با فقط یک فیلد موبایل
5. **Optimistic lock**: PATCH status باید `version` را از نوبت بگیرد
6. **Status transitions**: فقط انتقال‌های مجاز از وضعیت فعلی در dropdown نشان داده شود
7. **بررسی schedule**: اگر دکتر schedule نداشت هشدار نشان بده + لینک تنظیمات دکتر
8. **محور زمان RTL**: CSS grid دو ستونه — کارت‌ها چپ، محور زمان راست
9. **Reload نکن**: بعد از هر عملیات فقط `queryClient.invalidateQueries`
10. **وضعیت‌های درست**: `pending`, `confirmed`, `completed`, `cancelled_by_doctor`, `cancelled_by_user`, `no_show`, `expired`

---

## ⭐ الزام دیزاین: دقیقاً شبیه تصاویر مرجع

> **این صفحه باید pixel-perfect شبیه تصاویر مرجع باشد. هیچ اجزایی از دیزاین نباید تغییر کند.**

---

### همه تاریخ‌ها شمسی (Jalali)

- **تمام** تاریخ‌های نمایشی — فیلتر، جدول، کارت‌ها، محور زمان، تقویم popup — باید شمسی باشند
- از `Intl.DateTimeFormat('fa-IR-u-ca-persian')` برای تبدیل استفاده شود
- input فیلتر تاریخ: مقدار داخلی `YYYY-MM-DD` میلادی باشد (برای API) ولی **نمایش** شمسی
- تقویم popup باید ماه/سال/روز شمسی نشان دهد
- هیچ تاریخی به فرمت `mm/dd/yyyy` یا میلادی به کاربر نمایش داده نشود

---

### مشخصات دقیق دیزاین از تصاویر مرجع

#### Stats Bar
```
┌───────────────────────────────────────────────────────────────────┐
│  کل نوبت‌های امروز  │  نوبت‌های انجام شده  │  مراجعین در انتظار  │  نوبت‌های لغو شده  │
│    [آیکون بنفش]     │    [آیکون سبز]       │    [آیکون نارنجی]   │    [آیکون قرمز]    │
│       ۲۳۶           │       ۲۰۰             │       ۲۳۶           │       ۱۲           │
└───────────────────────────────────────────────────────────────────┘
```
- یک کارت سفید با border کمرنگ — ۴ بخش با divider عمودی بینشان
- آیکون‌ها: دایره رنگی با SVG گرافیکی داخل (نه heroicon ساده)
  - بنفش: grid/شبکه نقطه‌ای
  - سبز: فیگور آدم
  - نارنجی: ساعت شنی (hourglass)
  - قرمز: دایره با X
- عدد bold بزرگ + متن label زیرش (ترتیب: label بالا، عدد پایین — از راست به چپ)

#### Toolbar
```
[+ نوبت جدید] [≡] | [نمایش جدولی] [زمانبندی] | [پرسنل را انتخاب کنید... ▼] | [<] [۱۴۰۳/۰۶/۰۵] [>] [📅]
```
- دکمه "نوبت جدید": آبی تیره، آیکون +، گرد
- دکمه کنار آن: خاکستری border، آیکون ≡ (list/filter icon)
- تب‌های نما: "نمایش جدولی" (active=پس‌زمینه سفید، border) | "زمانبندی" (غیرactive=متن خاکستری)، کنار هم در یک pill container خاکستری کمرنگ
- Dropdown پرسنل: کشیده، placeholder "پرسنل را انتخاب کنید..."، فاصله زیاد بین تب‌ها و date nav
- Date nav: دو فلش `<` `>`، تاریخ شمسی وسط، آیکون تقویم در راست‌ترین موقعیت

#### جدول — ستون‌های هدر دکتر
```
┌─────────────────────┬──────────────────────┐
│     دکتر فتحی       │    دکتر امینی فر      │  ← سرتیتر گروه‌بندی دکتر
├──────┬──────┬───────┼───────┬──────┬────────┤
│ شروع │ پایان│ سرویس │ شروع  │ پایان│ سرویس  │  ← سرتیتر ستون‌ها
```
- وقتی چند دکتر: ستون‌های هر دکتر کنار هم، با سرتیتر دکتر spanning

#### Status Badge (inline قابل کلیک)
- شکل: pill (border-radius کامل)
- محتوا: `▼ [متن وضعیت]`
- رنگ‌بندی pill متناسب با وضعیت (همان رنگ‌های تعریف‌شده)

#### Status Dropdown (کلیک روی badge)
```
┌──────────────────────┐
│ ○ ثبت شده            │  ← دایره آبی توخالی + متن آبی
│ ○ قطعی شده           │  ← دایره سبز توخالی + متن سبز
│ ○ در حال پیگیری      │  ← دایره نارنجی توخالی + متن نارنجی
│ ○ سالن               │  ← دایره بنفش توخالی + متن بنفش
│ ● ویزیت شده          │  ← دایره سبز پر (وضعیت فعلی) + متن سبز
│ ○ لغو شده            │  ← دایره قرمز توخالی + متن قرمز
└──────────────────────┘
```
- کارت سفید با shadow
- هر گزینه: دایره رنگی (filled=وضعیت فعلی، outlined=بقیه) + متن رنگی
- با کلیک روی گزینه: PATCH و بسته شدن dropdown

#### دکمه عملیات
- متن "عملیات" + "..." در یک pill خاکستری کمرنگ
- یا فقط "..." به عنوان icon button

#### تقویم Popup (کلیک روی آیکون 📅)
```
┌─────────────────────────────────────┐
│  <    تیر ۱۴۰۴    >                 │
│  شنبه  یکشنبه  دوشنبه  ...  جمعه    │
│   ۱     ۲      ۳    ۴    ۵    ۶    ۷  │
│   ۸     ۹     ۱۰   ۱۱  [۱۲]  ۱۳  ۱۴ │  ← ۱۲ = روز انتخابی (circle آبی)
│  ...                                 │
└─────────────────────────────────────┘
```
- ظاهر: کارت سفید، border کمرنگ، shadow
- header: نام ماه شمسی + سال + دکمه‌های ماه قبل/بعد
- روز هفته: ش | ی | د | س | چ | پ | ج
- روز انتخابی: circle آبی solid
- امروز: circle خاکستری کمرنگ (اگر با انتخابی فرق داشت)
- Popup مانند calendar بسته می‌شود وقتی خارج از آن کلیک شود

#### Pagination
```
< 1 2 3 4 5 ... 20 >
```
- عدد فعال: آبی bold
- دکمه‌های قبل/بعد: فلش `<` `>`
