bb01268bcb
Full deadline tracking engine for Romanian construction permitting: - 16 deadline types across 5 categories (Avize, Completări, Analiză, Autorizare, Publicitate) - Working days vs calendar days with Romanian public holidays (Orthodox Easter via Meeus) - Backward deadlines (AC extension: 45 working days BEFORE expiry) - Chain deadlines (resolving one prompts adding the next) - Tacit approval auto-detection (overdue + applicable type) - Tabbed UI: Registru + Termene legale dashboard with stats/filters/table - Inline deadline cards in entry form with add/resolve/remove - Clock icon + count badge on registry table for entries with deadlines Also adds CLAUDE.md with full project context for AI assistant handoff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
/**
|
||
* Romanian working-day arithmetic.
|
||
*
|
||
* Fixed public holidays + Orthodox Easter-derived moveable feasts.
|
||
* Uses the Meeus algorithm for Orthodox Easter computation.
|
||
*/
|
||
|
||
// ── Fixed Romanian public holidays (month is 0-indexed) ──
|
||
|
||
interface FixedHoliday {
|
||
month: number;
|
||
day: number;
|
||
}
|
||
|
||
const FIXED_HOLIDAYS: FixedHoliday[] = [
|
||
{ month: 0, day: 1 }, // Anul Nou
|
||
{ month: 0, day: 2 }, // Anul Nou (zi 2)
|
||
{ month: 0, day: 24 }, // Ziua Unirii
|
||
{ month: 4, day: 1 }, // Ziua Muncii
|
||
{ month: 5, day: 1 }, // Ziua Copilului
|
||
{ month: 7, day: 15 }, // Adormirea Maicii Domnului
|
||
{ month: 10, day: 30 }, // Sfântul Andrei
|
||
{ month: 11, day: 1 }, // Ziua Națională
|
||
{ month: 11, day: 25 }, // Crăciunul
|
||
{ month: 11, day: 26 }, // Crăciunul (zi 2)
|
||
];
|
||
|
||
// ── Orthodox Easter via Meeus algorithm ──
|
||
|
||
function orthodoxEaster(year: number): Date {
|
||
const a = year % 4;
|
||
const b = year % 7;
|
||
const c = year % 19;
|
||
const d = (19 * c + 15) % 30;
|
||
const e = (2 * a + 4 * b - d + 34) % 7;
|
||
const month = Math.floor((d + e + 114) / 31); // 3 = March, 4 = April
|
||
const day = ((d + e + 114) % 31) + 1;
|
||
|
||
// Julian date — convert to Gregorian by adding 13 days (valid 1900–2099)
|
||
const julian = new Date(year, month - 1, day);
|
||
julian.setDate(julian.getDate() + 13);
|
||
return julian;
|
||
}
|
||
|
||
function getMovableHolidays(year: number): Date[] {
|
||
const easter = orthodoxEaster(year);
|
||
|
||
const goodFriday = new Date(easter);
|
||
goodFriday.setDate(easter.getDate() - 2);
|
||
|
||
const easterMonday = new Date(easter);
|
||
easterMonday.setDate(easter.getDate() + 1);
|
||
|
||
const rusaliiSunday = new Date(easter);
|
||
rusaliiSunday.setDate(easter.getDate() + 49);
|
||
|
||
const rusaliiMonday = new Date(easter);
|
||
rusaliiMonday.setDate(easter.getDate() + 50);
|
||
|
||
return [goodFriday, easter, easterMonday, rusaliiSunday, rusaliiMonday];
|
||
}
|
||
|
||
// ── Holiday cache per year ──
|
||
|
||
const holidayCache = new Map<number, Set<string>>();
|
||
|
||
function toKey(d: Date): string {
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
}
|
||
|
||
function getHolidaySet(year: number): Set<string> {
|
||
const cached = holidayCache.get(year);
|
||
if (cached) return cached;
|
||
|
||
const set = new Set<string>();
|
||
|
||
for (const h of FIXED_HOLIDAYS) {
|
||
set.add(toKey(new Date(year, h.month, h.day)));
|
||
}
|
||
|
||
for (const d of getMovableHolidays(year)) {
|
||
set.add(toKey(d));
|
||
}
|
||
|
||
holidayCache.set(year, set);
|
||
return set;
|
||
}
|
||
|
||
// ── Public API ──
|
||
|
||
export function isPublicHoliday(date: Date): boolean {
|
||
return getHolidaySet(date.getFullYear()).has(toKey(date));
|
||
}
|
||
|
||
export function isWeekend(date: Date): boolean {
|
||
const day = date.getDay();
|
||
return day === 0 || day === 6;
|
||
}
|
||
|
||
export function isWorkingDay(date: Date): boolean {
|
||
return !isWeekend(date) && !isPublicHoliday(date);
|
||
}
|
||
|
||
/**
|
||
* Add calendar days (simply skips no days).
|
||
* Supports negative values.
|
||
*/
|
||
export function addCalendarDays(start: Date, days: number): Date {
|
||
const result = new Date(start);
|
||
result.setDate(result.getDate() + days);
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Add working days, skipping weekends and Romanian public holidays.
|
||
* Supports negative values (for backward deadlines).
|
||
*/
|
||
export function addWorkingDays(start: Date, days: number): Date {
|
||
const result = new Date(start);
|
||
const direction = days >= 0 ? 1 : -1;
|
||
let remaining = Math.abs(days);
|
||
|
||
while (remaining > 0) {
|
||
result.setDate(result.getDate() + direction);
|
||
if (isWorkingDay(result)) {
|
||
remaining--;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Compute the due date for a deadline definition.
|
||
*/
|
||
export function computeDueDate(
|
||
startDate: Date,
|
||
days: number,
|
||
dayType: 'calendar' | 'working',
|
||
isBackward?: boolean,
|
||
): Date {
|
||
const effectiveDays = isBackward ? -days : days;
|
||
return dayType === 'working'
|
||
? addWorkingDays(startDate, effectiveDays)
|
||
: addCalendarDays(startDate, effectiveDays);
|
||
}
|