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:
@@ -9,11 +9,16 @@ import type { PatientProfile } from '@/types';
|
||||
import type { PatientFormValues } from '@/components/PatientRecordInfoForm';
|
||||
|
||||
describe('date helpers', () => {
|
||||
it('timestamp ↔ YYYY-MM-DD رفتوبرگشت', () => {
|
||||
// مبنا نیمهشبِ تهران است (نه UTC)، چون بکاند هم تاریخهای روزمحور را با
|
||||
// strtotime در تایمزون سرور (Asia/Tehran) ذخیره میکند.
|
||||
it('timestamp ↔ YYYY-MM-DD رفتوبرگشت روی نیمهشبِ تهران', () => {
|
||||
const ts = dateStrToTs('2025-01-07');
|
||||
expect(ts).toBe(Math.floor(Date.parse('2025-01-07T00:00:00Z') / 1000));
|
||||
expect(ts).toBe(Math.floor(Date.UTC(2025, 0, 6, 20, 30, 0) / 1000));
|
||||
expect(tsToDateStr(ts)).toBe('2025-01-07');
|
||||
});
|
||||
it('timestampهای ساختهشده توسط سرور یک روز عقب نمیروند', () => {
|
||||
expect(tsToDateStr(Math.floor(Date.UTC(2026, 6, 28, 20, 30, 0) / 1000))).toBe('2026-07-29');
|
||||
});
|
||||
it('خالی/نامعتبر → مقدار تهی', () => {
|
||||
expect(tsToDateStr(null)).toBe('');
|
||||
expect(tsToDateStr(0)).toBe('');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SelectOption } from '../components/ui/SearchableSelect';
|
||||
import type { PatientProfile, PatientProfileUpdate } from '../types';
|
||||
import type { PatientFormValues } from '../components/PatientRecordInfoForm';
|
||||
import { unixToIso, tehranWallClockToUnix } from './utils';
|
||||
|
||||
// ── enumهای ثابت فرم «اطلاعات پرونده» (بدون منبع API) ─────────────────────────
|
||||
export const GENDER_OPTS: SelectOption[] = [
|
||||
@@ -21,14 +22,15 @@ export const REFERRAL_OPTS: SelectOption[] = [
|
||||
].map((v) => ({ value: v, label: v }));
|
||||
|
||||
// ── تبدیل تاریخ: timestamp ثانیهای ↔ رشتهٔ میلادی YYYY-MM-DD ─────────────────
|
||||
// هر دو سمت به وقت ایراناند تا با نیمهشبِ محلیِ ذخیرهشده در بکاند همراستا باشند؛
|
||||
// تبدیل UTC روز را یک واحد جابهجا میکرد.
|
||||
export function tsToDateStr(ts?: number | null): string {
|
||||
if (!ts) return '';
|
||||
return new Date(ts * 1000).toISOString().slice(0, 10);
|
||||
return ts ? unixToIso(ts) : '';
|
||||
}
|
||||
export function dateStrToTs(s: string): number | null {
|
||||
if (!s) return null;
|
||||
const t = Date.parse(`${s}T00:00:00Z`);
|
||||
return Number.isNaN(t) ? null : Math.floor(t / 1000);
|
||||
const t = tehranWallClockToUnix(s, '00:00');
|
||||
return Number.isNaN(t) ? null : t;
|
||||
}
|
||||
|
||||
/** پروفایل API → مقادیر پیشفرض فرم. */
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
formatDate,
|
||||
formatDateTime,
|
||||
toGregorianDate,
|
||||
toTehranClockTime,
|
||||
tehranWallClockToUnix,
|
||||
unixToIso,
|
||||
todayIso,
|
||||
maskMobile,
|
||||
cn,
|
||||
toEnglishDigits,
|
||||
@@ -107,6 +111,42 @@ describe('toGregorianDate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('unixToIso / todayIso / toTehranClockTime', () => {
|
||||
// نیمهشبِ ۲۹ ژوئیهٔ ۲۰۲۶ به وقت تهران = ۲۸ ژوئیه ۲۰:۳۰ UTC. تبدیل با
|
||||
// toISOString همینجا یک روز عقب میرفت — رگرسیونِ «تاریخ خاص یک روز قبل».
|
||||
const tehranMidnight = Math.floor(Date.UTC(2026, 6, 28, 20, 30, 0) / 1000);
|
||||
|
||||
it('timestamp نیمهشبِ تهران همان روز را میدهد، نه روز قبل', () => {
|
||||
expect(unixToIso(tehranMidnight)).toBe('2026-07-29');
|
||||
});
|
||||
|
||||
it('لحظهٔ آخر شب به وقت تهران هنوز همان روز است', () => {
|
||||
const lateNight = Math.floor(Date.UTC(2026, 6, 29, 20, 0, 0) / 1000); // ۲۳:۳۰ تهران
|
||||
expect(unixToIso(lateNight)).toBe('2026-07-29');
|
||||
});
|
||||
|
||||
it('مقدار خالی رشتهٔ تهی میدهد', () => {
|
||||
expect(unixToIso(null)).toBe('');
|
||||
expect(unixToIso(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('todayIso همان تاریخ تهرانِ لحظهٔ جاری است', () => {
|
||||
expect(todayIso()).toBe(toGregorianDate(new Date()));
|
||||
expect(todayIso()).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
|
||||
it('ساعت دیواری تهران با رقم لاتین و ۲۴ساعته برمیگردد', () => {
|
||||
expect(toTehranClockTime(new Date(tehranMidnight * 1000))).toBe('00:00');
|
||||
expect(toTehranClockTime(new Date(Date.UTC(2026, 6, 29, 10, 0, 0)))).toBe('13:30');
|
||||
});
|
||||
|
||||
it('رفتوبرگشتِ tehranWallClockToUnix ↔ unixToIso روز را جابهجا نمیکند', () => {
|
||||
const ts = tehranWallClockToUnix('2026-07-29', '00:00');
|
||||
expect(ts).toBe(tehranMidnight);
|
||||
expect(unixToIso(ts)).toBe('2026-07-29');
|
||||
});
|
||||
});
|
||||
|
||||
describe('maskMobile', () => {
|
||||
it('شماره ۱۱ رقمی را ماسک میکند', () => {
|
||||
expect(maskMobile('09123456789')).toBe('0912***789');
|
||||
|
||||
@@ -87,6 +87,14 @@ export function toGregorianDate(d: Date): string {
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
// ساعت دیواریِ ایران به صورت «HH:MM» با رقم لاتین — برای مقداردهی <input type="time">
|
||||
// (بر خلاف formatTime که رقم فارسی میدهد و در input معتبر نیست).
|
||||
export function toTehranClockTime(d: Date): string {
|
||||
return new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: APP_TZ, hour12: false, hour: '2-digit', minute: '2-digit',
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
// API stores contract dates as Unix seconds; the date input speaks Y-m-d strings.
|
||||
export function isoToUnix(iso: string): number | null {
|
||||
const d = toDate(iso);
|
||||
@@ -98,6 +106,12 @@ export function unixToIso(ts: number | null | undefined): string {
|
||||
return toGregorianDate(new Date(ts * 1000));
|
||||
}
|
||||
|
||||
// «امروز» به وقت ایران. هرگز از `new Date().toISOString().slice(0,10)` استفاده نکن؛
|
||||
// آن تاریخِ UTC است و بعد از ۲۰:۳۰ بهوقت تهران یک روز جلو میافتد.
|
||||
export function todayIso(): string {
|
||||
return toGregorianDate(new Date());
|
||||
}
|
||||
|
||||
// عنوان «دکتر» فقط در نمایش افزوده میشود و هرگز در فیلد name دیتابیس ذخیره نمیشود
|
||||
// (backend با stripDoctorTitle حذف میکند). این helper تنها نقطهٔ افزودن عنوان است تا
|
||||
// در کل پنل یکسان باشد و از «دکتر دکتر …» یا نمایش بدون عنوان جلوگیری شود.
|
||||
|
||||
Reference in New Issue
Block a user