feat(timezone): implement Tehran timezone handling across the application

This commit is contained in:
hamed
2026-07-16 00:25:21 +03:30
parent 8cd1253b81
commit 2f060bd5be
10 changed files with 90 additions and 17 deletions
+40 -4
View File
@@ -14,6 +14,10 @@ export function formatNumber(n: number): string {
return new Intl.NumberFormat('fa-IR').format(n);
}
// تایم‌زون رسمی سراسری برنامه = ایران. همهٔ نمایش/تبدیل تاریخ باید با این tz باشد،
// مستقل از تایم‌زون مرورگرِ کاربر (اجباری).
export const APP_TZ = 'Asia/Tehran';
export function toDate(val: string | number | null | undefined): Date | null {
if (val == null || val === '') return null;
if (typeof val === 'number') return new Date(val * 1000);
@@ -26,6 +30,7 @@ export function formatDate(val: string | number | null | undefined): string {
const d = toDate(val);
if (!d || isNaN(d.getTime())) return '—';
return new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
timeZone: APP_TZ,
year: 'numeric', month: '2-digit', day: '2-digit',
}).format(d);
}
@@ -34,16 +39,47 @@ export function formatDateTime(val: string | number | null | undefined): string
const d = toDate(val);
if (!d || isNaN(d.getTime())) return '—';
return new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
timeZone: APP_TZ,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit',
}).format(d);
}
// ساعت (HH:MM) یک timestamp ثانیه‌ای به وقت ایران — برای تایم‌لاین/جدول نوبت‌ها.
export function formatTime(tsSeconds: number): string {
return new Intl.DateTimeFormat('fa-IR', {
timeZone: APP_TZ, hour: '2-digit', minute: '2-digit',
}).format(new Date(tsSeconds * 1000));
}
// اختلاف دقیقه‌ایِ یک تایم‌زون با UTC در لحظهٔ مشخص (ایران ثابت +03:30 است ولی این
// روش عمومی و درست است).
function tzOffsetMinutes(date: Date, tz: string): number {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: tz, hour12: false,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
}).formatToParts(date).reduce<Record<string, string>>((a, p) => (a[p.type] = p.value, a), {});
const asUTC = Date.UTC(+parts.year, +parts.month - 1, +parts.day, +parts.hour, +parts.minute, +parts.second);
return Math.round((asUTC - date.getTime()) / 60000);
}
// «ساعت دیواریِ ایران» (تاریخ Y-m-d + HH:MM) → Unix ثانیه. مستقل از tz مرورگر، تا
// ثبت نوبت با ساعت واردشده دقیقاً همان لحظه به وقت ایران باشد.
export function tehranWallClockToUnix(isoDate: string, time: string): number {
const [Y, M, D] = isoDate.split('-').map(Number);
const [h, m] = (time || '00:00').split(':').map(Number);
const asUtc = Date.UTC(Y, (M || 1) - 1, D || 1, h || 0, m || 0, 0);
const offset = tzOffsetMinutes(new Date(asUtc), APP_TZ);
return Math.floor((asUtc - offset * 60000) / 1000);
}
// تاریخ تقویمیِ میلادی (Y-m-d) یک لحظه، به وقت ایران.
export function toGregorianDate(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
// en-CA → قالب YYYY-MM-DD
return new Intl.DateTimeFormat('en-CA', {
timeZone: APP_TZ, year: 'numeric', month: '2-digit', day: '2-digit',
}).format(d);
}
// API stores contract dates as Unix seconds; the date input speaks Y-m-d strings.