# بازطراحی کامل صفحه نوبت‌ها (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 - دکمه‌های قبل/بعد: فلش `<` `>`