Add French holidays to the timesheet, including Easter Monday

Implements logic to calculate and display French national holidays, such as Easter Monday, within the timesheet view.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 55837015-10e9-4be9-b857-7f5e6be73772
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 981f81d9-47f7-41fe-b3b4-19b5c1d2aa5d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1cc377db-7ea0-49f2-97ce-c3e87e0228cc/55837015-10e9-4be9-b857-7f5e6be73772/KSYTI3T
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
SylvainP1 2026-04-21 10:53:37 +00:00
parent 3f8e83b6ad
commit a36c4a444a

View File

@ -38,6 +38,51 @@ import { useAdminUnlocked } from "@/lib/admin-mode";
import { getDaysInMonth, isWeekend, format } from "date-fns"; import { getDaysInMonth, isWeekend, format } from "date-fns";
import { fr } from "date-fns/locale"; import { fr } from "date-fns/locale";
function addDays(date: Date, days: number) {
const next = new Date(date);
next.setDate(next.getDate() + days);
return next;
}
function getEasterSunday(year: number) {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return new Date(year, month - 1, day);
}
function getFrenchHolidayName(date: Date) {
const year = date.getFullYear();
const dateStr = format(date, "yyyy-MM-dd");
const easterSunday = getEasterSunday(year);
const holidays: Record<string, string> = {
[`${year}-01-01`]: "Jour de l'an",
[format(addDays(easterSunday, 1), "yyyy-MM-dd")]: "Lundi de Pâques",
[`${year}-05-01`]: "Fête du Travail",
[`${year}-05-08`]: "Victoire 1945",
[format(addDays(easterSunday, 39), "yyyy-MM-dd")]: "Ascension",
[format(addDays(easterSunday, 50), "yyyy-MM-dd")]: "Lundi de Pentecôte",
[`${year}-07-14`]: "Fête nationale",
[`${year}-08-15`]: "Assomption",
[`${year}-11-01`]: "Toussaint",
[`${year}-11-11`]: "Armistice 1918",
[`${year}-12-25`]: "Noël",
};
return holidays[dateStr] || null;
}
type LocalEntry = { type LocalEntry = {
timesheetLineId: number; timesheetLineId: number;
date: string; date: string;
@ -100,10 +145,13 @@ export default function TimesheetDetailPage() {
const daysCount = getDaysInMonth(new Date(timesheet.year, timesheet.month - 1)); const daysCount = getDaysInMonth(new Date(timesheet.year, timesheet.month - 1));
const arr = Array.from({ length: daysCount }, (_, i) => { const arr = Array.from({ length: daysCount }, (_, i) => {
const date = new Date(timesheet.year, timesheet.month - 1, i + 1); const date = new Date(timesheet.year, timesheet.month - 1, i + 1);
const holidayName = getFrenchHolidayName(date);
return { return {
dateStr: format(date, "yyyy-MM-dd"), dateStr: format(date, "yyyy-MM-dd"),
dayNum: i + 1, dayNum: i + 1,
isWeekendDay: isWeekend(date), isWeekendDay: isWeekend(date),
isHoliday: Boolean(holidayName),
holidayName,
dayName: format(date, "EE", { locale: fr }) dayName: format(date, "EE", { locale: fr })
}; };
}); });
@ -404,8 +452,9 @@ export default function TimesheetDetailPage() {
key={day.dateStr} key={day.dateStr}
className={cn( className={cn(
"px-0 py-1 font-medium text-center border-b border-r", "px-0 py-1 font-medium text-center border-b border-r",
day.isWeekendDay ? "bg-muted/50" : "" day.isWeekendDay || day.isHoliday ? "bg-muted/50" : ""
)} )}
title={day.holidayName || undefined}
> >
<div className="flex flex-col items-center leading-tight"> <div className="flex flex-col items-center leading-tight">
<span className="text-[9px] uppercase opacity-60">{day.dayName.slice(0, 2)}</span> <span className="text-[9px] uppercase opacity-60">{day.dayName.slice(0, 2)}</span>
@ -466,11 +515,11 @@ export default function TimesheetDetailPage() {
key={day.dateStr} key={day.dateStr}
className={cn( className={cn(
"p-0 text-center border-r transition-colors select-none", "p-0 text-center border-r transition-colors select-none",
day.isWeekendDay ? "bg-muted/30" : "", day.isWeekendDay || day.isHoliday ? "bg-muted/30" : "",
isEditable ? "cursor-pointer hover:bg-primary/10" : "", isEditable ? "cursor-pointer hover:bg-primary/10" : "",
val > 0 ? "bg-primary/5 text-primary font-semibold" : "" val > 0 ? "bg-primary/5 text-primary font-semibold" : ""
)} )}
title={desc || undefined} title={desc || day.holidayName || undefined}
> >
{isEditable ? ( {isEditable ? (
<Popover> <Popover>