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:
@@ -85,6 +85,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
public function getUserIdentifier(): string { return $this->mobileNumber; }
|
||||
public function eraseCredentials(): void {}
|
||||
|
||||
/** موبایل همان شناسه ورود است؛ تغییر آن نامکاربری کاربر را نیز تغییر میدهد. یکتایی در سطح فراخوان بررسی شود. */
|
||||
public function setMobileNumber(string $mobileNumber): self { $this->mobileNumber = $mobileNumber; $this->updatedAt = time(); return $this; }
|
||||
public function setEmail(?string $email): self { $this->email = $email; return $this; }
|
||||
public function setRealName(?string $name): self { $this->realName = $name; $this->updatedAt = time(); return $this; }
|
||||
public function setNationalCode(?string $code): self
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -65,6 +65,7 @@ class ErrorCodes
|
||||
|
||||
// Profile
|
||||
public const ERR_PROFILE_NATIONAL_CODE_TAKEN = 'ERR_PROFILE_001';
|
||||
public const ERR_PROFILE_MOBILE_TAKEN = 'ERR_PROFILE_002';
|
||||
|
||||
// SMS Wallet
|
||||
public const ERR_SMS_WALLET_INSUFFICIENT = 'ERR_SMS_WALLET_INSUFFICIENT';
|
||||
@@ -140,6 +141,7 @@ class ErrorCodes
|
||||
self::ERR_SERVICE_NOT_FOUND => 'سرویس یافت نشد',
|
||||
self::ERR_PATIENT_NOT_FOUND => 'پرونده بیمار یافت نشد',
|
||||
self::ERR_PROFILE_NATIONAL_CODE_TAKEN => 'این کد ملی قبلاً برای کاربر دیگری ثبت شده است',
|
||||
self::ERR_PROFILE_MOBILE_TAKEN => 'این شماره موبایل قبلاً برای کاربر دیگری ثبت شده است',
|
||||
self::ERR_SESSION_NOT_FOUND => 'مراجعه یافت نشد',
|
||||
self::ERR_SMS_WALLET_INSUFFICIENT => 'موجودی کیف پیامک کافی نیست',
|
||||
self::ERR_RATING_NOT_ELIGIBLE => 'برای ثبت نظر یا امتیاز باید در یک ماه گذشته نوبت تاییدشده نزد این پزشک داشته باشید',
|
||||
|
||||
@@ -55,12 +55,27 @@ class UserProfile
|
||||
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||
private ?string $education = null;
|
||||
|
||||
#[ORM\Column(name: 'field_of_study', type: 'string', length: 100, nullable: true)]
|
||||
private ?string $fieldOfStudy = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, nullable: true)]
|
||||
private ?string $job = null;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $address = null;
|
||||
|
||||
#[ORM\Column(name: 'province_id', type: 'integer', nullable: true)]
|
||||
private ?int $provinceId = null;
|
||||
|
||||
#[ORM\Column(name: 'city_id', type: 'integer', nullable: true)]
|
||||
private ?int $cityId = null;
|
||||
|
||||
#[ORM\Column(name: 'postal_code', type: 'string', length: 20, nullable: true)]
|
||||
private ?string $postalCode = null;
|
||||
|
||||
#[ORM\Column(name: 'referral_source', type: 'string', length: 100, nullable: true)]
|
||||
private ?string $referralSource = null;
|
||||
|
||||
#[ORM\Column(name: 'home_phone', type: 'string', length: 30, nullable: true)]
|
||||
private ?string $homePhone = null;
|
||||
|
||||
@@ -116,8 +131,13 @@ class UserProfile
|
||||
public function getBloodType(): ?string { return $this->bloodType; }
|
||||
public function getMaritalStatus(): ?string { return $this->maritalStatus; }
|
||||
public function getEducation(): ?string { return $this->education; }
|
||||
public function getFieldOfStudy(): ?string { return $this->fieldOfStudy; }
|
||||
public function getJob(): ?string { return $this->job; }
|
||||
public function getAddress(): ?string { return $this->address; }
|
||||
public function getProvinceId(): ?int { return $this->provinceId; }
|
||||
public function getCityId(): ?int { return $this->cityId; }
|
||||
public function getPostalCode(): ?string { return $this->postalCode; }
|
||||
public function getReferralSource(): ?string { return $this->referralSource; }
|
||||
public function getHomePhone(): ?string { return $this->homePhone; }
|
||||
public function getWorkPhone(): ?string { return $this->workPhone; }
|
||||
public function getInsuranceId(): ?string { return $this->insuranceId; }
|
||||
@@ -140,8 +160,13 @@ class UserProfile
|
||||
public function setBloodType(?string $v): self { $this->bloodType = $v; $this->touch(); return $this; }
|
||||
public function setMaritalStatus(?string $v): self { $this->maritalStatus = $v; $this->touch(); return $this; }
|
||||
public function setEducation(?string $v): self { $this->education = $v; $this->touch(); return $this; }
|
||||
public function setFieldOfStudy(?string $v): self { $this->fieldOfStudy = $v; $this->touch(); return $this; }
|
||||
public function setJob(?string $v): self { $this->job = $v; $this->touch(); return $this; }
|
||||
public function setAddress(?string $v): self { $this->address = $v; $this->touch(); return $this; }
|
||||
public function setProvinceId(?int $v): self { $this->provinceId = $v; $this->touch(); return $this; }
|
||||
public function setCityId(?int $v): self { $this->cityId = $v; $this->touch(); return $this; }
|
||||
public function setPostalCode(?string $v): self { $this->postalCode = $v; $this->touch(); return $this; }
|
||||
public function setReferralSource(?string $v): self { $this->referralSource = $v; $this->touch(); return $this; }
|
||||
public function setHomePhone(?string $v): self { $this->homePhone = $v; $this->touch(); return $this; }
|
||||
public function setWorkPhone(?string $v): self { $this->workPhone = $v; $this->touch(); return $this; }
|
||||
public function setInsuranceId(?string $v): self { $this->insuranceId = $v; $this->touch(); return $this; }
|
||||
@@ -170,8 +195,13 @@ class UserProfile
|
||||
'blood_type' => $this->bloodType,
|
||||
'marital_status' => $this->maritalStatus,
|
||||
'education' => $this->education,
|
||||
'field_of_study' => $this->fieldOfStudy,
|
||||
'job' => $this->job,
|
||||
'address' => $this->address,
|
||||
'province_id' => $this->provinceId,
|
||||
'city_id' => $this->cityId,
|
||||
'postal_code' => $this->postalCode,
|
||||
'referral_source' => $this->referralSource,
|
||||
'home_phone' => $this->homePhone,
|
||||
'work_phone' => $this->workPhone,
|
||||
'insurance_id' => $this->insuranceId,
|
||||
|
||||
Reference in New Issue
Block a user