From 92a258832b3961ac554acce181bcd9ab2411a98c Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 11 Jun 2026 13:58:46 +0330 Subject: [PATCH] feat: complete redesign of the appointments page with new UI and functionality --- .claude/prompt/appointments-redesign.md | 463 ++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 .claude/prompt/appointments-redesign.md diff --git a/.claude/prompt/appointments-redesign.md b/.claude/prompt/appointments-redesign.md new file mode 100644 index 00000000..caacfde5 --- /dev/null +++ b/.claude/prompt/appointments-redesign.md @@ -0,0 +1,463 @@ +# بازطراحی کامل صفحه نوبت‌ها (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 +- دکمه‌های قبل/بعد: فلش `<` `>`