feat(patient): complete "اطلاعات پرونده" demographic form

Backend:
- Add profile columns field_of_study, province_id, city_id, postal_code,
  referral_source (UserProfile + migration).
- Extend PATCH /api/v1/patient/{uuid} to persist all demographic fields
  and return them in the patient profile payload.
- Support editable mobile (login identifier): validation, uniqueness,
  User.setMobileNumber, new ERR_PROFILE_002.
- Update docs/api/patient.md.

Frontend:
- New reusable Input, Field, and PatientRecordInfoForm (RHF + Zod).
- usePatient/useUpdatePatient hooks and patientForm mapping helpers.
- Extend the existing "info" tab in MyPatientsPage to the full field set
  via the shared form (province/city/insurance options, Jalali date).

Tests: Patient entity + PATCH integration (PHPUnit); form, hooks, and
mapping helpers (Vitest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-13 11:51:18 +03:30
co-authored by Claude Opus 4.8
parent 9d8de9bc33
commit 580bc983b3
19 changed files with 1042 additions and 128 deletions
+53 -8
View File
@@ -64,13 +64,21 @@ class PatientController extends BaseController
'full_name' => trim(($patient->getRealName() ?? '') . ' ' . ($p?->getFamily() ?? '')) ?: $patient->getRealName(),
'name' => $patient->getRealName(),
'family' => $p?->getFamily(),
'fathers_name' => $p?->getFathersName(),
'national_code' => $p?->getNationalCode() ?? $patient->getNationalCode(),
'gender' => $p?->getGender(),
'date_of_birth' => $p?->getDateOfBirth(),
'blood_type' => $p?->getBloodType(),
'marital_status' => $p?->getMaritalStatus(),
'education' => $p?->getEducation(),
'field_of_study' => $p?->getFieldOfStudy(),
'job' => $p?->getJob(),
'address' => $p?->getAddress(),
'province_id' => $p?->getProvinceId(),
'city_id' => $p?->getCityId(),
'postal_code' => $p?->getPostalCode(),
'referral_source' => $p?->getReferralSource(),
'description' => $p?->getDescription(),
'home_phone' => $p?->getHomePhone(),
'work_phone' => $p?->getWorkPhone(),
'mobile' => $patient->getMobileNumber(),
@@ -251,19 +259,45 @@ class PatientController extends BaseController
$patient->setNationalCode($nationalCode);
}
}
// موبایل = شناسه ورود کاربر؛ تغییر آن باید ۱۱ رقمی معتبر و در سطح کاربران یکتا باشد
if (array_key_exists('mobile', $data)) {
$mobile = trim((string) ($data['mobile'] ?? ''));
if ($mobile !== '' && $mobile !== $patient->getMobileNumber()) {
if (!preg_match('/^09\d{9}$/', $mobile)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422, 'mobile');
}
$owner = $this->userRepo->findByMobile($mobile);
if ($owner !== null && $owner->getId() !== $patient->getId()) {
return $this->error(
ErrorCodes::ERR_PROFILE_MOBILE_TAKEN,
ErrorCodes::message(ErrorCodes::ERR_PROFILE_MOBILE_TAKEN),
409,
'mobile'
);
}
$patient->setMobileNumber($mobile);
}
}
$this->userRepo->save($patient);
$profile = $this->profileRepo->findByUser($patient) ?? new UserProfile($patient);
$stringFields = [
'family' => 'setFamily',
'gender' => 'setGender',
'blood_type' => 'setBloodType',
'marital_status' => 'setMaritalStatus',
'job' => 'setJob',
'address' => 'setAddress',
'home_phone' => 'setHomePhone',
'work_phone' => 'setWorkPhone',
'family' => 'setFamily',
'fathers_name' => 'setFathersName',
'gender' => 'setGender',
'blood_type' => 'setBloodType',
'marital_status' => 'setMaritalStatus',
'education' => 'setEducation',
'field_of_study' => 'setFieldOfStudy',
'job' => 'setJob',
'address' => 'setAddress',
'postal_code' => 'setPostalCode',
'referral_source' => 'setReferralSource',
'description' => 'setDescription',
'home_phone' => 'setHomePhone',
'work_phone' => 'setWorkPhone',
];
foreach ($stringFields as $key => $setter) {
if (array_key_exists($key, $data)) {
@@ -272,6 +306,17 @@ class PatientController extends BaseController
}
}
$intFields = [
'province_id' => 'setProvinceId',
'city_id' => 'setCityId',
];
foreach ($intFields as $key => $setter) {
if (array_key_exists($key, $data)) {
$v = $data[$key];
$profile->$setter(($v === null || $v === '') ? null : (int) $v);
}
}
if (array_key_exists('national_code', $data)) {
$nc = trim((string) ($data['national_code'] ?? ''));
$profile->setNationalCode($nc === '' ? null : $nc);