Files
ArchiTools/src/modules/registratura/services/working-days.ts
T
Marius Tarau bb01268bcb feat(registratura): add legal deadline tracking system (Termene Legale)
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>
2026-02-18 11:27:34 +02:00

147 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 19002099)
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);
}