Files
CRM_Montzar/apps/api/src/contacts/birthday-month.ts
T
MahdiandCursor 13ee70764b feat: show this month's birthdays and send a greeting SMS
Each owner sees their customers born this Jalali month and can text a custom greeting.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 19:55:11 +03:30

71 lines
2.1 KiB
TypeScript

import { jalaliMonthName, toJalali } from '../common/jalali';
export const BIRTHDAY_SMS_DEFAULT = '{نام} عزیز، تولدتان مبارک.';
export type BirthdayRow = {
id: string;
firstName: string;
lastName: string;
mobile: string | null;
birthDate: Date;
};
export type BirthdayPick = {
id: string;
firstName: string;
lastName: string;
mobile: string | null;
day: number;
};
export function fillBirthdaySmsText(template: string, firstName: string, lastName: string) {
const name = `${firstName} ${lastName}`.trim().replace(/\s+/g, ' ');
return template.replaceAll('{نام}', name).replaceAll('{name}', name).trim();
}
function tehranJalali(d: Date) {
const [gy, gm, gd] = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Tehran',
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
.format(d)
.split('-')
.map(Number);
return toJalali(gy, gm, gd);
}
function birthJalali(d: Date) {
return toJalali(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
}
/** متولدین ماه جاری تهران. پیش‌نمایش فقط روزهای امروز و بعد است. */
export function selectMonthBirthdays(people: BirthdayRow[], now = new Date(), previewLimit = 6) {
const today = tehranJalali(now);
const matched = people.flatMap((person) => {
const birth = birthJalali(person.birthDate);
if (birth.jm !== today.jm) return [];
return [
{
id: person.id,
firstName: person.firstName,
lastName: person.lastName,
mobile: person.mobile,
day: birth.jd,
},
];
});
const byDay = (a: BirthdayPick, b: BirthdayPick) => a.day - b.day || a.firstName.localeCompare(b.firstName, 'fa');
const upcoming = matched.filter((row) => row.day >= today.jd).sort(byDay);
const passed = matched.filter((row) => row.day < today.jd).sort(byDay);
return {
month: today.jm,
monthName: jalaliMonthName(today.jm),
today: today.jd,
total: matched.length,
preview: upcoming.slice(0, previewLimit),
items: [...upcoming, ...passed],
};
}