Files
clinicpro/.claude/prompt/appointments-redesign.md
T

464 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# بازطراحی کامل صفحه نوبت‌ها (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
- دکمه‌های قبل/بعد: فلش `<` `>`