Files
CRM_Montzar/apps/api/src/routines/recurrence.ts
T

102 lines
3.3 KiB
TypeScript

import type { RoutineRecurrence } from '@mento/shared';
import { ROUTINE_WEEKDAY_LABELS } from '@mento/shared';
import {
addDays,
dayDiff,
jalaliDayOfMonth,
persianWeekday,
toFaDigits,
weekStartKey,
} from './routine-dates';
export type RecurrenceRule = {
recurrence: RoutineRecurrence;
startDate: string;
endDate: string | null;
weekdays: number[];
intervalDays: number | null;
monthDay: number | null;
specificDates: string[];
};
function weeklyDays(rule: RecurrenceRule): number[] {
return rule.weekdays.length ? rule.weekdays : [persianWeekday(rule.startDate)];
}
export function occursOn(rule: RecurrenceRule, day: string): boolean {
if (day < rule.startDate) return false;
if (rule.endDate && day > rule.endDate) return false;
switch (rule.recurrence) {
case 'daily':
return true;
case 'weekdays':
case 'weekly':
return weeklyDays(rule).includes(persianWeekday(day));
case 'biweekly': {
if (!weeklyDays(rule).includes(persianWeekday(day))) return false;
const weeks = Math.floor(
dayDiff(weekStartKey(rule.startDate), weekStartKey(day)) / 7,
);
return weeks % 2 === 0;
}
case 'monthly': {
const target = rule.monthDay ?? jalaliDayOfMonth(rule.startDate);
const current = jalaliDayOfMonth(day);
if (current === target) return true;
// ماه‌هایی که روز هدف را ندارند (مثلاً ۳۱ در مهر): آخرین روز ماه
const isLastDay = jalaliDayOfMonth(addDays(day, 1)) === 1;
return isLastDay && target > current;
}
case 'every_n_days': {
const n = Math.max(1, rule.intervalDays ?? 1);
return dayDiff(rule.startDate, day) % n === 0;
}
case 'specific_dates':
return rule.specificDates.includes(day);
default:
return false;
}
}
function joinFa(items: string[]): string {
if (items.length <= 1) return items.join('');
return `${items.slice(0, -1).join('، ')} و ${items[items.length - 1]}`;
}
function describeWeekdays(days: number[]): string {
const sorted = [...new Set(days)].sort((a, b) => a - b);
if (sorted.length === 7) return 'هر روز';
const contiguous =
sorted.length >= 3 &&
sorted.every((d, i) => i === 0 || d === sorted[i - 1]! + 1);
if (contiguous) {
return `${ROUTINE_WEEKDAY_LABELS[sorted[0]!]} تا ${ROUTINE_WEEKDAY_LABELS[sorted[sorted.length - 1]!]}`;
}
return `هر ${joinFa(sorted.map((d) => ROUTINE_WEEKDAY_LABELS[d]!))}`;
}
export function describeRecurrence(rule: RecurrenceRule): string {
switch (rule.recurrence) {
case 'daily':
return 'هر روز';
case 'weekdays':
case 'weekly':
return describeWeekdays(weeklyDays(rule));
case 'biweekly':
return `هر دو هفته، ${joinFa(weeklyDays(rule).map((d) => ROUTINE_WEEKDAY_LABELS[d]!))}`;
case 'monthly': {
const day = rule.monthDay ?? jalaliDayOfMonth(rule.startDate);
return day === 1 ? 'اول هر ماه' : `روز ${toFaDigits(day)} هر ماه`;
}
case 'every_n_days': {
const n = Math.max(1, rule.intervalDays ?? 1);
return n === 1 ? 'هر روز' : `هر ${toFaDigits(n)} روز یک‌بار`;
}
case 'specific_dates':
return `${toFaDigits(rule.specificDates.length)} تاریخ مشخص`;
default:
return '';
}
}