# دسترسی پزشک و مدیر کلینیک به پرونده‌های کلینیک ## پروژه `clinicpro` (backend + پنل ادمین) — بعد از `clinic-appointment-operations-fix.md` و `appointment-confirm-flow.md` اجرا شود. ## زمینه پرونده‌ها per-محیط silo شده‌اند: `PatientRecord` مالک چندریختی دارد — `entityType` (`doctor|clinic|system`) + `entityId` با یکتایی `(entity_type, entity_id, user_id)`. `PatientController::resolveEntity` (~1186) هر کاربر را به **یک** scope نگاشت می‌کند (پزشک → پرونده‌های شخصی خودش، کلینیک → پرونده‌های کلینیک) و `ownsRecord()` (~1232) تساوی دقیق می‌سنجد. نتیجه فعلی: پزشکِ دعوت‌شده به کلینیک، پرونده‌های بیمارانش **در آن کلینیک** را نمی‌بیند (فقط مدیر کلینیک می‌بیند). ## مشکل / هدف - پزشک عضو کلینیک و مدیر کلینیک هر دو به پرونده‌های بیماران آن پزشک در آن کلینیک دسترسی داشته باشند (مشاهده + مدیریت). - تا وقتی پزشک در کلینیک فعال است (`ClinicDoctorPermission.active` / عضویت)، همه پرونده‌های مرتبطش در آن کلینیک برایش قابل مشاهده/مدیریت باشد. - با پایان همکاری یا غیرفعال شدن، دسترسی پزشک طبق سطح دسترسی سیستم محدود/قطع شود؛ مدیر کلینیک دسترسی کامل بماند. ## فایل‌های مرتبط | فایل | نقش | |------|-----| | `src/Patient/Controller/PatientController.php` | `resolveEntity` (~1186)، `ownsRecord` (~1221-1240)، همه endpoint های پرونده/session/پرداخت | | `src/Patient/Entity/PatientRecord.php` | مالک چندریختی، بدون FK پزشک | | `src/Patient/Entity/PatientSession.php` | `appointment` nullable → پل به `appointment.doctor` | | `src/Patient/Service/PatientService.php` | ساخت پرونده/session (`autoCreateForEntity` ~144) | | `src/Clinic/Security/ClinicDoctorPermissionChecker.php` | `can(user, clinic, 'patients', action)` — `active=false` را رد می‌کند | | `src/Clinic/Entity/ClinicDoctorPermission.php` | resource `patients` در `DEFAULT_PERMISSIONS` | | `src/Shared/Context/EntityContextResolver.php` | کانتکست فعال (پزشکی که داخل کلینیک سوییچ کرده) | | `assets/admin/pages/MyPatientsPage.tsx`, `PatientsListPage.tsx`, `PatientDetailPage.tsx` | UI پرونده‌ها | | `assets/admin/stores/authStore.ts`, `hooks/useClinicContext.ts` | کانتکست SPA (`switchContext`) | ## وضعیت فعلی ```php // PatientController::resolveEntity — نگاشت تک‌مقصدی: // ROLE_DOCTOR → ['doctor', doctorId] // فقط پرونده‌های مطب شخصی // ROLE_CLINIC → ['clinic', clinicId] // فقط پرونده‌های کلینیک // ROLE_SECRETARY → از UserActiveContext // ownsRecord(): record.entityType === entityType && record.entityId === entityId ``` نکته کلیدی مدل: پرونده کلینیکی per-بیمار است نه per-پزشک (unique روی clinic+user). «پرونده‌های بیماران آن پزشک» یعنی پرونده‌های کلینیکی‌ای که بیمارشان با آن پزشک session/نوبت داشته — از مسیر `PatientSession.appointment.doctor` (و برای سشن‌های دستی `createdByType/createdById`) قابل استخراج است. ## وظایف ### ۱. Backend — دسترسی پزشک به پرونده‌های کلینیک `resolveEntity`/`ownsRecord` را از نگاشت تک‌مقصدی به مدل «scope فعال + عضویت» ارتقا بده: - وقتی پزشک با `UserActiveContext` روی کانتکست کلینیک است (SPA با `switchContext` این را ست می‌کند)، scope پرونده = `['clinic', clinicId]` **مشروط به** `ClinicDoctorPermissionChecker::can(user, clinic, 'patients', action)` — که خودش عضویت غیرفعال را رد می‌کند. این الزام «قطع دسترسی بعد از پایان همکاری» را بدون منطق جدید برآورده می‌کند. - محدودسازی به «بیماران آن پزشک»: در لیست پرونده‌ها (`GET /api/v1/patient(s)`) وقتی scope=clinic و کاربر پزشک عضو است (نه مدیر)، فیلتر کن به پرونده‌هایی که حداقل یک session با `appointment.doctor = doctor` یا `createdByType='doctor' AND createdById=doctorId` دارند (subquery/EXISTS در repository). مدیر کلینیک بدون این فیلتر، همه را می‌بیند. - دسترسی تک‌پرونده (detail/session/پرداخت/یادداشت/پیوست): همان قاعده — مدیر کلینیک کامل؛ پزشک عضو فعال فقط اگر پرونده طبق فیلتر بالا «مالِ بیماران خودش» باشد. تصمیم باز که باید حین اجرا گرفته و مستند شود: آیا پزشک به کل پرونده مشترک بیمار (شامل session های پزشک دیگر همان کلینیک) دید دارد یا فقط session های خودش؟ پیش‌فرض پیشنهادی: دید کامل به پرونده، مدیریت فقط روی session های خودش. - `assertPatientGate` (feature اشتراک `patient_records`) سر جای خودش بماند — گیت اشتراک باید بر اساس محیط کلینیک چک شود نه اشتراک شخصی پزشک. ### ۲. Backend — منشی منشی کلینیک (`DoctorSecretary` با `OWNER_CLINIC` و `active`) طبق همان الگو: scope کلینیک + محدود به پزشکان محول‌شده + `SecretaryPermissionChecker::can(..., 'patients', ...)`. رفتار فعلی منشی نباید پس‌رفت کند. ### ۳. Frontend — نمایش پرونده‌های کلینیک برای پزشک - وقتی پزشک کانتکست کلینیک را انتخاب کرده (`useClinicContext` مقدار دارد)، `MyPatientsPage`/`PatientsListPage` باید پرونده‌های کلینیکِ scope شده را نشان دهند — احتمالاً بدون تغییر فرانت کار می‌کند چون scope سمت سرور است؛ تست کن و فقط اگر endpoint/پارامتر جدید لازم شد دست بزن. - حالت خطای «دسترسی قطع شده» (پزشک غیرفعال‌شده): پیام فارسی روشن، نه صفحه خالی. ### ۴. تست - پزشک عضو فعال در کلینیک `09024206041` (طبق `TEST_USERS.md` بساز/استفاده کن): در کانتکست کلینیک پرونده بیمارانش را می‌بیند و session/پرداخت ثبت می‌کند؛ در کانتکست شخصی فقط پرونده‌های مطب خودش. - مدیر کلینیک: همه پرونده‌های کلینیک، قبل و بعد از غیرفعال‌سازی پزشک. - پزشک را غیرفعال کن (`ClinicDoctorPermission.active=false`): پزشک 403/فیلتر می‌شود، مدیر همچنان کامل؛ پرونده‌ها و تاریخچه دست‌نخورده می‌مانند (هیچ حذف/انتقالی رخ نمی‌دهد). - قطعی‌کردن نوبت کلینیکی توسط پزشک عضو (خروجی پرامپت قبلی) پرونده را در محیط کلینیک می‌سازد و همان پرونده برای هر دو نقش دیده می‌شود. - `docs/api/*` برای هر endpoint تغییرکرده به‌روز شود. ## نکات مهم - **FK پزشک به `PatientRecord` اضافه نکن** — یکتایی `(clinic, user)` عمداً پرونده مشترک کلینیکی است؛ ارتباط پزشک از مسیر session/appointment استخراج می‌شود. - Voter نساز؛ الگوی checker service موجود. - لیست‌ها با DQL array hydration (`getArrayResult`)؛ فیلتر EXISTS را در repository اضافه کن نه در PHP. - اگر schema تغییر کرد (بعید، ولی مثلاً index برای کوئری EXISTS): migration.