Files
clinicpro/.claude/prompt/representation-admin-panel-access.md
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

16 KiB
Raw Permalink Blame History

دسترسی نقش نماینده (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/userinfoprimary_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 — بدون شاخه‌ی نماینده:

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:

#[IsGranted('ROLE_ADMIN')]
class AdminApiController extends BaseController
{
    // ... createDoctor(), createClinic(), appointments() همگی ذیل این قانون‌اند
}

Doctor — رابطه با نماینده:

#[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های فعلی:

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 — فقط ۴ نقش:

if (primaryRole === "admin") { ... }
if (primaryRole === "clinic") { ... }
if (primaryRole === "doctor") { ... }
if (primaryRole === "secretary") { ... }
// representation وجود ندارد

DashboardPage.tsx — admin-only endpoints:

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 فعلی بر اساس نقش:

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، چون یک کاربر ممکن است هم‌زمان چند نقش داشته باشد):

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 از آن استفاده کنند تا کد تکراری نشود):

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

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/userinfoprimary_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 با همان کاربر وارد شو: فقط ۴ آیتم منو دیده شود و افزودن پزشک/کلینیک کار کند.