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

24 KiB
Raw Blame History

بازطراحی کامل صفحه نوبت‌ها (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):

  • pendingconfirmed | cancelled_by_doctor | cancelled_by_user | expired
  • confirmedcompleted | 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‌های پر را هم برگرداند با اطلاعات نوبت:

{
  "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:

#[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:

{
  "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

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

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‌های رزرو‌شده:

{ "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
  • دکمه‌های قبل/بعد: فلش < >