# دسترسی نقش نماینده (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 ;
return <>{children}>;
}
// ...
} /> {/* همه نقشها */}
} /> {/* همه نقشها */}
} />
// 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 --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 با همان کاربر وارد شو: فقط ۴ آیتم منو دیده شود و افزودن پزشک/کلینیک کار کند.