diff --git a/.claude/prompt/admin-user-detail-show-profile.md b/.claude/prompt/admin-user-detail-show-profile.md new file mode 100644 index 00000000..202b1bc1 --- /dev/null +++ b/.claude/prompt/admin-user-detail-show-profile.md @@ -0,0 +1,113 @@ +# نمایش پروفایل کاربر در صفحه‌ی جزئیات کاربرِ پنل ادمین + +## پروژه + +`clinicpro` (Admin React frontend). تغییر عمدتاً frontend است؛ endpoint پروفایل از قبل موجود است و به ادمین دسترسی می‌دهد. (یک تصمیم درباره‌ی side-effect لازم است — به بخش «نکات» نگاه کن.) + +## زمینه + +صفحه‌ی `/admin/users/{uuid}` (`assets/admin/pages/UserDetailPage.tsx`) فقط اطلاعات **حساب کاربری** را از `GET /api/v1/admin/users/{uuid}` نشان می‌دهد: نام، موبایل، ایمیل، نقش‌ها، وضعیت، UUID. اما **پروفایل بیمار** (کد ملی، جنسیت، تاریخ تولد، گروه خونی، وضعیت تأهل، تحصیلات، شغل، آدرس، تلفن‌ها، بیمه، سوابق پزشکی) را نشان نمی‌دهد. ادمین باید بتواند پروفایل کامل کاربر را همین‌جا ببیند. + +پروفایل از `GET /api/v1/user-profile/{uuid}` می‌آید که **هم با user-uuid کار می‌کند** (resolve شده) و **به `ROLE_ADMIN` اجازه‌ی دیدن هر پروفایلی را می‌دهد** (`canAccess` → admin مجاز). پاسخ دوبار تودرتو است: `{ success, data: { data: {...profile...} } }`. + +## مشکل / هدف + +به `UserDetailPage.tsx` یک بخش «پروفایل کاربر» اضافه کن که با `GET /api/v1/user-profile/{uuid}` پروفایل را می‌گیرد و فیلدها را به‌صورت **فقط-خواندنی و خوانا** (کارت‌های اطلاعات + بخش سوابق پزشکی) نمایش می‌دهد. اگر کاربر پروفایل پر نکرده، حالت «ثبت نشده» نشان داده شود (نه خطا). + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `assets/admin/pages/UserDetailPage.tsx` | صفحه‌ی جزئیات کاربر — محل افزودن بخش پروفایل | +| `assets/admin/lib/api.ts` | `api.get`, `ApiResponse`, JWT از `localStorage['clinicpro-auth']` | +| `assets/admin/lib/utils.ts` | `formatDate`, `formatDateTime` (و در صورت نیاز `formatNumber`) | +| `src/UserProfile/Controller/UserProfileController.php` | `show()` — مرجع قرارداد (تغییر نمی‌کند، مگر تصمیم read-only گرفته شود) | +| `src/UserProfile/Entity/UserProfile.php` | `toArray()` — فیلدهای پروفایل | + +## وضعیت فعلی (کد واقعی) + +### `UserDetailPage.tsx` — فقط user query + +```tsx +const { data, isLoading, isError } = useQuery({ + queryKey: ['admin-user', uuid], + queryFn: () => api.get>(`/api/v1/admin/users/${uuid}`), + enabled: !!uuid, +}); +const user: AdminUserDetail | undefined = (data?.data as any)?.data ?? data?.data; +// ... فقط نام/موبایل/ایمیل/نقش/وضعیت/UUID رندر می‌شود؛ پروفایل نیست +``` + +### قرارداد پروفایل (`UserProfile.toArray`) + +```json +{ + "uuid": "...", "user_uuid": "...", + "label": null, "family": null, "fathers_name": null, + "national_code": null, "national_code_approved": false, + "gender": null, "date_of_birth": null, "blood_type": null, + "marital_status": null, "education": null, "job": null, + "address": null, "home_phone": null, "work_phone": null, + "insurance_id": null, "basic_insurance_id": null, "supplementary_insurance_id": null, + "other": { "disease": [...], "allergies": [...], "medications": [...], "surgeries": [...], "family_history": [...], "relatives": [...] }, + "sharing_with_user": false, "description": null, + "created_at": ..., "updated_at": ... +} +``` +> پاسخ: `{ success, data: { data: {...} } }` → استخراج با `res?.data?.data ?? res?.data`. + +## وظایف + +### ۱. کوئری پروفایل در `UserDetailPage.tsx` + +```tsx +interface UserProfileData { + uuid: string; user_uuid: string; + label: string | null; family: string | null; fathers_name: string | null; + national_code: string | null; gender: string | null; date_of_birth: number | null; + blood_type: string | null; marital_status: string | null; education: string | null; + job: string | null; address: string | null; home_phone: string | null; work_phone: string | null; + basic_insurance_id: number | null; supplementary_insurance_id: number | null; + other: Record | null; +} + +const profileQ = useQuery({ + queryKey: ['admin-user-profile', uuid], + queryFn: () => api.get>(`/api/v1/user-profile/${uuid}`), + enabled: !!uuid, + retry: false, +}); +const profile: UserProfileData | undefined = + (profileQ.data as any)?.data?.data ?? (profileQ.data as any)?.data; +``` + +### ۲. بخش «پروفایل کاربر» (read-only) در رندر + +- یک کارت `cp-card` جدید بعد از گرید اطلاعات موجود اضافه کن با عنوان «پروفایل کاربر». +- از همان کامپوننت `InfoCard` موجود برای فیلدها استفاده کن (آیکن + label + value؛ مقدار null → «ثبت نشده» خودش هندل می‌شود). +- فیلدها با برچسب فارسی: نام، نام خانوادگی، نام پدر، کد ملی، جنسیت (`male`→مرد/`female`→زن)، تاریخ تولد (شمسی)، گروه خونی، وضعیت تأهل، تحصیلات، شغل، آدرس، تلفن منزل، تلفن محل کار، شماره بیمه پایه/تکمیلی. +- بخش **سوابق پزشکی** (از `other`): آلرژی‌ها، داروها، جراحی‌ها، سابقه‌ی خانوادگی، بیماری‌ها — هرکدام لیست؛ اگر خالی، «موردی ثبت نشده». ساختار آیتم‌های `other.*` را از داده‌ی واقعی استنتاج کن (با یک `curl` نمونه)، توهم‌سازی نکن. +- تاریخ تولد: اگر `date_of_birth` عدد (Unix) است با `formatDate` شمسی؛ اگر رشته‌ی شمسی ذخیره شده، همان را نشان بده (شکل واقعی را با curl چک کن). + +### ۳. حالت‌های loading / خالی + +- وقتی `profileQ.isLoading` → skeleton کوچک. +- وقتی پروفایل همه‌ی فیلدهایش null است (کاربر چیزی پر نکرده) → پیام «این کاربر هنوز پروفایلی تکمیل نکرده است» به‌جای کارت‌های خالی، یا کارت‌ها با «ثبت نشده». +- خطا (۴۰۳/۴۰۴ نامحتمل چون ادمین است) → یک پیام کوچک، نه شکستن کل صفحه. + +## نکات مهم + +- **side-effect مهم — تصمیم لازم:** `GET /api/v1/user-profile/{uuid}` در حال حاضر برای کاربری که پروفایل ندارد، **یک پروفایل خالی lazy-create می‌کند** (در `resolveProfile`). برای «دیدن» توسط ادمین، ساختِ رکورد هنگام GET نامطلوب است. **یکی را انتخاب کن:** + 1. (ساده) همین رفتار را بپذیر — یک پروفایل خالی برای کاربر ساخته می‌شود؛ بی‌ضرر ولی side-effect دارد. + 2. (تمیزتر، توصیه‌شده) در `UserProfileController::resolveProfile`، lazy-create را فقط وقتی انجام بده که درخواست‌دهنده **خودِ کاربر** است، نه ادمینی که پروفایل دیگری را می‌بیند؛ یعنی برای ادمینِ بیننده، اگر پروفایل نبود `null` برگردان (→ ۴۰۴ یا «پروفایل ندارد») بدون ساخت. این یک تغییر کوچک backend است (`docs/api/user-profile.md` را هم به‌روز کن). + - **اگر گزینه ۲ را انتخاب کردی، این بخش backend است؛ آن را اول انجام بده و سند را به‌روز کن.** +- **double-nesting:** پروفایل در `res.data.data` است (مثل بقیه‌ی `success(['data'=>...])`). +- **فقط-خواندنی:** ادمین اینجا فقط پروفایل را می‌بیند؛ ویرایش پروفایل بیمار توسط ادمین خارج از این پرامپت است (اگر خواستی پیشنهاد بده ولی پیاده نکن). +- از کلاس‌های CSS و کامپوننت‌های موجود (`cp-card`, `InfoCard`, `skeleton`) استفاده کن؛ کتابخانه‌ی جدید نیار؛ RTL. +- تاریخ‌ها شمسی با `formatDate`/`formatDateTime` موجود. +- **تست:** + - `ddev exec npx tsc --noEmit --project tsconfig.json 2>&1 | head -30` + - `ddev exec yarn dev` (build admin) — خطای CSS lightningcss از قبل هست و بلوک نمی‌کند؛ فقط خطای TS مهم است. + - یک `curl` واقعی روی `GET /api/v1/user-profile/7a63a8d2-6483-4ae7-9c38-54c399cde699` با توکن ادمین برای دیدن شکل دقیق `other`/`date_of_birth`. + - در مرورگر: `/admin/users/{uuid}` بخش پروفایل را با داده‌ی واقعی یا «ثبت نشده» نشان دهد. + - اگر گزینه ۲ را انتخاب کردی: `ddev exec php -l` + به‌روزرسانی `docs/api/user-profile.md`. diff --git a/docs/api/user-profile.md b/docs/api/user-profile.md index fa905821..18ca4e24 100644 --- a/docs/api/user-profile.md +++ b/docs/api/user-profile.md @@ -97,7 +97,7 @@ Get a user profile. |-------|------|-------------| | `uuid` | string (UUID) | **Profile UUID or the owning User UUID** — both are accepted | -> **Auto-resolve & lazy-create:** `{uuid}` is first looked up as a profile uuid, then as a user uuid. If it is the **current user's** (or, for admins, any user's) uuid and that user has **no profile yet**, an empty profile is created and returned (HTTP 200) — so a freshly registered user always gets an editable profile instead of a 404. The response includes both the profile `uuid` and `user_uuid`; keep the profile `uuid` for subsequent PATCHes. +> **Auto-resolve & lazy-create:** `{uuid}` is first looked up as a profile uuid, then as a user uuid. If it is the **current user's own** uuid and they have **no profile yet**, an empty profile is created and returned (HTTP 200) — so a freshly registered user always gets an editable profile instead of a 404. An **admin** reading **another** user's missing profile gets `404` (no profile is created as a side effect of reading). The response includes both the profile `uuid` and `user_uuid`; keep the profile `uuid` for subsequent PATCHes. ### Response `200` Full profile object including all fields (all `null` for a newly created empty profile). Response is double-nested: extract with `data.data`. diff --git a/src/UserProfile/Controller/UserProfileController.php b/src/UserProfile/Controller/UserProfileController.php index 4047d91a..63dc3bc0 100644 --- a/src/UserProfile/Controller/UserProfileController.php +++ b/src/UserProfile/Controller/UserProfileController.php @@ -95,8 +95,9 @@ class UserProfileController extends BaseController /** * Resolve a profile from a uuid that may be the profile's own uuid or the * owning user's uuid. When $createIfMissing is true and the uuid belongs to - * a user (the current user or, for admins, anyone) without a profile, an - * empty profile is created and persisted. + * the **current user** without a profile, an empty profile is created and + * persisted. An admin viewing another user's missing profile gets null (no + * record is created as a side effect of reading). */ private function resolveProfile(string $uuid, User $currentUser, bool $createIfMissing): ?UserProfile { @@ -115,11 +116,9 @@ class UserProfileController extends BaseController return $profile; } - if (!$createIfMissing) { - return null; - } - - if ($targetUser->getId() !== $currentUser->getId() && !$currentUser->hasRole('ROLE_ADMIN')) { + // Only lazy-create for the user's own profile, never as a side effect of + // an admin reading someone else's. + if (!$createIfMissing || $targetUser->getId() !== $currentUser->getId()) { return null; }