# پرامپت: داشبورد چند-نقشه (Multi-Role Dashboard) ## هدف کلی پنل `/admin/` باید علاوه بر ادمین، برای نقش‌های زیر نیز کار کند — هر نقش فقط بخش‌هایی می‌بیند که به آن دسترسی دارد: | نقش | نام فارسی | ROLE در Symfony | |-----|-----------|-----------------| | ادمین سیستم | مدیر کل | `ROLE_ADMIN` | | صاحب کلینیک | مالک کلینیک | `ROLE_CLINIC` | | دکتر عضو کلینیک | پزشک | `ROLE_DOCTOR` | | منشی | منشی | `ROLE_SECRETARY` | --- ## وضعیت فعلی (مهم — قبل از تغییر بخوان) ### بک‌اند - کلاس `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` - `DoctorSecretary`: فیلد `doctor` (ManyToOne)، `secretary` (ManyToOne به User)، `permissions` (JSON): ``` { version:1, resources: { appointments: { view, create, cancel, update_status }, addresses: { view, create, update, delete }, clinic_info: { view, update }, insurances: { view, create, update, delete } }} ``` - 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 فعلی --- ## مرحله ۱ — endpoint شناسایی کاربر (بک‌اند) ### فایل جدید: `src/Auth/Controller/MeController.php` ``` GET /api/v1/me [IS_AUTHENTICATED_FULLY] ``` پاسخ: ```json { "success": true, "data": { "uuid": "...", "mobile": "09...", "name": "دکتر علی ...", "roles": ["ROLE_USER", "ROLE_DOCTOR"], "primary_role": "doctor", "context": { "doctor_uuid": "...", "doctor_name": "دکتر علی احمدی" } } } ``` **قانون `primary_role`** (اولویت‌بندی): - `ROLE_ADMIN` → `"admin"` - `ROLE_CLINIC` → `"clinic"` - `ROLE_DOCTOR` → `"doctor"` - `ROLE_SECRETARY` → `"secretary"` - بقیه → `"user"` **پر کردن `context`**: - `ROLE_DOCTOR`: از `DoctorRepository::findByUser($user)` → `{doctor_uuid, doctor_name}` - `ROLE_CLINIC`: از `ClinicRepository::findOneBy(['user' => $user])` → `{clinic_uuid, clinic_name, clinic_logo}` - `ROLE_SECRETARY`: از `DoctorSecretaryRepository::findActiveBySecretary($user)` (متد جدید) → `{doctor_uuid, doctor_name, secretary_uuid, permissions}` - اگر موجودیت پیدا نشد: `context: null` **متد جدید در `DoctorSecretaryRepository`**: ```php public function findActiveBySecretary(User $user): ?DoctorSecretary { return $this->findOneBy(['secretary' => $user, 'active' => true]); } ``` **security.yaml** — endpoint `/api/v1/me` را به firewall `api` اضافه کن (نه public_endpoints، چون نیاز به احراز هویت دارد — فایروال `api` آن را پوشش می‌دهد). --- ## مرحله ۲ — به‌روز کردن `authStore.ts` (فرانت‌اند) ```typescript // assets/admin/stores/authStore.ts interface AuthState { token: string | null; refreshToken: string | null; isAuthenticated: boolean; // فیلدهای جدید: userUuid: string | null; userName: string | null; primaryRole: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'user' | null; context: Record | null; } ``` - متد `login(token, refreshToken)`: بعد از ذخیره token، یک `GET /api/v1/me` بزند و نتیجه را ذخیره کند - متد `logout()`: همه فیلدها را پاک کند - متد جدید `fetchMe()`: `GET /api/v1/me` و update store — در `App.tsx` هنگام mount فراخوانی شود (اگر token موجود بود اما `primaryRole` خالی بود، تا بعد از reload صفحه role بازیابی شود) --- ## مرحله ۳ — محافظت route ها (فرانت‌اند) ### در `App.tsx`، کامپوننت `RoleRoute` اضافه کن: ```tsx function RoleRoute({ roles, children }: { roles: string[]; children: ReactNode }) { const primaryRole = useAuthStore(s => s.primaryRole); if (!primaryRole) return
در حال بارگذاری...
; if (!roles.includes(primaryRole)) return ; return <>{children}; } ``` **Route های فقط ادمین** (با `` بپوشان): - `/admin/users`, `/admin/users/:uuid` - `/admin/payments` - `/admin/settlements` - `/admin/representations`, `/admin/representations/:uuid` - `/admin/comments` - `/admin/ratings` - `/admin/sms` - `/admin/categories` - `/admin/blogs`, `/admin/blogs/new`, `/admin/blogs/:uuid/edit` - `/admin/secretaries` - `/admin/clinics` (لیست کل کلینیک‌ها) **Route های admin + clinic**: - `/admin/clinics/:uuid` — ادمین همه را می‌بیند، clinic فقط کلینیک خودش را - `/admin/doctors` — ادمین همه، clinic فقط پزشکان کلینیکش **Route های مشترک همه نقش‌ها**: - `/admin/dashboard` - `/admin/appointments`, `/admin/appointments/:uuid` **Route های جدید**: - `/admin/my-clinic` → `` — فقط `ROLE_CLINIC` --- ## مرحله ۴ — Sidebar پویا (فرانت‌اند) ### فایل `assets/admin/components/layout/Sidebar.tsx` ساختار `sections` را به یک تابع تبدیل کن که `primaryRole` و `context` می‌گیرد: ```tsx function buildSections( primaryRole: string | null, context: Record | null ): Section[] ``` #### ادمین — همه لینک‌های فعلی (بدون تغییر) #### صاحب کلینیک (`clinic`): ``` عمومی: • داشبورد /admin/dashboard • کلینیک من /admin/my-clinic • پزشکان /admin/doctors مدیریت: • نوبت‌ها /admin/appointments ``` #### دکتر (`doctor`): ``` عمومی: • داشبورد /admin/dashboard مدیریت: • نوبت‌های من /admin/appointments ``` #### منشی (`secretary`) — بر اساس `context.permissions.resources`: ``` عمومی: • داشبورد /admin/dashboard مدیریت (شرطی): • نوبت‌ها /admin/appointments ← اگر appointments.view = true ``` در Sidebar، permissions را از `useAuthStore(s => s.context)` بخوان. --- ## مرحله ۵ — endpoint های داشبورد جدید (بک‌اند) ### فایل جدید: `src/Dashboard/Controller/DashboardController.php` سه endpoint جداگانه — هر سه از `BaseController` extend می‌کنند: --- ### `GET /api/v1/dashboard/clinic` `[ROLE_CLINIC]` پاسخ: ```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 } ] } ``` پیاده‌سازی: - کلینیک را از `ClinicRepository::findOneBy(['user' => $user])` بگیر - اگر نبود: `throw new AppException('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`: لیست پزشکان کلینیک با شمارش نوبت امروز آن‌ها --- ### `GET /api/v1/dashboard/doctor` `[ROLE_DOCTOR]` پاسخ: ```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":"..." } ] } ``` پیاده‌سازی: - دکتر از `DoctorRepository::findByUser($user)` — اگر نبود `404` - `today_appointments`: نوبت‌های این دکتر با `slot_start` در بازه ابتدا تا انتهای امروز - `tomorrow_appointments`: همان برای فردا - `avg_rating`: AVG(overall) از جدول `ratings` برای این دکتر - `clinics`: کلینیک‌هایی که این دکتر در `clinic_doctors` آن‌هاست --- ### `GET /api/v1/dashboard/secretary` `[ROLE_SECRETARY]` پاسخ: ```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" } ] } ``` پیاده‌سازی: - از `DoctorSecretaryRepository::findActiveBySecretary($user)` اولین رابطه فعال بگیر - اگر نبود: `throw new AppException('ERR_FORBIDDEN_001', 'دسترسی منشی تنظیم نشده', 403)` - بررسی `appointments.view = true` در permissions — اگر false بود، `today_appointments` آرایه خالی برگردان - نوبت‌های دکتر مربوطه را برگردان --- ## مرحله ۶ — DashboardPage.tsx چند-نقشه (فرانت‌اند) فایل `assets/admin/pages/DashboardPage.tsx` را به این شکل بازنویسی کن: ```tsx export default function DashboardPage() { const primaryRole = useAuthStore(s => s.primaryRole); if (!primaryRole) return ; if (primaryRole === 'admin') return ; if (primaryRole === 'clinic') return ; if (primaryRole === 'doctor') return ; if (primaryRole === 'secretary') return ; return

نقش شما برای داشبورد تعریف نشده

; } ``` **AdminDashboard**: کد فعلی DashboardPage عیناً — فقط در یک تابع بپیچ **ClinicDashboard**: - یک query به `/api/v1/dashboard/clinic` - ۴ کارت KPI: تعداد پزشکان / نوبت امروز / نوبت این ماه / دعوتنامه در انتظار - جدول نوبت‌های امروز (ستون: بیمار، پزشک، زمان، وضعیت) - لیست پزشکان با تعداد نوبت امروز - دکمه "مدیریت کلینیک" → navigate به `/admin/my-clinic` **DoctorDashboard**: - یک query به `/api/v1/dashboard/doctor` - ۴ کارت KPI: نوبت امروز / فردا / این ماه / میانگین امتیاز (با ستاره) - جدول نوبت‌های امروز (ستون: بیمار، موبایل `dir="ltr"`, زمان، وضعیت) - لیست کلینیک‌های عضو به شکل badge **SecretaryDashboard**: - یک query به `/api/v1/dashboard/secretary` - نام دکتر مربوطه در header کارت - ۲ کارت KPI: نوبت امروز / فردا - جدول نوبت‌های امروز - لیست مجوزهای فعال با آیکون ✓ --- ## مرحله ۷ — صفحه "کلینیک من" (فرانت‌اند) ### فایل جدید: `assets/admin/pages/MyClinicPage.tsx` ```tsx export default function MyClinicPage() { const context = useAuthStore(s => s.context); const clinicUuid = context?.clinic_uuid; if (!clinicUuid) return (

کلینیک شما هنوز ثبت نشده است.

); // همان محتوای ClinicDetailPage — اما uuid از context // دکمه "حذف کلینیک" نشان داده نشود // بقیه همه فعال: ویرایش، تغییر وضعیت، آپلود لوگو، گالری، دعوت پزشک } ``` بهترین رویکرد: کد مشترک را از `ClinicDetailPage.tsx` در یک کامپوننت `ClinicDetailView` جدا کن که `uuid` و `showDeleteButton` را به عنوان prop می‌گیرد. هر دو صفحه از آن استفاده کنند. --- ## مرحله ۸ — نوبت‌های فیلترشده (بک‌اند + فرانت‌اند) ### بک‌اند — endpoint جدید: `GET /api/v1/my/appointments` `[IS_AUTHENTICATED_FULLY]` در یک Controller جدید یا در `AppointmentController`: ``` GET /api/v1/my/appointments?page=1&limit=15&status=...&search=... ``` بر اساس نقش فیلتر: - `ROLE_ADMIN`: همه نوبت‌ها (redirect به `/api/v1/admin/appointments`) - `ROLE_CLINIC`: نوبت‌هایی که doctor آن در `clinic_doctors` این کلینیک است - `ROLE_DOCTOR`: نوبت‌های این دکتر - `ROLE_SECRETARY`: نوبت‌های دکتری که این منشی به آن وصل است (اگر `appointments.view = true`) پاسخ: همان فرمت `paginated()` موجود. ### فرانت‌اند — `AppointmentsPage.tsx` ```tsx const primaryRole = useAuthStore(s => s.primaryRole); const endpoint = primaryRole === 'admin' ? `/api/v1/admin/appointments?...` : `/api/v1/my/appointments?...`; ``` --- ## نکات مهم پیاده‌سازی ### CSS / UI — فقط template CSS - `.card`, `.card-pad`, `.badge.green/.blue/.amber/.violet/.gray` - `.btn.primary/.ghost/.soft/.sm` - `.skeleton` برای loading - `.empty` برای حالت خالی - گرادیان آواتار: `HUES_LIST = [256, 205, 162, 295, 272]` با OKLCH: `background: \`linear-gradient(145deg, oklch(0.62 0.15 ${hue}), oklch(0.48 0.16 ${hue}))\`` - هیچ Tailwind نیست ### پاسخ‌های API - `$this->success($data)` → `{ success, data: $data }` — برای single resource - `$this->paginated($items, $total, $page, $limit)` → `{ success, data: $items[], meta: {...} }` - `$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 بعد از هر مرحله: `ddev exec php bin/console cache:clear` و `ddev exec yarn dev` --- ## خلاصه فایل‌های جدید/تغییریافته ### بک‌اند (جدید) - `src/Auth/Controller/MeController.php` - `src/Dashboard/Controller/DashboardController.php` ### بک‌اند (تغییر) - `src/Secretary/Repository/DoctorSecretaryRepository.php` — اضافه: `findActiveBySecretary()` ### فرانت‌اند (تغییر) - `assets/admin/stores/authStore.ts` — اضافه: primaryRole، context، fetchMe() - `assets/admin/App.tsx` — اضافه: RoleRoute، fetchMe در mount، route های جدید - `assets/admin/components/layout/Sidebar.tsx` — تبدیل به پویا - `assets/admin/pages/DashboardPage.tsx` — multi-role - `assets/admin/pages/AppointmentsPage.tsx` — endpoint پویا ### فرانت‌اند (جدید) - `assets/admin/pages/MyClinicPage.tsx`