From a36c4a444aacf86ace48c00b2f839693c3d017d7 Mon Sep 17 00:00:00 2001 From: SylvainP1 <5533467-SylvainP1@users.noreply.replit.com> Date: Tue, 21 Apr 2026 10:53:37 +0000 Subject: [PATCH] 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 --- .../cra-app/src/pages/timesheet-detail.tsx | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/artifacts/cra-app/src/pages/timesheet-detail.tsx b/artifacts/cra-app/src/pages/timesheet-detail.tsx index 71bc78c..2112a51 100644 --- a/artifacts/cra-app/src/pages/timesheet-detail.tsx +++ b/artifacts/cra-app/src/pages/timesheet-detail.tsx @@ -38,6 +38,51 @@ import { useAdminUnlocked } from "@/lib/admin-mode"; import { getDaysInMonth, isWeekend, format } from "date-fns"; 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 = { + [`${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 = { timesheetLineId: number; date: string; @@ -100,10 +145,13 @@ export default function TimesheetDetailPage() { const daysCount = getDaysInMonth(new Date(timesheet.year, timesheet.month - 1)); const arr = Array.from({ length: daysCount }, (_, i) => { const date = new Date(timesheet.year, timesheet.month - 1, i + 1); + const holidayName = getFrenchHolidayName(date); return { dateStr: format(date, "yyyy-MM-dd"), dayNum: i + 1, isWeekendDay: isWeekend(date), + isHoliday: Boolean(holidayName), + holidayName, dayName: format(date, "EE", { locale: fr }) }; }); @@ -404,8 +452,9 @@ export default function TimesheetDetailPage() { key={day.dateStr} className={cn( "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} >
{day.dayName.slice(0, 2)} @@ -466,11 +515,11 @@ export default function TimesheetDetailPage() { key={day.dateStr} className={cn( "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" : "", val > 0 ? "bg-primary/5 text-primary font-semibold" : "" )} - title={desc || undefined} + title={desc || day.holidayName || undefined} > {isEditable ? (