Files
clinicpro/.claude/prompt/representation-admin-panel-access.md
T
hamed fe73fa1a05 feat: add ROLE_REPRESENTATION access to admin panel for managing doctors and clinics
- Updated authStore to include 'representation' role.
- Modified DoctorFormPage and DoctorsPage to handle different endpoints based on user role.
- Created new RepresentationActionController for handling doctor and clinic creation by representatives.
- Added new API endpoints for representatives to manage doctors, clinics, and view appointments.
- Updated documentation to reflect new role and API changes.
2026-06-19 13:20:40 +03:30

224 lines
16 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.
# دسترسی نقش نماینده (ROLE_REPRESENTATION) به پنل ادمین
## پروژه
`clinicpro` (Backend + Admin React SPA). کاملاً داخل همین پروژه است؛ پرامپت همتای frontend عمومی لازم نیست.
## زمینه
کاربرانِ دارای نقش `ROLE_REPRESENTATION` (نماینده‌ی فروش/شهر) باید بتوانند وارد پنل ادمین (`https://clinic-pro.ddev.site/admin/dashboard`) شوند و کارهای محدودِ خودشان را انجام دهند:
1. **افزودن پزشک**
2. **افزودن کلینیک**
3. **دیدن نوبت‌های پزشکانی که زیرمجموعه‌ی همان نماینده‌اند** (پزشکانی که `Doctor.representationId` = id همان نماینده است)
4. **داشبورد اختصاصی خودشان** (درآمد/کمیسیون ماهانه و سالانه که از قبل موجود است)
الان این نقش عملاً به پنل راه ندارد و حتی اگر داشته باشد همه‌چیز مسدود است. شکاف‌های دقیق:
- `App\Auth\Controller\AuthController::resolvePrimaryRole()` هیچ شاخه‌ای برای `ROLE_REPRESENTATION` ندارد → چنین کاربری `primary_role: 'user'` می‌گیرد و پنل او را نمی‌شناسد.
- `App\Admin\Controller\AdminApiController` در سطح کلاس `#[IsGranted('ROLE_ADMIN')]` است → endpointهای `createDoctor` (`POST /api/v1/admin/doctors`) و `createClinic` (`POST /api/v1/admin/clinic`) برای نماینده مسدودند.
- Admin SPA: `assets/admin/App.tsx` تابع `RoleRoute` فقط `['admin']` را برای کلینیک‌ها/پزشکان می‌پذیرد؛ `Sidebar.tsx` فقط برای `admin|clinic|doctor|secretary` منو دارد (نماینده ندارد)؛ `DashboardPage.tsx` فقط endpointهای `/api/v1/admin/dashboard/*` را صدا می‌زند (admin-only).
- هیچ endpointی برای «نوبت‌های پزشکانِ یک نماینده» وجود ندارد.
## مشکل / هدف
به نقش `ROLE_REPRESENTATION` اجازه‌ی ورود به پنل و دسترسی به ۴ قابلیت بالا داده شود — بدون اینکه دسترسی‌های ادمین (کاربران، پرداخت‌ها، تسویه، بلاگ و...) برای او باز شود.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/Auth/Controller/AuthController.php` | `resolvePrimaryRole()` و gate لاگین staff (`/oauth/userinfo``primary_role`) |
| `src/Admin/Controller/AdminApiController.php` | کلاس `#[IsGranted('ROLE_ADMIN')]`؛ متدهای `createDoctor`, `createClinic`, `appointments` |
| `src/Representation/Controller/RepresentationController.php` | الگوی permission نماینده (`$rep->getUser()->getId() === $user->getId()`)؛ dashboard ماهانه/سالانه |
| `src/Representation/Repository/RepresentationRepository.php` | یافتن نماینده از روی user (`findByUser`) |
| `src/Doctor/Entity/Doctor.php` | `representationId` (FK به نماینده) — مبنای «پزشکان این نماینده» |
| `src/Appointment/...` (Repository/Controller) | منبع لیست نوبت‌ها برای endpoint جدید |
| `assets/admin/App.tsx` | `RoleRoute` + ثبت routeهای جدید |
| `assets/admin/components/layout/Sidebar.tsx` | منوی نقش `representation` |
| `assets/admin/pages/DashboardPage.tsx` | شاخه‌ی داشبورد نماینده |
| `assets/admin/pages/AppointmentsPage.tsx` | انتخاب endpoint نوبت‌ها بر اساس نقش |
| `assets/admin/stores/authStore.ts` | `primaryRole` (از `primary_role` می‌آید — تغییر لازم نیست، فقط مقدار جدید) |
| `docs/api/representation.md`, `docs/api/admin.md`, `docs/api/auth.md` | مستندسازی |
## وضعیت فعلی (کد واقعی)
`resolvePrimaryRole` — بدون شاخه‌ی نماینده:
```php
private function resolvePrimaryRole(User $user): string
{
$roles = $user->getRoles();
if (in_array('ROLE_ADMIN', $roles, true)) return 'admin';
if (in_array('ROLE_CLINIC', $roles, true)) return 'clinic';
if (in_array('ROLE_DOCTOR', $roles, true)) return 'doctor';
if (in_array('ROLE_SECRETARY', $roles, true)) return 'secretary';
return 'user';
}
```
`AdminApiController` — همه چیز admin-only:
```php
#[IsGranted('ROLE_ADMIN')]
class AdminApiController extends BaseController
{
// ... createDoctor(), createClinic(), appointments() همگی ذیل این قانون‌اند
}
```
`Doctor` — رابطه با نماینده:
```php
#[ORM\Column(name: 'representation_id', type: 'integer', nullable: true)]
private ?int $representationId = null;
public function getRepresentationId(): ?int { return $this->representationId; }
```
Admin SPA `App.tsx` — RoleRoute و routeهای فعلی:
```tsx
function RoleRoute({ roles, children }: { roles: string[]; children: React.ReactNode }) {
const primaryRole = useAuthStore(s => s.primaryRole);
if (!roles.includes(primaryRole)) return <Navigate to="/admin/dashboard" replace />;
return <>{children}</>;
}
// ...
<Route path="dashboard" element={<DashboardPage />} /> {/* همه نقش‌ها */}
<Route path="appointments" element={<AppointmentsPage />} /> {/* همه نقش‌ها */}
<Route path="clinics" element={<RoleRoute roles={['admin']}><ClinicsPage /></RoleRoute>} />
// doctors هم اکنون فقط ذیل admin در دسترس است
```
`Sidebar.tsx` — فقط ۴ نقش:
```tsx
if (primaryRole === "admin") { ... }
if (primaryRole === "clinic") { ... }
if (primaryRole === "doctor") { ... }
if (primaryRole === "secretary") { ... }
// representation وجود ندارد
```
`DashboardPage.tsx` — admin-only endpoints:
```tsx
const statsQ = useQuery({ queryFn: () => api.get('/api/v1/admin/dashboard/stats') });
const chartsQ = useQuery({ queryFn: () => api.get(`/api/v1/admin/dashboard/charts?...`) });
const recentQ = useQuery({ queryFn: () => api.get('/api/v1/admin/dashboard/recent') });
```
`AppointmentsPage.tsx` — انتخاب endpoint فعلی بر اساس نقش:
```ts
const isAdmin = primaryRole === 'admin';
const apptEndpoint = isAdmin ? '/api/v1/admin/appointments' : '/api/v1/my/appointments';
```
## وظایف
### ۱. Backend — شناختن نقش نماینده در `primary_role`
در `resolvePrimaryRole()` شاخه‌ی نماینده را **قبل از `return 'user'`** اضافه کن (اولویت پایین‌تر از admin/clinic/doctor/secretary، چون یک کاربر ممکن است هم‌زمان چند نقش داشته باشد):
```php
if (in_array('ROLE_SECRETARY', $roles, true)) return 'secretary';
if (in_array('ROLE_REPRESENTATION', $roles, true)) return 'representation';
return 'user';
```
اگر در `AuthController` یک gate صریح برای «نقش staff مجاز ورود» هست (لیست ROLE_ADMIN/DOCTOR/CLINIC/SECRETARY که کاربر عادی را رد می‌کند)، `ROLE_REPRESENTATION` را هم به آن لیست اضافه کن تا با `/oauth/token` + `/oauth/userinfo` وارد شود. (اگر چنین gateی وجود ندارد، نیازی نیست.)
### ۲. Backend — اجازه‌ی createDoctor و createClinic به نماینده
`AdminApiController` در سطح کلاس `ROLE_ADMIN` است و این قانون بر method-level مقدم می‌شود؛ پس صرفِ گذاشتن قانونِ بازتر روی متد کافی نیست. **کم‌ریسک‌ترین راه: یک controller جدید بساز**`src/Representation/Controller/RepresentationActionController.php` — با دو route نازک که منطق ساخت پزشک/کلینیک را اجرا کنند (یا منطق مشترک را به یک سرویس استخراج کن و هر دو controller از آن استفاده کنند تا کد تکراری نشود):
```php
#[IsGranted(new Expression("is_granted('ROLE_ADMIN') or is_granted('ROLE_REPRESENTATION')"))]
#[Route('/api/v1/representation/doctor', methods: ['POST'])]
public function createDoctor(Request $request, #[CurrentUser] User $user): JsonResponse { /* همان منطق createDoctor */ }
#[IsGranted(new Expression("is_granted('ROLE_ADMIN') or is_granted('ROLE_REPRESENTATION')"))]
#[Route('/api/v1/representation/clinic', methods: ['POST'])]
public function createClinic(Request $request, #[CurrentUser] User $user): JsonResponse { /* همان منطق createClinic */ }
```
- **مالکیت داده (مهم):** وقتی نماینده پزشک می‌سازد، نماینده‌ی کاربر جاری را با `RepresentationRepository::findByUser($user)` بگیر و `Doctor::setRepresentationId($rep->getId())` را ست کن تا بعداً در فیلتر «نوبت‌های پزشکان من» دیده شود. برای کلینیک هم اگر فیلد مالکیت/شهر مرتبط هست همان را ست کن.
- بدنه‌ی request و شکل پاسخ همان قرارداد فعلی `createDoctor`/`createClinic` در `docs/api/admin.md` بماند (تا فرم‌های موجود admin بدون تغییر کار کنند).
- در Admin SPA، فرم افزودن پزشک/کلینیک برای نماینده باید این endpointهای جدید را صدا بزند و برای ادمین همان endpointهای `/api/v1/admin/*` را (انتخاب بر اساس `primaryRole`).
> اگر تیم ترجیح می‌دهد به‌جای controller جدا، قانون کلاس `AdminApiController` را به Expression تبدیل کند: مراقب باش که در آن صورت **همه‌ی** متدهای آن کلاس باز می‌شوند؛ پس باید روی تک‌تک متدهای admin-only دیگر `#[IsGranted('ROLE_ADMIN')]` گذاشته شود. این پرریسک است؛ controller جدا توصیه می‌شود.
### ۳. Backend — endpoint نوبت‌های پزشکانِ نماینده
```
GET /api/v1/representation/appointments?page=&limit=&status=&from=&to=
```
- `#[IsGranted(new Expression("is_granted('ROLE_REPRESENTATION') or is_granted('ROLE_ADMIN')"))]`
- نماینده‌ی کاربر جاری را با `findByUser($user)` پیدا کن؛ اگر نبود → `403`.
- در Appointment Repository یک متد `findByRepresentation(int $representationId, array $filters)` اضافه کن که join بزند: `appointment.doctor d` و شرط `d.representationId = :repId`. خروجی با `$this->paginated($items, $total, $page, $limit)`.
- شکل هر آیتم همان `Appointment::toArray()` که ادمین مصرف می‌کند، تا `AppointmentsPage` بدون تغییر ساختاری آن را نشان دهد.
### ۴. Backend — endpoint نماینده‌ی کاربر جاری (برای داشبورد)
داشبورد فرانت به uuid نماینده نیاز دارد. یک endpoint بساز:
```
GET /api/v1/representation/me
```
- `#[IsGranted('ROLE_REPRESENTATION')]` (یا ادمین‌یا‌نماینده)
- نماینده‌ی `#[CurrentUser]` را با `findByUser` برگردان (`$this->success(['data' => $rep->toArray()])`). اگر نبود → `404`.
### ۵. Admin SPA — مسیرها و گارد نقش
در `App.tsx`:
- مسیرهای `doctors` (لیست/افزودن) و `clinics` (لیست/افزودن) را به `RoleRoute roles={['admin','representation']}` تغییر بده (هر دو زیرمسیر فرم افزودن هم).
- `dashboard` و `appointments` (الان «همه نقش‌ها») برای نماینده باز بمانند.
- هیچ مسیر admin-only دیگری (`users`, `payments`, `settlements`, `blogs`, `categories`, `sms`, `representations`, ...) را برای نماینده باز **نکن**.
### ۶. Admin SPA — منوی Sidebar برای نماینده
در `Sidebar.tsx` شاخه‌ی `if (primaryRole === "representation") { ... }` با آیتم‌ها:
- داشبورد (`/admin/dashboard`)
- پزشکان (`/admin/doctors`)
- کلینیک‌ها (`/admin/clinics`)
- نوبت‌ها (`/admin/appointments`)
از همان ساختار `Section`/`SectionItem` و آیکن‌های موجود استفاده کن.
### ۷. Admin SPA — داشبورد نماینده
در `DashboardPage.tsx` بر اساس `primaryRole` شاخه بزن:
- اگر `representation`: اول `GET /api/v1/representation/me` را بگیر تا `uuid` نماینده را داشته باشی، سپس:
- `GET /api/v1/representation/{uuid}/dashboard/monthly`
- `GET /api/v1/representation/{uuid}/dashboard/yearly`
(شکل پاسخ در `docs/api/representation.md`: `total_appointments`، کمیسیون، و...)
- کارت‌ها/نمودار را با همین داده‌ها بساز؛ از کارت‌های admin-only (کاربران/پرداخت/تسویه) استفاده نکن.
- endpointهای `/api/v1/admin/dashboard/*` نباید برای نماینده صدا زده شوند (۴۰۳ می‌دهند).
### ۸. Admin SPA — صفحه نوبت‌ها برای نماینده
در `AppointmentsPage.tsx`:
```ts
const isRepresentation = primaryRole === 'representation';
const apptEndpoint = isAdmin
? '/api/v1/admin/appointments'
: isRepresentation
? '/api/v1/representation/appointments'
: '/api/v1/my/appointments';
```
- بقیه‌ی فیلترها (status/from/to/page/limit) همان‌طور پاس داده شوند.
### ۹. مستندسازی
- `docs/api/auth.md`: مقدار جدید `primary_role: "representation"` و دسترسی این نقش به پنل.
- `docs/api/admin.md`: کنار `createDoctor`/`createClinic` ذکر کن که نسخه‌ی نماینده (`/api/v1/representation/doctor` و `/api/v1/representation/clinic`) هم وجود دارد و `representation_id` پزشک خودکار ست می‌شود.
- `docs/api/representation.md`: endpointهای جدید `GET /api/v1/representation/appointments`، `GET /api/v1/representation/me`، و `POST /api/v1/representation/doctor|clinic` با method/path/permission/query/response و نمونه JSON واقعی.
## نکات مهم
- **اصل امنیت:** نماینده فقط به داده‌ی خودش دسترسی دارد. در همه‌ی endpointهای نماینده، نماینده را از `#[CurrentUser]` و `findByUser` پیدا کن — **هرگز** به `uuid`/`representationId` ورودیِ کلاینت برای تعیین مالکیت اعتماد نکن (الگوی موجود در `RepresentationController`: `$rep->getUser()->getId() !== $user->getId() && !hasRole('ROLE_ADMIN')` → 403).
- قانون سطح کلاس `#[IsGranted('ROLE_ADMIN')]` روی `AdminApiController` نباید ضعیف شود؛ ساخت پزشک/کلینیکِ نماینده را در controller جدا بگذار (وظیفه‌ی ۲).
- `Doctor.representationId` ستون scalar است (نه association)؛ join/شرط در DQL مستقیم روی همین ستون.
- تاریخ‌ها Unix timestamp؛ لیست‌های admin با `paginated()` (items از `data`, total از `meta.totalRecords`). در Admin SPA: paginated → `data?.data` / `data?.meta?.totalRecords`؛ single → `data?.data` (گاهی double-nested).
- اگر هیچ Entityی تغییر نکرد migration لازم نیست (این feature عمدتاً منطق/route/permission است). اگر برای مالکیت کلینیکِ نماینده فیلد جدیدی لازم شد، migration بساز و اجرا کن.
- بعد از تغییر هر controller، فایل docs مربوطه را همان session به‌روز کن (قانون استاندارد پروژه).
- **تست‌ها:** با توکن یک کاربر `ROLE_REPRESENTATION` (`ddev exec php bin/console lexik:jwt:generate-token <mobile> --user-class="App\\Auth\\Entity\\User"`) چک کن:
- `GET /oauth/userinfo``primary_role: "representation"`
- `POST /api/v1/representation/doctor` و `/clinic` → ۲۰۱ و `representation_id` ست‌شده
- `GET /api/v1/representation/appointments` → فقط نوبت‌های پزشکانِ همان نماینده
- `GET /api/v1/representation/me` و dashboard ماهانه/سالانه → ۲۰۰
- `GET /api/v1/admin/users` با همان توکن → **۴۰۳** (نباید دسترسی داشته باشد)
- در Admin SPA با همان کاربر وارد شو: فقط ۴ آیتم منو دیده شود و افزودن پزشک/کلینیک کار کند.