feat: add doctor_uuid support in authStore and update DoctorDetailPage to prioritize doctorUuid

This commit is contained in:
hamed
2026-06-13 19:19:50 +03:30
parent 09c393a574
commit ab5dca4fb6
5 changed files with 250 additions and 7 deletions
@@ -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<string, any>;
}
```
**ب) `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`
+2 -1
View File
@@ -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);
+7
View File
@@ -6,6 +6,7 @@ export interface ContextItem {
db_uuid: string;
name: string;
role: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'user';
doctor_uuid?: string;
permissions?: Record<string, any>;
}
@@ -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<AuthState>()(
primaryRole: null,
dbUuid: null,
dbKey: null,
doctorUuid: null,
context: null,
availableContexts: [],
@@ -79,6 +82,7 @@ export const useAuthStore = create<AuthState>()(
primaryRole: null,
dbUuid: null,
dbKey: null,
doctorUuid: null,
context: null,
availableContexts: [],
}),
@@ -94,6 +98,7 @@ export const useAuthStore = create<AuthState>()(
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<AuthState>()(
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<AuthState>()(
primaryRole: s.primaryRole,
dbUuid: s.dbUuid,
dbKey: s.dbKey,
doctorUuid: s.doctorUuid,
context: s.context,
availableContexts: s.availableContexts,
}),
+6 -2
View File
@@ -248,6 +248,7 @@ Authorization: Bearer <token>
"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 <token>
"type": "clinic",
"db_uuid": "clinic-uuid-...",
"name": "کلینیک سلامت",
"role": "clinic"
"role": "clinic",
"doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966"
}
]
}
@@ -277,6 +279,7 @@ Authorization: Bearer <token>
| `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 <token>
"type": "clinic",
"db_uuid": "clinic-uuid-...",
"name": "کلینیک سلامت",
"role": "clinic"
"role": "clinic",
"doctor_uuid": "a6ef5d29-38b8-4e69-b1ef-27a304696966"
}
}
}
+7 -4
View File
@@ -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(),
];
}
}