From 2206f02396902076358839ab09e6223c50fb2674 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Tue, 23 Jun 2026 17:21:33 +0330 Subject: [PATCH] feat: enrich patient record with full profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/v1/patient/{uuid} now returns a `profile` object from UserProfile (demographics, contact, insurance names resolved). Admin record detail shows a "patient info" section. Empty fields render as "ثبت نشده". Co-Authored-By: Claude Opus 4.8 --- .../prompt/patient-record-profile-binding.md | 93 +++++++++++++++++++ assets/admin/pages/MyPatientsPage.tsx | 36 +++++++ assets/admin/types/index.ts | 17 ++++ docs/api/patient.md | 30 +++++- src/Patient/Controller/PatientController.php | 34 ++++++- 5 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 .claude/prompt/patient-record-profile-binding.md diff --git a/.claude/prompt/patient-record-profile-binding.md b/.claude/prompt/patient-record-profile-binding.md new file mode 100644 index 00000000..47f91861 --- /dev/null +++ b/.claude/prompt/patient-record-profile-binding.md @@ -0,0 +1,93 @@ +# اتصال پرونده‌ی بیمار به پروفایل بیمار + +## پروژه + +`clinicpro` (backend + admin frontend) + +## زمینه + +پرونده‌ی بیمار (`PatientRecord`) فعلاً فقط به `User` وصل است و در پنل صرفاً نام و موبایل (و اخیراً کد ملی) نمایش داده می‌شود. اطلاعات کامل دموگرافیک بیمار که در `UserProfile` ذخیره شده، برای کلینیک/مطب قابل مشاهده نیست. + +## مشکل / هدف + +وقتی کاربری به یک مطب یا کلینیک اضافه می‌شود، **اطلاعات پروفایل او باید برای آن کلینیک مشخص باشد**: نام و نام خانوادگی، کد ملی، جنسیت، تاریخ تولد، بیمه‌ی پایه/مکمل، تماس و … . یعنی پرونده‌ی بیمار باید بر اساس `UserProfile` غنی شود. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `src/UserProfile/Entity/UserProfile.php` | پروفایل کامل بیمار | +| `src/Patient/Entity/PatientRecord.php` | پرونده؛ `toArray()` فعلاً فقط `user_name/user_mobile/user_national_code` | +| `src/Patient/Controller/PatientController.php` | `list` + `show` | +| `src/Patient/Repository/PatientRecordRepository.php` | کوئری‌ها | +| `assets/admin/pages/MyPatientsPage.tsx` | لیست کارتی + صفحه‌ی جزئیات پرونده (بنر بالای پرونده) | +| `assets/admin/types/index.ts` | `PatientRecord` type | +| `docs/api/patient.md` | مستندات | + +## وضعیت فعلی + +`UserProfile` فیلدهای موجود (نمونه): + +```php +private ?string $family; +private ?string $fathersName; +private ?string $nationalCode; +private ?string $gender; +private ?int $dateOfBirth; // unix +private ?string $bloodType; +private ?string $maritalStatus; +private ?string $job; +private ?string $address; +private ?string $homePhone; +private ?int $basicInsuranceId; +private ?int $supplementaryInsuranceId; +``` + +`PatientRecord::toArray()`: + +```php +return [ + 'uuid' => $this->uuid, + 'entity_type' => $this->entityType, + 'entity_id' => $this->entityId, + 'user_uuid' => $this->user->getUuid(), + 'user_name' => $this->user->getRealName(), + 'user_mobile' => $this->user->getMobileNumber(), + 'user_national_code' => $this->user->getNationalCode(), + 'created_by_type' => $this->createdByType, + 'created_at' => $this->createdAt, +]; +``` + +پروفایل بیمار اصلاً در پاسخ نیست. + +## وظایف + +### ۱. تحلیل رابطه User ↔ UserProfile + +`UserProfile` را کامل بخوان: رابطه به `User`، repository، و اینکه آیا یک‌به‌یک است. روش گرفتن پروفایل از روی `User` را پیدا کن. + +### ۲. غنی‌سازی پاسخ پرونده + +- در `show` (و در صورت نیاز `list`) اطلاعات پروفایل بیمار را به پاسخ اضافه کن. توصیه: یک کلید `profile` در پاسخِ `show` (جزئیات کامل)، و در `list` فقط فیلدهای سبک (نام، جنسیت، کد ملی) برای کارت. +- نام بیمه‌ها از id به نام resolve شود (LEFT JOIN یا lookup روی `insurances`). +- اگر پروفایل وجود نداشت، مقادیر null برگردد (نه خطا). + +> برای `list` از kept-light استفاده کن تا کوئری سنگین نشود؛ برای `show` می‌توان پروفایل کامل را join کرد. + +### ۳. Admin Frontend — نمایش پروفایل در پرونده + +- در صفحه‌ی جزئیات پرونده (بنر بالای تاریخچه‌ی مراجعات)، یک بخش «اطلاعات بیمار» با فیلدهای پروفایل (نام کامل، کد ملی، جنسیت، تاریخ تولد شمسی با `formatDate`، بیمه پایه/مکمل، تماس) اضافه کن. +- `PatientRecord` type در `types/index.ts` را با `profile` به‌روز کن. +- اگر فیلدی خالی بود، «ثبت نشده» نشان بده. + +### ۴. مستندات + +`docs/api/patient.md` را با ساختار جدید پاسخ (`profile`) برای `show` و فیلدهای سبک `list` به‌روز کن. + +## نکات مهم + +- تاریخ تولد unix timestamp است؛ با `formatDate()` شمسی نمایش بده. +- `entity_type` می‌تواند `doctor` یا `clinic` باشد؛ نمایش پروفایل برای هر دو یکسان. +- این پرامپت مکمل `create-patient-without-signup.md` است؛ اگر بیمار جدید بدون ثبت‌نام ساخته می‌شود، همان فیلدهای پروفایل باید قابل ذخیره و سپس قابل نمایش باشند — ساختار `UserProfile` را یکدست نگه‌دار. +- کوئری list نباید N+1 شود؛ از join استفاده کن. diff --git a/assets/admin/pages/MyPatientsPage.tsx b/assets/admin/pages/MyPatientsPage.tsx index 365e01a3..c97a8278 100644 --- a/assets/admin/pages/MyPatientsPage.tsx +++ b/assets/admin/pages/MyPatientsPage.tsx @@ -169,6 +169,13 @@ function MyPatientsPageInner() { ), }); + const { data: recordDetail } = useQuery>({ + queryKey: ["patient-detail", selectedRecord?.uuid], + queryFn: () => api.get(`/api/v1/patient/${selectedRecord!.uuid}`), + enabled: !!selectedRecord, + }); + const patientProfile = (recordDetail?.data as PatientRecord | undefined)?.profile ?? null; + const { data: sessionsData, isLoading: sessionsLoading } = useQuery< PaginatedResponse >({ @@ -885,6 +892,35 @@ function MyPatientsPageInner() { + {patientProfile && ( +
+
اطلاعات بیمار
+
+ {([ + ["نام کامل", patientProfile.full_name], + ["کد ملی", patientProfile.national_code], + ["جنسیت", patientProfile.gender === "male" ? "مرد" : patientProfile.gender === "female" ? "زن" : patientProfile.gender], + ["تاریخ تولد", patientProfile.date_of_birth ? formatDate(patientProfile.date_of_birth) : null], + ["گروه خونی", patientProfile.blood_type], + ["وضعیت تأهل", patientProfile.marital_status], + ["شغل", patientProfile.job], + ["موبایل", patientProfile.mobile], + ["تلفن منزل", patientProfile.home_phone], + ["بیمه پایه", patientProfile.basic_insurance_name], + ["بیمه تکمیلی", patientProfile.supplementary_insurance_name], + ["آدرس", patientProfile.address], + ] as [string, string | null | undefined][]).map(([label, value]) => ( +
+
{label}
+
+ {value ? value : ثبت نشده} +
+
+ ))} +
+
+ )} +
`date_of_birth` یک Unix timestamp است؛ سمت کلاینت با `formatDate()` شمسی نمایش داده می‌شود. `profile` برای هر دو `entity_type` (doctor/clinic) یکسان است. + **Errors:** | Code | HTTP | Description | diff --git a/src/Patient/Controller/PatientController.php b/src/Patient/Controller/PatientController.php index 924e6084..f9d3e390 100644 --- a/src/Patient/Controller/PatientController.php +++ b/src/Patient/Controller/PatientController.php @@ -37,8 +37,37 @@ class PatientController extends BaseController private readonly ClinicRepository $clinicRepo, private readonly DoctorSecretaryRepository $secretaryRepo, private readonly UserActiveContextRepository $contextRepo, + private readonly \App\UserProfile\Repository\UserProfileRepository $profileRepo, + private readonly \App\Insurance\Repository\InsuranceRepository $insuranceRepo, ) {} + private function buildPatientProfile(User $patient): array + { + $p = $this->profileRepo->findByUser($patient); + $insName = function (?int $id): ?string { + if ($id === null) { return null; } + return $this->insuranceRepo->find($id)?->getName(); + }; + + return [ + 'full_name' => trim(($patient->getRealName() ?? '') . ' ' . ($p?->getFamily() ?? '')) ?: $patient->getRealName(), + 'national_code' => $p?->getNationalCode() ?? $patient->getNationalCode(), + 'gender' => $p?->getGender(), + 'date_of_birth' => $p?->getDateOfBirth(), + 'blood_type' => $p?->getBloodType(), + 'marital_status' => $p?->getMaritalStatus(), + 'job' => $p?->getJob(), + 'address' => $p?->getAddress(), + 'home_phone' => $p?->getHomePhone(), + 'work_phone' => $p?->getWorkPhone(), + 'mobile' => $patient->getMobileNumber(), + 'basic_insurance_id' => $p?->getBasicInsuranceId(), + 'basic_insurance_name' => $insName($p?->getBasicInsuranceId()), + 'supplementary_insurance_id' => $p?->getSupplementaryInsuranceId(), + 'supplementary_insurance_name' => $insName($p?->getSupplementaryInsuranceId()), + ]; + } + #[Route('/api/v1/patient/search-user', methods: ['GET'])] public function searchUser(Request $request, #[CurrentUser] User $user): JsonResponse { @@ -148,7 +177,10 @@ class PatientController extends BaseController return $this->error(ErrorCodes::ERR_PATIENT_NOT_FOUND, ErrorCodes::message(ErrorCodes::ERR_PATIENT_NOT_FOUND), 404); } - return $this->success($record->toArray()); + $data = $record->toArray(); + $data['profile'] = $this->buildPatientProfile($record->getUser()); + + return $this->success($data); } #[Route('/api/v1/patient/{uuid}/sessions', methods: ['GET'])]