fix: keep birthdays and show Shamsi dates

Imported customers keep their birthday, and assistant text plus remaining calendars use the Jalali date.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-13 18:04:53 +03:30
co-authored by Cursor
parent 5f4cc47c41
commit cf326d1ed9
34 changed files with 637 additions and 105 deletions
+5 -7
View File
@@ -1,6 +1,6 @@
import type { GamificationEvent, GamificationTxDto } from '@mento/shared';
import { GAMIFICATION_EVENTS } from '@mento/shared';
import { toFaDigits } from '@/lib/jalali';
import { formatJalaliDateTime, formatJalaliDay, toFaDigits } from '@/lib/jalali';
export { apiError } from '@/lib/routines';
@@ -81,15 +81,13 @@ export function timeAgo(iso: string): string {
if (hours < 24) return `${toFaDigits(hours)} ساعت پیش`;
const days = Math.floor(hours / 24);
if (days < 7) return `${toFaDigits(days)} روز پیش`;
return new Intl.DateTimeFormat('fa-IR', { dateStyle: 'medium' }).format(d);
return formatJalaliDay(d);
}
export function formatDateTime(iso: string): string {
try {
return new Intl.DateTimeFormat('fa-IR', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(iso));
} catch {
return iso;
}
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return formatJalaliDateTime(d);
}
export const REWARD_ICONS = ['gift', 'star', 'trophy', 'medal', 'crown', 'calendar-sun', 'bag', 'target', 'fire'] as const;
+95
View File
@@ -204,6 +204,11 @@ export function formatJalaliDayClock(d: Date): string {
return `${toFaDigits(jd)} ${jalaliMonthName(jm)} ${formatClockFa(d)}`;
}
/** مثل: ۸ شهریور ۱۴۰۵، ۱۶:۳۰ */
export function formatJalaliDateTime(d: Date): string {
return `${formatJalaliDay(d)}، ${formatClockFa(d)}`;
}
export function calendarDaysBetween(from: Date, to: Date): number {
return Math.round(
(startOfLocalDay(to).getTime() - startOfLocalDay(from).getTime()) / 86_400_000,
@@ -270,3 +275,93 @@ export function formatJalaliNumeric(d: Date): string {
const { jy, jm, jd } = dateToJalali(d);
return `${toFaDigits(jy)}/${toFaDigits(String(jm).padStart(2, '0'))}/${toFaDigits(String(jd).padStart(2, '0'))}`;
}
const GREG_MONTHS: Array<[string, number]> = [
['ژانویه', 1],
['january', 1],
['فوریه', 2],
['february', 2],
['مارس', 3],
['march', 3],
['آوریل', 4],
['april', 4],
['ژوئن', 6],
['june', 6],
['ژوئیه', 7],
['جولای', 7],
['july', 7],
['آگوست', 8],
['آگست', 8],
['اوت', 8],
['august', 8],
['سپتامبر', 9],
['september', 9],
['اکتبر', 10],
['october', 10],
['نوامبر', 11],
['november', 11],
['دسامبر', 12],
['december', 12],
['مه', 5],
['می', 5],
['may', 5],
];
function toAsciiDigits(value: string) {
return value
.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d)))
.replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d)));
}
function gregMonthNumber(name: string): number | null {
const key = name.trim().toLowerCase();
return GREG_MONTHS.find(([label]) => label === key)?.[1] ?? null;
}
function inferGregorianYear(month: number, day: number, now = new Date()): number {
const gy = now.getFullYear();
const gm = now.getMonth() + 1;
const gd = now.getDate();
if (month > gm || (month === gm && day > gd)) return gy - 1;
return gy;
}
/** «21 سپتامبر» و تاریخ میلادی عددی را در متن نمایشی به شمسی تبدیل می‌کند. */
export function rewriteGregorianDates(text: string, now = new Date()): string {
const monthAlt = GREG_MONTHS.map(([name]) => name).join('|');
const dayMonth = new RegExp(
`(^|[^\\p{L}\\d])([0-9۰-۹]{1,2})\\s*(${monthAlt})(?![\\p{L}\\u200c])(?:\\s+([0-9۰-۹]{4}))?`,
'giu',
);
const monthDay = new RegExp(
`(^|[^\\p{L}\\d])(${monthAlt})(?![\\p{L}\\u200c])\\s+([0-9۰-۹]{1,2})(?:\\s*,?\\s*([0-9۰-۹]{4}))?`,
'giu',
);
const iso = /\b((?:19|20)\d{2})-(\d{2})-(\d{2})\b/g;
const stamp = (year: number, month: number, day: number) => {
if (month < 1 || month > 12 || day < 1 || day > 31 || year < 1700 || year > 2500) return null;
const j = toJalali(year, month, day);
return `${toFaDigits(j.jy)}/${toFaDigits(String(j.jm).padStart(2, '0'))}/${toFaDigits(String(j.jd).padStart(2, '0'))}`;
};
const fromParts = (dayRaw: string, monthName: string, yearRaw?: string) => {
const day = Number(toAsciiDigits(dayRaw));
const month = gregMonthNumber(monthName);
if (!month) return null;
const year = yearRaw ? Number(toAsciiDigits(yearRaw)) : inferGregorianYear(month, day, now);
return stamp(year, month, day);
};
let out = text.replace(dayMonth, (full, lead: string, day: string, month: string, year?: string) => {
const next = fromParts(day, month, year);
return next ? `${lead}${next}` : full;
});
out = out.replace(monthDay, (full, lead: string, month: string, day: string, year?: string) => {
const next = fromParts(day, month, year);
return next ? `${lead}${next}` : full;
});
out = out.replace(iso, (full, year: string, month: string, day: string) => {
return stamp(Number(year), Number(month), Number(day)) ?? full;
});
return out;
}