refactor: normalize date handling to Tehran timezone
- Updated date handling in BlogSeoFields and ScheduleSection to use Tehran timezone utilities for consistency. - Introduced `toTehranClockTime`, `tehranWallClockToUnix`, and `todayIso` functions for accurate date representation. - Modified various components to utilize these new utilities, ensuring that date strings are correctly formatted and timestamps are accurately converted. - Enhanced API documentation to clarify the handling of date fields, emphasizing the importance of server-local midnight. - Added tests to verify that date overrides and holidays maintain the correct day without shifting due to timezone discrepancies.
This commit is contained in:
@@ -42,6 +42,21 @@ class AppointmentSettingsController extends BaseController
|
||||
private readonly \App\Secretary\Security\SecretaryAccessChecker $secretaryAccess,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* تاریخِ روزمحور (`Y-m-d`) را به نیمهشبِ همان روز در تایمزون سرور تبدیل میکند.
|
||||
* بدون این نرمالسازی، ورودیهایی که ساعت دارند (مثلاً ISO با `Z`) روز را جابهجا
|
||||
* میکنند. `null` یعنی ورودی نامعتبر.
|
||||
*/
|
||||
private function parseDayTimestamp(string $value): ?int
|
||||
{
|
||||
$ts = strtotime(trim($value));
|
||||
if ($ts === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) strtotime(date('Y-m-d', $ts) . ' 00:00:00');
|
||||
}
|
||||
|
||||
/**
|
||||
* نوع نوبتدهی پس از اولین ثبت غیرقابلتغییر است — اما فقط داخل همان context.
|
||||
* پزشکی که در مطب شخصی نوبتدهی اسلاتی دارد، همچنان میتواند در کلینیک سرویسی
|
||||
@@ -289,8 +304,8 @@ class AppointmentSettingsController extends BaseController
|
||||
return $err;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($dateStr);
|
||||
if ($timestamp === false || $timestamp === -1) {
|
||||
$timestamp = $this->parseDayTimestamp($dateStr);
|
||||
if ($timestamp === null) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'فرمت تاریخ نادرست است', 422, 'date');
|
||||
}
|
||||
|
||||
@@ -320,8 +335,8 @@ class AppointmentSettingsController extends BaseController
|
||||
if (array_key_exists('reason', $data)) $override->setReason($data['reason']);
|
||||
if (array_key_exists('custom_slots', $data)) $override->setSetting($data['custom_slots']);
|
||||
if (!empty($data['date'])) {
|
||||
$ts = strtotime($data['date']);
|
||||
if ($ts !== false) $override->setDate($ts);
|
||||
$ts = $this->parseDayTimestamp($data['date']);
|
||||
if ($ts !== null) $override->setDate($ts);
|
||||
}
|
||||
|
||||
$this->overrideRepo->save($override);
|
||||
@@ -430,10 +445,10 @@ class AppointmentSettingsController extends BaseController
|
||||
return $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'کلینیک فقط میتواند تعطیلی مخصوص خودش را ثبت کند', 403, 'clinic_uuid');
|
||||
}
|
||||
|
||||
$startTs = strtotime($startStr);
|
||||
$endTs = strtotime($endStr);
|
||||
$startTs = $this->parseDayTimestamp($startStr);
|
||||
$endTs = $this->parseDayTimestamp($endStr);
|
||||
|
||||
if (!$startTs || !$endTs || $endTs < $startTs) {
|
||||
if ($startTs === null || $endTs === null || $endTs < $startTs) {
|
||||
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'تاریخ نادرست است', 422);
|
||||
}
|
||||
|
||||
@@ -461,12 +476,12 @@ class AppointmentSettingsController extends BaseController
|
||||
if (array_key_exists('active', $data)) $holiday->setActive((bool) $data['active']);
|
||||
if (array_key_exists('reason', $data)) $holiday->setReason($data['reason']);
|
||||
if (!empty($data['start_date'])) {
|
||||
$ts = strtotime($data['start_date']);
|
||||
if ($ts) $holiday->setStartDate($ts);
|
||||
$ts = $this->parseDayTimestamp($data['start_date']);
|
||||
if ($ts !== null) $holiday->setStartDate($ts);
|
||||
}
|
||||
if (!empty($data['end_date'])) {
|
||||
$ts = strtotime($data['end_date']);
|
||||
if ($ts) $holiday->setEndDate($ts);
|
||||
$ts = $this->parseDayTimestamp($data['end_date']);
|
||||
if ($ts !== null) $holiday->setEndDate($ts);
|
||||
}
|
||||
|
||||
$this->holidayRepo->save($holiday);
|
||||
|
||||
@@ -91,6 +91,9 @@ class DateOverride
|
||||
'clinic_uuid' => $this->clinic?->getUuid(),
|
||||
'context' => $this->clinic === null ? 'personal' : 'clinic',
|
||||
'date' => $this->date,
|
||||
// نمایش تاریخ باید از این رشته خوانده شود؛ تبدیل timestamp در مرورگر
|
||||
// با UTC انجام میشود و برای نیمهشبِ تهران یک روز عقب میافتد.
|
||||
'date_string' => date('Y-m-d', $this->date),
|
||||
'active' => $this->active,
|
||||
'reason' => $this->reason,
|
||||
'custom_slots' => $this->setting ?? [],
|
||||
|
||||
@@ -90,6 +90,10 @@ class Holiday
|
||||
'scope' => $this->clinic === null ? 'global' : 'clinic',
|
||||
'start_date' => $this->startDate,
|
||||
'end_date' => $this->endDate,
|
||||
// نمایش تاریخ باید از این رشتهها خوانده شود؛ تبدیل timestamp در مرورگر
|
||||
// با UTC انجام میشود و برای نیمهشبِ تهران یک روز عقب میافتد.
|
||||
'start_date_string' => date('Y-m-d', $this->startDate),
|
||||
'end_date_string' => date('Y-m-d', $this->endDate),
|
||||
'active' => $this->active,
|
||||
'reason' => $this->reason,
|
||||
'created_at' => $this->createdAt,
|
||||
|
||||
Reference in New Issue
Block a user