diff --git a/.claude/prompt/fix-profile-in-clinic-context.md b/.claude/prompt/fix-profile-in-clinic-context.md new file mode 100644 index 00000000..82587daf --- /dev/null +++ b/.claude/prompt/fix-profile-in-clinic-context.md @@ -0,0 +1,228 @@ +# رفع باگ پروفایل دکتر در context کلینیک — doctor_uuid با clinic_uuid اشتباه گرفته می‌شود + +## زمینه + +دکتری که عضو چند کلینیک است، در صفحه «انتخاب محیط کاری» می‌تواند به context کلینیک سوئیچ کند. بعد از سوئیچ، `dbUuid` در authStore برابر **uuid کلینیک** می‌شود. وقتی دکتر به `/admin/profile` می‌رود، `DoctorProfilePage` از `dbUuid` به عنوان uuid دکتر استفاده می‌کند و همه API callها با uuid اشتباه صدا زده می‌شوند: + +- `GET /api/v1/doctor/c9c6a393-...` (uuid کلینیک) → 404 +- `GET /api/v1/appointment-slots?doctor_uuid=c9c6a393-...` → 404 + +کاربر تست: `09123456783` — دکتر `8b066a5e-3742-4bf7-8087-0b1f8d98de08` (uuid دکتر) که عضو کلینیک `c9c6a393-9182-4388-8cdc-6423026eeb54` است. + +## مشکل / هدف + +`dbUuid` دو معنی متفاوت دارد: +- در context مطب شخصی: uuid دکتر ✓ +- در context کلینیک: uuid کلینیک ✗ (اما `DoctorProfilePage` آن را uuid دکتر فرض می‌کند) + +راه‌حل: `doctor_uuid` را به عنوان فیلد جداگانه در `userinfo` و `switch-context` response اضافه کن و در store ذخیره کن. `DoctorProfilePage` باید از `doctorUuid` استفاده کند نه `dbUuid`. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/Auth/Controller/AuthController.php` | `userInfo()` خط 516، `switchContext()` خط 571 | +| `assets/admin/stores/authStore.ts` | `fetchMe()` و `switchContext()` — خط 86 و 105 | +| `assets/admin/pages/DoctorDetailPage.tsx` | `uuid = isOwnProfile ? (dbUuid ?? undefined) : paramUuid` — خط 2162 | +| `assets/admin/pages/DoctorProfilePage.tsx` | wrapper که `isOwnProfile` را به DoctorDetailPage پاس می‌دهد | + +## وضعیت فعلی + +### Backend — `userInfo()` response (خط 536-548) + +```php +return $this->success([ + 'uuid' => $user->getUuid(), + 'primary_role' => $primaryRole, + 'db_uuid' => $dbUuid, // ← وقتی context کلینیک است، این uuid کلینیک است + 'context' => $context, + 'available_contexts' => $availableContexts, + // ← doctor_uuid وجود ندارد +]); +``` + +### Backend — `buildAvailableContexts()` (خط 614-661) + +```php +if ($doctor = $this->doctorRepo->findByUser($user)) { + $contexts[] = [ + 'type' => 'doctor', + 'db_uuid' => $doctor->getUuid(), // ← uuid دکتر در این context + 'role' => 'doctor', + ]; + foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) { + $contexts[] = [ + 'type' => 'clinic', + 'db_uuid' => $clinic->getUuid(), // ← uuid کلینیک در این context + 'role' => 'clinic', + // ← doctor_uuid ندارد + ]; + } +} +``` + +### Frontend — `authStore.ts` (خط 19، 94-95) + +```ts +interface AuthState { + dbUuid: string | null; // ← فقط یک uuid — در context کلینیک، uuid کلینیک است + // ← doctorUuid وجود ندارد +} + +// fetchMe: +set({ + dbUuid: d.db_uuid ?? null, // ← uuid کلینیک یا دکتر بسته به context + // ← doctorUuid set نمی‌شود +}); +``` + +### Frontend — `DoctorDetailPage.tsx` (خط 2162) + +```tsx +const uuid = isOwnProfile ? (dbUuid ?? undefined) : paramUuid; +// ← وقتی context کلینیک است: uuid = uuid کلینیک → همه API ها با uuid اشتباه صدا زده می‌شوند +``` + +## وظایف + +### ۱. Backend — اضافه کردن `doctor_uuid` به response های auth + +در `src/Auth/Controller/AuthController.php`: + +**الف) `userInfo()` (خط 536):** فیلد `doctor_uuid` را اضافه کن: + +```php +$doctor = $this->doctorRepo->findByUser($user); + +return $this->success([ + 'id' => $user->getId(), + 'uuid' => $user->getUuid(), + 'mobile_number' => $user->getMobileNumber(), + 'realName' => $user->getRealName(), + 'status' => $user->getStatus(), + 'roles' => $user->getRoles(), + 'primary_role' => $primaryRole, + 'db_uuid' => $dbUuid, + 'db_key' => $dbKey, + 'doctor_uuid' => $doctor?->getUuid(), // ← اضافه شود + 'context' => $context, + 'available_contexts' => $availableContexts, +]); +``` + +**ب) `buildAvailableContexts()` (خط 626):** `doctor_uuid` را به context های کلینیک اضافه کن: + +```php +foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) { + $contexts[] = [ + 'type' => 'clinic', + 'db_uuid' => $clinic->getUuid(), + 'name' => $clinic->getName() ?? '', + 'role' => 'clinic', + 'doctor_uuid' => $doctor->getUuid(), // ← اضافه شود + ]; +} +``` + +**ج) `switchContext()` response (خط 595):** بدون تغییر — `context` کامل برمی‌گردد و شامل `doctor_uuid` خواهد بود. + +### ۲. Frontend — اضافه کردن `doctorUuid` به authStore + +در `assets/admin/stores/authStore.ts`: + +**الف) `ContextItem` interface (خط 4):** + +```ts +export interface ContextItem { + type: 'doctor' | 'clinic'; + db_uuid: string; + name: string; + role: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'user'; + doctor_uuid?: string; // ← اضافه شود — فقط در context های کلینیک + permissions?: Record; +} +``` + +**ب) `AuthState` interface (خط 12):** + +```ts +interface AuthState { + // ... + dbUuid: string | null; + doctorUuid: string | null; // ← اضافه شود — همیشه uuid دکتر، بدون توجه به context + // ... +} +``` + +**ج) مقدار اولیه (خط 62):** + +```ts +dbUuid: null, +doctorUuid: null, // ← اضافه شود +``` + +**د) `logout` (خط 72):** + +```ts +doctorUuid: null, // ← اضافه شود +``` + +**ه) `fetchMe` (خط 91):** + +```ts +set({ + dbUuid: d.db_uuid ?? null, + doctorUuid: d.doctor_uuid ?? null, // ← اضافه شود + // ... +}); +``` + +**و) `switchContext` (خط 111):** + +```ts +set({ + dbUuid: res.data.db_uuid, + doctorUuid: res.data.context?.doctor_uuid ?? get().doctorUuid, // ← context کلینیک doctor_uuid دارد، بقیه همان قبلی + dbKey: res.data.db_key, + context: res.data.context, + primaryRole: res.data.context?.role ?? null, +}); +``` + +**ز) `partialize` (خط 121):** + +```ts +partialize: (s) => ({ + // ... + doctorUuid: s.doctorUuid, // ← اضافه شود + // ... +}) +``` + +### ۳. Frontend — استفاده از `doctorUuid` در `DoctorDetailPage` + +در `assets/admin/pages/DoctorDetailPage.tsx` خط 2160-2162: + +```tsx +const primaryRole = useAuthStore(s => s.primaryRole); +const dbUuid = useAuthStore(s => s.dbUuid); +const doctorUuid = useAuthStore(s => s.doctorUuid); // ← اضافه شود + +// تغییر خط 2162: +const uuid = isOwnProfile + ? (doctorUuid ?? dbUuid ?? undefined) // ← doctorUuid اولویت دارد + : paramUuid; +``` + +این تضمین می‌کند که حتی در context کلینیک، uuid دکتر برای همه API callها استفاده شود. + +## نکات مهم + +- **migration لازم نیست** — هیچ entity تغییر نمی‌کند +- `doctorUuid` فقط برای کاربرانی که role دکتر دارند پر می‌شود — برای admin و clinic-owner خالص `null` است +- `get().doctorUuid` در `switchContext` — اگر context جدید `doctor_uuid` نداشت (مثلاً مطب شخصی که `db_uuid` خودش uuid دکتر است)، مقدار قبلی حفظ شود +- وقتی context مطب شخصی دکتر است (`type: 'doctor'`)، `db_uuid` خودش uuid دکتر است — `doctor_uuid` جداگانه نیاز نیست اما خوب است همان مقدار ست شود +- بعد از تغییر backend: `cache:clear` +- بعد از تغییر frontend: `yarn dev` و TypeScript check +- مستندات: `docs/api/auth.md` را با فیلد `doctor_uuid` در response های `userinfo` و `switch-context` به‌روزرسانی کن +- کاربر تست: `09123456783` — doctor_uuid: `8b066a5e-3742-4bf7-8087-0b1f8d98de08` diff --git a/assets/admin/pages/DoctorDetailPage.tsx b/assets/admin/pages/DoctorDetailPage.tsx index 4cef9a10..954655da 100644 --- a/assets/admin/pages/DoctorDetailPage.tsx +++ b/assets/admin/pages/DoctorDetailPage.tsx @@ -2159,7 +2159,8 @@ export default function DoctorDetailPage({ isOwnProfile = false }: { isOwnProfil const qc = useQueryClient(); const primaryRole = useAuthStore(s => s.primaryRole); const dbUuid = useAuthStore(s => s.dbUuid); - const uuid = isOwnProfile ? (dbUuid ?? undefined) : paramUuid; + const doctorUuid = useAuthStore(s => s.doctorUuid); + const uuid = isOwnProfile ? (doctorUuid ?? dbUuid ?? undefined) : paramUuid; const [editOpen, setEditOpen] = useState(searchParams.get('edit') === '1'); const [deleteOpen, setDeleteOpen] = useState(false); diff --git a/assets/admin/stores/authStore.ts b/assets/admin/stores/authStore.ts index 36fbdb8b..06e1016d 100644 --- a/assets/admin/stores/authStore.ts +++ b/assets/admin/stores/authStore.ts @@ -6,6 +6,7 @@ export interface ContextItem { db_uuid: string; name: string; role: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'user'; + doctor_uuid?: string; permissions?: Record; } @@ -18,6 +19,7 @@ interface AuthState { primaryRole: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'user' | null; dbUuid: string | null; dbKey: string | null; + doctorUuid: string | null; context: ContextItem | null; availableContexts: ContextItem[]; @@ -61,6 +63,7 @@ export const useAuthStore = create()( primaryRole: null, dbUuid: null, dbKey: null, + doctorUuid: null, context: null, availableContexts: [], @@ -79,6 +82,7 @@ export const useAuthStore = create()( primaryRole: null, dbUuid: null, dbKey: null, + doctorUuid: null, context: null, availableContexts: [], }), @@ -94,6 +98,7 @@ export const useAuthStore = create()( primaryRole: d.primary_role ?? null, dbUuid: d.db_uuid ?? null, dbKey: d.db_key ?? null, + doctorUuid: d.doctor_uuid ?? null, context: d.context ?? null, availableContexts: d.available_contexts ?? [], }); @@ -111,6 +116,7 @@ export const useAuthStore = create()( set({ dbUuid: res.data.db_uuid, dbKey: res.data.db_key, + doctorUuid: res.data.context?.doctor_uuid ?? get().doctorUuid, context: res.data.context, primaryRole: res.data.context?.role ?? null, }); @@ -128,6 +134,7 @@ export const useAuthStore = create()( primaryRole: s.primaryRole, dbUuid: s.dbUuid, dbKey: s.dbKey, + doctorUuid: s.doctorUuid, context: s.context, availableContexts: s.availableContexts, }), diff --git a/docs/api/auth.md b/docs/api/auth.md index 966e9dc8..0e7fa281 100644 --- a/docs/api/auth.md +++ b/docs/api/auth.md @@ -248,6 +248,7 @@ Authorization: Bearer "primary_role": "doctor", "db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966", "db_key": "hmac-sha256-hash...", + "doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966", "context": { "type": "doctor", "db_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966", @@ -265,7 +266,8 @@ Authorization: Bearer "type": "clinic", "db_uuid": "clinic-uuid-...", "name": "کلینیک سلامت", - "role": "clinic" + "role": "clinic", + "doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966" } ] } @@ -277,6 +279,7 @@ Authorization: Bearer | `primary_role` | string | نقش اصلی: `admin` \| `clinic` \| `doctor` \| `secretary` \| `user` | | `db_uuid` | string\|null | UUID موجودیت فعال (null = هنوز context انتخاب نشده) | | `db_key` | string\|null | `HMAC-SHA256(db_uuid, APP_SECRET)` برای اعتبارسنجی | +| `doctor_uuid` | string\|null | UUID دکتر — ثابت است حتی در context کلینیک که `db_uuid` برابر UUID کلینیک است. برای کاربران غیر دکتر: `null` | | `context` | object\|null | context فعال انتخاب‌شده | | `available_contexts` | array | همه محیط‌های کاری قابل انتخاب | @@ -335,7 +338,8 @@ Authorization: Bearer "type": "clinic", "db_uuid": "clinic-uuid-...", "name": "کلینیک سلامت", - "role": "clinic" + "role": "clinic", + "doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966" } } } diff --git a/src/Auth/Controller/AuthController.php b/src/Auth/Controller/AuthController.php index c029865c..0f7294cd 100644 --- a/src/Auth/Controller/AuthController.php +++ b/src/Auth/Controller/AuthController.php @@ -532,6 +532,7 @@ class AuthController extends BaseController $dbUuid = $activeCtx?->getDbUuid(); $dbKey = $dbUuid !== null ? $this->buildDbKey($dbUuid) : null; $context = $dbUuid !== null ? $this->findContextByDbUuid($dbUuid, $availableContexts) : null; + $doctor = $this->doctorRepo->findByUser($user); return $this->success([ 'id' => $user->getId(), @@ -543,6 +544,7 @@ class AuthController extends BaseController 'primary_role' => $primaryRole, 'db_uuid' => $dbUuid, 'db_key' => $dbKey, + 'doctor_uuid' => $doctor?->getUuid(), 'context' => $context, 'available_contexts' => $availableContexts, ]); @@ -625,10 +627,11 @@ class AuthController extends BaseController ]; foreach ($this->clinicRepo->findByDoctor($doctor) as $clinic) { $contexts[] = [ - 'type' => 'clinic', - 'db_uuid' => $clinic->getUuid(), - 'name' => $clinic->getName() ?? '', - 'role' => 'clinic', + 'type' => 'clinic', + 'db_uuid' => $clinic->getUuid(), + 'name' => $clinic->getName() ?? '', + 'role' => 'clinic', + 'doctor_uuid' => $doctor->getUuid(), ]; } }