fix(profile): persist & expose birthday as a single Unix field

Birthday was never saved (hydrate forced null) and not returned in a form the
clients use. Now:
- hydrate stores the incoming `birthday` (Unix) into date_of_birth; `birthday`
  takes priority over a stray `date_of_birth: null` in the same payload so it
  can't be wiped.
- toArray exposes a single `birthday` (Unix) key — drop the duplicate
  `date_of_birth` output to avoid overlap.
- admin UserDetailPage reads profile.birthday (formatDate renders Jalali).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-06-16 11:42:31 +03:30
co-authored by Claude Opus 4.8
parent 800f9ad8cd
commit b60dbdadba
4 changed files with 90 additions and 9 deletions
@@ -0,0 +1,78 @@
# ذخیره و نمایش صحیح تاریخ تولد در پروفایل (Unix)
## پروژه
`clinicpro` (Backend — منبع حقیقت).
> **Cross-repo:** سایت عمومی `nobat724_front` تاریخ تولد را به‌صورت **Unix timestamp** با کلید `birthday` می‌فرستد (تبدیل جلالی↔Unix کاملاً سمت فرانت با `changeDateType` انجام می‌شود) و انتظار دارد همان Unix را برای نمایش بگیرد. پرامپت همتای فرانت: `nobat724_front/.claude/prompt/profile-birthday-send-direction-fix.md`. این پرامپت **اول** اجرا شود.
## زمینه
در فرم اطلاعات کاربری (`/dashboard`)، فیلد «تاریخ تولد» مقدارش با کلید `birthday` به‌صورت **Unix timestamp** ارسال می‌شود (فرانت با `moment(jalali,"jYYYY/jMM/jDD").unix()` تبدیل می‌کند). اما در `UserProfileController::hydrate` این مقدار **عمداً نادیده گرفته و `null` ذخیره می‌شود** (یک TODO نیمه‌کاره). در نتیجه تاریخ تولد هرگز ذخیره نمی‌شود؛ و `toArray` کلید `birthday` را اصلاً برنمی‌گرداند (فقط `date_of_birth`)، پس فرانت چیزی برای نمایش ندارد.
## مشکل / هدف
۱. ورودی: `birthday` (Unix integer) را در ستون `date_of_birth` ذخیره کن (به‌جای `null`).
۲. خروجی: `toArray` کلید `birthday` = همان Unix (`date_of_birth`) را برگرداند تا فرانت با `changeDateType` به جلالی نمایش دهد.
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/UserProfile/Controller/UserProfileController.php` | `hydrate` — ذخیره `birthday`(Unix) در `date_of_birth` |
| `src/UserProfile/Entity/UserProfile.php` | `toArray` — افزودن کلید `birthday` (= Unix `date_of_birth`) |
## وضعیت فعلی (کد واقعی)
`hydrate` — مقدار دور ریخته می‌شود:
```php
// birthday: accept Jalali string "1370-05-15" stored as-is converted to Unix
if (array_key_exists('birthday', $data) && $data['birthday'] !== null) {
// Will be replaced with JalaliDateService in Task 16
$profile->setDateOfBirth(null); // ❌ هرگز ذخیره نمی‌شود
}
if (array_key_exists('date_of_birth', $data)) $profile->setDateOfBirth($data['date_of_birth']);
```
`UserProfile`:
```php
#[ORM\Column(name: 'date_of_birth', type: 'integer', nullable: true)]
private ?int $dateOfBirth = null;
// toArray():
'date_of_birth' => $this->dateOfBirth, // کلید birthday وجود ندارد
```
> فرانت `birthday` را Unix می‌فرستد (مثلاً `-405...` یا یک integer مثبت/منفی برای تاریخ‌های قبل/بعد ۱۹۷۰). ممکن است به‌صورت string-encoded integer هم بیاید؛ با `(int)` ایمن تبدیل کن.
## وظایف
### ۱. ذخیره `birthday` (Unix) در `hydrate`
بلوک نیمه‌کاره را با ذخیره‌ی واقعی جایگزین کن:
```php
if (array_key_exists('birthday', $data)) {
$b = $data['birthday'];
$profile->setDateOfBirth(($b === null || $b === '') ? null : (int) $b);
}
if (array_key_exists('date_of_birth', $data)) {
$profile->setDateOfBirth($data['date_of_birth'] === null ? null : (int) $data['date_of_birth']);
}
```
### ۲. خروجی `birthday` در `toArray`
در `UserProfile::toArray`، کنار `date_of_birth`، کلید `birthday` را هم برگردان (همان مقدار Unix):
```php
'date_of_birth' => $this->dateOfBirth,
'birthday' => $this->dateOfBirth,
```
> هر دو کلید همان Unix‌اند؛ `birthday` برای مصرف مستقیم فرانت (که `moment.unix(birthday)` می‌زند) و `date_of_birth` برای سازگاری عقب‌رو.
### ۳. مستندسازی
اگر سند پروفایلی در `docs/api/` هست، فیلد `birthday` (Unix در ورودی و خروجی) را اضافه کن. اگر نیست، نیازی به ساخت فایل جدید نیست.
## نکات مهم
- **هیچ تبدیل جلالی در بک‌اند لازم نیست** — `JalaliDateService` استفاده نمی‌شود؛ تبدیل کاملاً سمت فرانت است. ساده نگه‌دار.
- `birthday` ممکن است integer یا string-encoded-integer باشد → `(int)` ایمن.
- migration لازم نیست (`date_of_birth` integer از قبل هست).
- پاسخ‌ها از `BaseController`.
- تست: `PATCH /api/v1/user-profile/{uuid}` با `{"birthday": 12345678}` → در DB `date_of_birth = 12345678`؛ سپس `GET` → هم `birthday` و هم `date_of_birth` برابر `12345678`. round-trip سالم.
+2 -2
View File
@@ -36,7 +36,7 @@ interface UserProfileData {
uuid: string;
national_code: string | null;
gender: string | null;
date_of_birth: number | null;
birthday: number | null;
blood_type: string | null;
marital_status: string | null;
education: string | null;
@@ -539,7 +539,7 @@ export default function UserDetailPage() {
<InfoCard icon={UserIcon} label="نام پدر" value={profile.fathers_name} />
<InfoCard icon={ClipboardDocumentIcon} label="کد ملی" value={profile.national_code} mono copyable />
<InfoCard icon={UserIcon} label="جنسیت" value={profile.gender ? GENDER_LABELS[profile.gender] ?? profile.gender : null} />
<InfoCard icon={CalendarIcon} label="تاریخ تولد" value={profile.date_of_birth ? formatDate(profile.date_of_birth as any) : null} />
<InfoCard icon={CalendarIcon} label="تاریخ تولد" value={profile.birthday ? formatDate(profile.birthday) : null} />
<InfoCard icon={UserIcon} label="گروه خونی" value={profile.blood_type} />
<InfoCard icon={UserIcon} label="وضعیت تأهل" value={profile.marital_status ? MARITAL_LABELS[profile.marital_status] ?? profile.marital_status : null} />
<InfoCard icon={UserIcon} label="تحصیلات" value={profile.education} />
@@ -155,13 +155,16 @@ class UserProfileController extends BaseController
);
if (array_key_exists('sharing_with_user', $data)) $profile->setSharingWithUser((bool) $data['sharing_with_user']);
// birthday: accept Jalali string "1370-05-15" stored as-is converted to Unix
if (array_key_exists('birthday', $data) && $data['birthday'] !== null) {
// Store as string-encoded Unix; for now keep as null if conversion unavailable
// Will be replaced with JalaliDateService in Task 16
$profile->setDateOfBirth(null);
// birthday / date_of_birth: Unix timestamp (frontend converts Jalali↔Unix
// via changeDateType). `birthday` takes priority; `date_of_birth` is only a
// fallback so a stray `date_of_birth: null` in the payload can't wipe it.
if (array_key_exists('birthday', $data)) {
$b = $data['birthday'];
$profile->setDateOfBirth(($b === null || $b === '') ? null : (int) $b);
} elseif (array_key_exists('date_of_birth', $data)) {
$b = $data['date_of_birth'];
$profile->setDateOfBirth(($b === null || $b === '') ? null : (int) $b);
}
if (array_key_exists('date_of_birth', $data)) $profile->setDateOfBirth($data['date_of_birth']);
// Insurance references (category IDs)
if (array_key_exists('basic_insurance', $data)) {
+1 -1
View File
@@ -160,7 +160,7 @@ class UserProfile
'national_code' => $this->nationalCode,
'national_code_approved' => $this->nationalCodeApproved,
'gender' => $this->gender,
'date_of_birth' => $this->dateOfBirth,
'birthday' => $this->dateOfBirth,
'blood_type' => $this->bloodType,
'marital_status' => $this->maritalStatus,
'education' => $this->education,