diff --git a/.claude/prompt/multi-role-dashboard.md b/.claude/prompt/multi-role-dashboard.md index eaa5e522..19a36561 100644 --- a/.claude/prompt/multi-role-dashboard.md +++ b/.claude/prompt/multi-role-dashboard.md @@ -13,14 +13,44 @@ --- -## وضعیت فعلی (مهم — قبل از تغییر بخوان) +## وضعیت فعلی API (مهم — قبل از پیاده‌سازی بخوان) + +### API های موجود (استفاده کن، تغییر نده) + +| Endpoint | داکیومنت | توضیح | +|----------|----------|-------| +| `GET /oauth/userinfo` | [auth.md](../docs/api/auth.md) | اطلاعات پایه کاربر — بدون `primary_role` و `context` | +| `GET /api/v1/admin/dashboard/stats` | [admin.md](../docs/api/admin.md) | KPI های داشبورد ادمین | +| `GET /api/v1/admin/dashboard/charts` | [admin.md](../docs/api/admin.md) | نمودار ۳۰ روزه ادمین | +| `GET /api/v1/admin/dashboard/recent` | [admin.md](../docs/api/admin.md) | آخرین فعالیت‌ها برای ادمین | +| `GET /api/v1/admin/appointments` | [admin.md](../docs/api/admin.md) | لیست همه نوبت‌ها — فقط ROLE_ADMIN | +| `GET /api/v1/appointments/doctor/{doctorUuid}` | [appointment.md](../docs/api/appointment.md) | نوبت‌های یک دکتر خاص | +| `GET /api/v1/appointments/user` | [appointment.md](../docs/api/appointment.md) | نوبت‌های کاربر جاری | +| `GET /api/v1/clinics/{uuid}` | [clinic.md](../docs/api/clinic.md) | جزئیات کلینیک | +| `GET /api/v1/admin/clinics` | [admin.md](../docs/api/admin.md) | لیست کلینیک‌ها — فقط ROLE_ADMIN | + +### API های جدید که باید ساخته شوند + +| Endpoint | فایل کنترلر | توضیح | +|----------|-------------|-------| +| `GET /api/v1/me` | `src/Auth/Controller/MeController.php` | شناسایی نقش + context کاربر | +| `GET /api/v1/dashboard/clinic` | `src/Dashboard/Controller/DashboardController.php` | داشبورد صاحب کلینیک | +| `GET /api/v1/dashboard/doctor` | همان | داشبورد دکتر | +| `GET /api/v1/dashboard/secretary` | همان | داشبورد منشی | +| `GET /api/v1/my/appointments` | `src/Appointment/Controller/MyAppointmentsController.php` | نوبت‌های فیلترشده بر اساس نقش | + +> بعد از ساخت هر API جدید، فایل مستندات متناظر در `docs/api/` را نیز به‌روز کن. + +--- + +## وضعیت فعلی کد (مهم — قبل از تغییر بخوان) ### بک‌اند - کلاس `AdminApiController` با `#[IsGranted('ROLE_ADMIN')]` روی کل کلاس — تمام endpoint های داشبورد فعلی فقط برای ادمین - موجودیت‌ها: - `User`: فیلد `roles: array` — مقادیر ممکن: `ROLE_USER`, `ROLE_ADMIN`, `ROLE_DOCTOR`, `ROLE_CLINIC`, `ROLE_SECRETARY` - `Clinic`: فیلد `user` (ManyToOne به User) — صاحب کلینیک. رابطه ManyToMany با `Doctor` از طریق جدول `clinic_doctors` - - `Doctor`: فیلد `user` (OneToOne به User). دارای `mobileNumber` و رابطه با `Specialty` + - `Doctor`: فیلد `user` (OneToOne به User). دارای `mobileNumber` و رابطه با `Specialty`. متد `findByUser(User)` در `DoctorRepository` موجود است. - `DoctorSecretary`: فیلد `doctor` (ManyToOne)، `secretary` (ManyToOne به User)، `permissions` (JSON): ``` { version:1, resources: { @@ -30,22 +60,19 @@ insurances: { view, create, update, delete } }} ``` +- `ClinicRepository::findByUser(User $user)` موجود است. +- `DoctorRepository::findByUser(User $user)` موجود است. - JWT: از LexikJWTBundle — payload شامل: `username` (mobile_number)، `roles` (آرایه)، `iat`، `exp` -- endpoint های فعلی داشبورد: - - `GET /api/v1/admin/dashboard/stats` → ۱۲ KPI عمومی - - `GET /api/v1/admin/dashboard/charts` → نمودار ۳۰ روزه - - `GET /api/v1/admin/dashboard/recent` → آخرین نوبت‌ها، پرداخت‌ها، کاربران - - همه با `ROLE_ADMIN` ### فرانت‌اند - `authStore.ts` (Zustand + persist در `clinicpro-auth`): فقط `token`، `refreshToken`، `isAuthenticated` - `Sidebar.tsx`: لیست ثابت — بدون هیچ فیلتر نقشی - `App.tsx`: همه routes با `PrivateRoute` (فقط isAuthenticated بررسی می‌شود) -- `DashboardPage.tsx`: سه query + نمودار Recharts + mini lists فعلی +- `DashboardPage.tsx`: سه query به `/api/v1/admin/dashboard/*` + نمودار Recharts + mini lists --- -## مرحله ۱ — endpoint شناسایی کاربر (بک‌اند) +## مرحله ۱ — endpoint شناسایی کاربر (بک‌اند) — API جدید ### فایل جدید: `src/Auth/Controller/MeController.php` @@ -53,6 +80,8 @@ GET /api/v1/me [IS_AUTHENTICATED_FULLY] ``` +> این endpoint با `/oauth/userinfo` فرق دارد — علاوه بر اطلاعات پایه، `primary_role` و `context` (مثل uuid دکتر/کلینیک) را هم برمی‌گرداند. + پاسخ: ```json { @@ -80,7 +109,7 @@ GET /api/v1/me [IS_AUTHENTICATED_FULLY] **پر کردن `context`**: - `ROLE_DOCTOR`: از `DoctorRepository::findByUser($user)` → `{doctor_uuid, doctor_name}` -- `ROLE_CLINIC`: از `ClinicRepository::findOneBy(['user' => $user])` → `{clinic_uuid, clinic_name, clinic_logo}` +- `ROLE_CLINIC`: از `ClinicRepository::findByUser($user)` → `{clinic_uuid, clinic_name, clinic_logo}` - `ROLE_SECRETARY`: از `DoctorSecretaryRepository::findActiveBySecretary($user)` (متد جدید) → `{doctor_uuid, doctor_name, secretary_uuid, permissions}` - اگر موجودیت پیدا نشد: `context: null` @@ -92,7 +121,7 @@ public function findActiveBySecretary(User $user): ?DoctorSecretary } ``` -**security.yaml** — endpoint `/api/v1/me` را به firewall `api` اضافه کن (نه public_endpoints، چون نیاز به احراز هویت دارد — فایروال `api` آن را پوشش می‌دهد). +**داکیومنت**: بعد از پیاده‌سازی، endpoint را به `docs/api/auth.md` اضافه کن. --- @@ -206,11 +235,12 @@ function buildSections( --- -## مرحله ۵ — endpoint های داشبورد جدید (بک‌اند) +## مرحله ۵ — endpoint های داشبورد جدید (بک‌اند) — API های جدید ### فایل جدید: `src/Dashboard/Controller/DashboardController.php` -سه endpoint جداگانه — هر سه از `BaseController` extend می‌کنند: +سه endpoint جداگانه — هر سه از `BaseController` extend می‌کنند. +پاسخ با `$this->success($data)` — یعنی فرانت با `data?.data` می‌خواند. --- @@ -219,29 +249,33 @@ function buildSections( پاسخ: ```json { - "clinic": { "uuid":"...", "name":"...", "is_active": true, "logo":"..." }, - "stats": { - "total_doctors": 5, - "today_appointments": 12, - "this_month_appointments": 87, - "pending_invitations": 2 - }, - "today_appointments": [ - { "uuid":"...", "patient_name":"...", "doctor_name":"...", "slot_start": 1234567890, "status":"reserved" } - ], - "doctors": [ - { "uuid":"...", "name":"دکتر ...", "specialty":"...", "today_count": 3 } - ] + "success": true, + "data": { + "clinic": { "uuid":"...", "name":"...", "is_active": true, "logo":"..." }, + "stats": { + "total_doctors": 5, + "today_appointments": 12, + "this_month_appointments": 87, + "pending_invitations": 2 + }, + "today_appointments": [ + { "uuid":"...", "patient_name":"...", "doctor_name":"...", "slot_start": 1234567890, "status":"reserved" } + ], + "doctors": [ + { "uuid":"...", "name":"دکتر ...", "specialty":"...", "today_count": 3 } + ] + } } ``` پیاده‌سازی: -- کلینیک را از `ClinicRepository::findOneBy(['user' => $user])` بگیر -- اگر نبود: `throw new AppException('ERR_NOT_FOUND_001', 'کلینیک یافت نشد', 404)` +- کلینیک را از `ClinicRepository::findByUser($user)` بگیر +- اگر نبود: `return $this->error('ERR_NOT_FOUND_001', 'کلینیک یافت نشد', 404)` - `today_appointments` و `this_month_appointments`: از جدول `appointments` با JOIN به `clinic_doctors` فیلتر کن - `pending_invitations`: از `clinic_doctor_invitations` با `status='pending'` بشمار - `today_appointments` لیست: ۵ نوبت اخیر امروز این کلینیک (از طریق JOIN `clinic_doctors`) - `doctors`: لیست پزشکان کلینیک با شمارش نوبت امروز آن‌ها +- از DQL array hydration استفاده کن (`.getArrayResult()`) --- @@ -250,29 +284,33 @@ function buildSections( پاسخ: ```json { - "doctor": { "uuid":"...", "name":"...", "degree":"...", "profile_image":"..." }, - "stats": { - "today_appointments": 5, - "tomorrow_appointments": 3, - "this_month_appointments": 42, - "avg_rating": 4.7, - "total_ratings": 18 - }, - "today_appointments": [ - { "uuid":"...", "patient_name":"...", "patient_mobile":"...", "slot_start": 1234567890, "status":"reserved" } - ], - "clinics": [ - { "uuid":"...", "name":"کلینیک ...", "logo":"..." } - ] + "success": true, + "data": { + "doctor": { "uuid":"...", "name":"...", "degree":"..." }, + "stats": { + "today_appointments": 5, + "tomorrow_appointments": 3, + "this_month_appointments": 42, + "avg_rating": 4.7, + "total_ratings": 18 + }, + "today_appointments": [ + { "uuid":"...", "patient_name":"...", "patient_mobile":"...", "slot_start": 1234567890, "status":"reserved" } + ], + "clinics": [ + { "uuid":"...", "name":"کلینیک ...", "logo":"..." } + ] + } } ``` پیاده‌سازی: -- دکتر از `DoctorRepository::findByUser($user)` — اگر نبود `404` +- دکتر از `DoctorRepository::findByUser($user)` — اگر نبود `return $this->error(..., 404)` - `today_appointments`: نوبت‌های این دکتر با `slot_start` در بازه ابتدا تا انتهای امروز - `tomorrow_appointments`: همان برای فردا -- `avg_rating`: AVG(overall) از جدول `ratings` برای این دکتر +- `avg_rating`: AVG(overall) از جدول `ratings` برای این دکتر — با DQL - `clinics`: کلینیک‌هایی که این دکتر در `clinic_doctors` آن‌هاست +- از DQL array hydration استفاده کن --- @@ -281,24 +319,29 @@ function buildSections( پاسخ: ```json { - "doctor": { "uuid":"...", "name":"...", "degree":"..." }, - "permissions": { ... }, - "stats": { - "today_appointments": 4, - "tomorrow_appointments": 2 - }, - "today_appointments": [ - { "uuid":"...", "patient_name":"...", "patient_mobile":"...", "slot_start": 1234567890, "status":"reserved" } - ] + "success": true, + "data": { + "doctor": { "uuid":"...", "name":"...", "degree":"..." }, + "permissions": { "version": 1, "resources": { ... } }, + "stats": { + "today_appointments": 4, + "tomorrow_appointments": 2 + }, + "today_appointments": [ + { "uuid":"...", "patient_name":"...", "patient_mobile":"...", "slot_start": 1234567890, "status":"reserved" } + ] + } } ``` پیاده‌سازی: - از `DoctorSecretaryRepository::findActiveBySecretary($user)` اولین رابطه فعال بگیر -- اگر نبود: `throw new AppException('ERR_FORBIDDEN_001', 'دسترسی منشی تنظیم نشده', 403)` +- اگر نبود: `return $this->error('ERR_FORBIDDEN_001', 'دسترسی منشی تنظیم نشده', 403)` - بررسی `appointments.view = true` در permissions — اگر false بود، `today_appointments` آرایه خالی برگردان - نوبت‌های دکتر مربوطه را برگردان +**داکیومنت**: بعد از پیاده‌سازی، فایل `docs/api/dashboard.md` جدید بساز. + --- ## مرحله ۶ — DashboardPage.tsx چند-نقشه (فرانت‌اند) @@ -318,23 +361,26 @@ export default function DashboardPage() { } ``` -**AdminDashboard**: کد فعلی DashboardPage عیناً — فقط در یک تابع بپیچ +**AdminDashboard**: کد فعلی DashboardPage عیناً — فقط در یک تابع بپیچ. از endpointهای موجود `/api/v1/admin/dashboard/*` استفاده می‌کند. **ClinicDashboard**: -- یک query به `/api/v1/dashboard/clinic` +- یک query به `GET /api/v1/dashboard/clinic` (جدید) +- استخراج: `data?.data` (چون `$this->success()` یک‌بار nest می‌کند) - ۴ کارت KPI: تعداد پزشکان / نوبت امروز / نوبت این ماه / دعوتنامه در انتظار - جدول نوبت‌های امروز (ستون: بیمار، پزشک، زمان، وضعیت) - لیست پزشکان با تعداد نوبت امروز - دکمه "مدیریت کلینیک" → navigate به `/admin/my-clinic` **DoctorDashboard**: -- یک query به `/api/v1/dashboard/doctor` +- یک query به `GET /api/v1/dashboard/doctor` (جدید) +- استخراج: `data?.data` - ۴ کارت KPI: نوبت امروز / فردا / این ماه / میانگین امتیاز (با ستاره) - جدول نوبت‌های امروز (ستون: بیمار، موبایل `dir="ltr"`, زمان، وضعیت) - لیست کلینیک‌های عضو به شکل badge **SecretaryDashboard**: -- یک query به `/api/v1/dashboard/secretary` +- یک query به `GET /api/v1/dashboard/secretary` (جدید) +- استخراج: `data?.data` - نام دکتر مربوطه در header کارت - ۲ کارت KPI: نوبت امروز / فردا - جدول نوبت‌های امروز @@ -357,6 +403,7 @@ export default function MyClinicPage() { ); + // از endpoint موجود GET /api/v1/clinics/{uuid} استفاده کن // همان محتوای ClinicDetailPage — اما uuid از context // دکمه "حذف کلینیک" نشان داده نشود // بقیه همه فعال: ویرایش، تغییر وضعیت، آپلود لوگو، گالری، دعوت پزشک @@ -365,35 +412,48 @@ export default function MyClinicPage() { بهترین رویکرد: کد مشترک را از `ClinicDetailPage.tsx` در یک کامپوننت `ClinicDetailView` جدا کن که `uuid` و `showDeleteButton` را به عنوان prop می‌گیرد. هر دو صفحه از آن استفاده کنند. +> `GET /api/v1/clinics/{uuid}` و `PATCH /api/v1/clinic/{uuid}` هر دو موجودند — نیازی به API جدید نیست. + --- -## مرحله ۸ — نوبت‌های فیلترشده (بک‌اند + فرانت‌اند) +## مرحله ۸ — نوبت‌های فیلترشده (بک‌اند + فرانت‌اند) — API جدید ### بک‌اند — endpoint جدید: `GET /api/v1/my/appointments` `[IS_AUTHENTICATED_FULLY]` -در یک Controller جدید یا در `AppointmentController`: +فایل جدید: `src/Appointment/Controller/MyAppointmentsController.php` ``` GET /api/v1/my/appointments?page=1&limit=15&status=...&search=... ``` بر اساس نقش فیلتر: -- `ROLE_ADMIN`: همه نوبت‌ها (redirect به `/api/v1/admin/appointments`) +- `ROLE_ADMIN`: forward به همان query موجود در `AdminApiController::appointments()` - `ROLE_CLINIC`: نوبت‌هایی که doctor آن در `clinic_doctors` این کلینیک است -- `ROLE_DOCTOR`: نوبت‌های این دکتر +- `ROLE_DOCTOR`: نوبت‌های این دکتر — از `DoctorRepository::findByUser($user)` uuid بگیر - `ROLE_SECRETARY`: نوبت‌های دکتری که این منشی به آن وصل است (اگر `appointments.view = true`) -پاسخ: همان فرمت `paginated()` موجود. +پاسخ: همان فرمت `$this->paginated()` موجود: +```json +{ + "success": true, + "data": [ { "uuid":"...", "doctor_name":"...", "patient_name":"...", "slot_start":..., "status":"..." } ], + "meta": { "totalRecords": 100, "totalPages": 7, "currentPage": 1 } +} +``` + +**داکیومنت**: بعد از پیاده‌سازی، endpoint را به `docs/api/appointment.md` اضافه کن. ### فرانت‌اند — `AppointmentsPage.tsx` ```tsx const primaryRole = useAuthStore(s => s.primaryRole); const endpoint = primaryRole === 'admin' - ? `/api/v1/admin/appointments?...` - : `/api/v1/my/appointments?...`; + ? `/api/v1/admin/appointments` + : `/api/v1/my/appointments`; ``` +> برای ادمین از endpoint موجود استفاده می‌شود؛ برای بقیه نقش‌ها از endpoint جدید. + --- ## نکات مهم پیاده‌سازی @@ -408,22 +468,29 @@ const endpoint = primaryRole === 'admin' - هیچ Tailwind نیست ### پاسخ‌های API -- `$this->success($data)` → `{ success, data: $data }` — برای single resource -- `$this->paginated($items, $total, $page, $limit)` → `{ success, data: $items[], meta: {...} }` +- `$this->success($data)` → `{ success, data: $data }` — فرانت با `data?.data` می‌خواند +- `$this->paginated($items, $total, $page, $limit)` → `{ success, data: $items[], meta: {...} }` — فرانت با `data?.data` و `data?.meta?.totalRecords` - `$this->error(...)` → `{ success:false, errors:[...] }` -### ترتیب اجرا (پیشنهادی) -1. `MeController` + بک‌اند test با curl -2. `authStore.ts` — اضافه کردن fetchMe + فیلدهای جدید -3. `App.tsx` — fetchMe در mount -4. `DashboardController` — هر سه endpoint -5. `DashboardPage.tsx` — sub-dashboardها -6. `Sidebar.tsx` — پویا -7. `App.tsx` — RoleRoute -8. `MyClinicPage.tsx` -9. `AppointmentsPage.tsx` — فیلتر endpoint +### بک‌اند — قوانین کلی +- همه Admin queries از DQL array hydration استفاده کنند (`.getArrayResult()`) +- Timestamps همه integer Unix هستند +- نوبت‌های «امروز»: بازه `strtotime('today midnight')` تا `strtotime('tomorrow midnight') - 1` -بعد از هر مرحله: `ddev exec php bin/console cache:clear` و `ddev exec yarn dev` +### ترتیب اجرا (پیشنهادی) +1. `DoctorSecretaryRepository::findActiveBySecretary()` — متد جدید +2. `MeController` — بک‌اند، تست با curl +3. `authStore.ts` — اضافه کردن fetchMe + فیلدهای جدید +4. `App.tsx` — fetchMe در mount +5. `DashboardController` — هر سه endpoint +6. `DashboardPage.tsx` — sub-dashboardها +7. `Sidebar.tsx` — پویا +8. `App.tsx` — RoleRoute +9. `MyClinicPage.tsx` +10. `MyAppointmentsController` — بک‌اند +11. `AppointmentsPage.tsx` — فیلتر endpoint +12. بعد از هر مرحله بک‌اند: `ddev exec php bin/console cache:clear` +13. بعد از هر مرحله فرانت‌اند: `ddev exec yarn dev` --- @@ -432,6 +499,7 @@ const endpoint = primaryRole === 'admin' ### بک‌اند (جدید) - `src/Auth/Controller/MeController.php` - `src/Dashboard/Controller/DashboardController.php` +- `src/Appointment/Controller/MyAppointmentsController.php` ### بک‌اند (تغییر) - `src/Secretary/Repository/DoctorSecretaryRepository.php` — اضافه: `findActiveBySecretary()` @@ -445,3 +513,8 @@ const endpoint = primaryRole === 'admin' ### فرانت‌اند (جدید) - `assets/admin/pages/MyClinicPage.tsx` + +### داکیومنت (به‌روزرسانی/جدید) +- `docs/api/auth.md` — اضافه: `GET /api/v1/me` +- `docs/api/appointment.md` — اضافه: `GET /api/v1/my/appointments` +- `docs/api/dashboard.md` — **فایل جدید** برای سه endpoint داشبورد