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:
@@ -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 سالم.
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user