espace-paie-odentas/components/DatePickerCalendar.tsx
odentas c2a54ecd89 fix: Utiliser l'année réelle sélectionnée dans le calendrier au lieu de supposer
Problème: Le système supposait l'année basée sur le contexte, ce qui causait des erreurs.

Solution:
- Ajout de formatDateFrWithYear() pour retourner DD/MM/YYYY
- Le calendrier DatePickerCalendar retourne maintenant des dates avec l'année complète
- parseFrenchedDate() supporte maintenant DD/MM/YYYY et DD/MM
- Si l'année est dans la date (DD/MM/YYYY), elle est utilisée directement
- Plus besoin de supposer l'année basée sur oct/nov/déc -> jan/fev/mar
- Les dates de début et fin se calculent correctement à partir des vraies dates ISO

Cela garantit que:
1. Les dates en janvier 2026 restent bien en 2026
2. Les champs date début/date fin se remplissent avec les bonnes années
3. Aucune supposition erronée n'est faite
2025-12-19 17:58:33 +01:00

361 lines
12 KiB
TypeScript

"use client";
import React, { useState, useEffect, useRef } from "react";
import { X, ChevronLeft, ChevronRight } from "lucide-react";
import {
parseDateString,
formatGroupsToInput,
formatGroupsToPDFMonkey,
hasMultipleMoons,
convertIsoDatesToGroups,
formatDateFr,
formatDateFrWithYear,
parseFrenchedDate,
} from "@/lib/dateFormatter";
interface DatePickerCalendarProps {
isOpen: boolean;
onClose: () => void;
onApply: (result: {
selectedDates: string[]; // format input "12/10, 13/10, ..."
hasMultiMonth: boolean;
pdfFormatted: string; // "le 12/10 ; du 14/10 au 17/10."
}) => void;
initialDates?: string[]; // format input "12/10, 13/10, ..."
title: string;
minDate?: string; // "2025-10-01"
maxDate?: string; // "2025-10-31"
}
export default function DatePickerCalendar({
isOpen,
onClose,
onApply,
initialDates = [],
title,
minDate,
maxDate,
}: DatePickerCalendarProps) {
const [selectedIsos, setSelectedIsos] = useState<string[]>([]);
const [currentMonth, setCurrentMonth] = useState(new Date());
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const modalRef = useRef<HTMLDivElement>(null);
const prevIsOpenRef = useRef(isOpen);
// Initialiser avec les dates initiales
useEffect(() => {
// Vérifier si la modale vient de s'ouvrir (transition de false à true)
const justOpened = isOpen && !prevIsOpenRef.current;
prevIsOpenRef.current = isOpen;
if (justOpened) {
if (initialDates.length > 0) {
// Convertir format input "12/10" en ISO
// On utilise le minDate ou maxDate comme contexte d'année
const yearContext = minDate || maxDate || new Date().toISOString().slice(0, 10);
const isos = initialDates
.map(d => parseFrenchedDate(d.trim(), yearContext))
.filter(iso => iso.length === 10);
setSelectedIsos(isos);
// Mettre le calendrier sur le premier mois
if (isos.length > 0) {
const firstDate = new Date(isos[0] + "T00:00:00Z");
setCurrentMonth(firstDate);
}
} else {
// Réinitialiser les sélections si pas de dates initiales
setSelectedIsos([]);
if (minDate) {
// Si pas de dates initiales, utiliser la date de début du contrat
const startDate = new Date(minDate + "T00:00:00Z");
setCurrentMonth(startDate);
}
}
}
}, [isOpen, initialDates, minDate, maxDate]);
// Initialiser la position au centre
useEffect(() => {
if (isOpen && position === null) {
const centerX = (typeof window !== "undefined" ? window.innerWidth : 1024) - 450;
const centerY = Math.max(20, ((typeof window !== "undefined" ? window.innerHeight : 768) - 500) * 0.4);
setPosition({ x: Math.max(20, centerX), y: centerY });
}
}, [isOpen, position]);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
if (!position || !modalRef.current) return;
setIsDragging(true);
setDragOffset({
x: e.clientX - position.x,
y: e.clientY - position.y,
});
};
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging || !position) return;
setPosition({
x: e.clientX - dragOffset.x,
y: e.clientY - dragOffset.y,
});
};
const handleMouseUp = () => {
setIsDragging(false);
};
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, dragOffset, position]);
const handleDateClick = (day: number) => {
// Créer la date en UTC pour éviter les décalages de fuseau horaire
const isoStr = new Date(Date.UTC(currentMonth.getFullYear(), currentMonth.getMonth(), day))
.toISOString()
.slice(0, 10);
if (isDateOutOfRange(isoStr)) return;
setSelectedIsos(prev => {
if (prev.includes(isoStr)) {
return prev.filter(d => d !== isoStr);
} else {
return [...prev, isoStr].sort();
}
});
};
const isDateOutOfRange = (isoStr: string): boolean => {
if (minDate && isoStr < minDate) return true;
if (maxDate && isoStr > maxDate) return true;
return false;
};
const isDateSelected = (day: number): boolean => {
// Créer la date en UTC
const isoStr = new Date(Date.UTC(currentMonth.getFullYear(), currentMonth.getMonth(), day))
.toISOString()
.slice(0, 10);
return selectedIsos.includes(isoStr);
};
const isDateDisabled = (day: number): boolean => {
// Créer la date en UTC
const isoStr = new Date(Date.UTC(currentMonth.getFullYear(), currentMonth.getMonth(), day))
.toISOString()
.slice(0, 10);
return isDateOutOfRange(isoStr);
};
const getDaysInMonth = (date: Date) => {
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
};
const getFirstDayOfMonth = (date: Date) => {
return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
};
const renderCalendar = () => {
const daysInMonth = getDaysInMonth(currentMonth);
const firstDay = getFirstDayOfMonth(currentMonth);
const days: (number | null)[] = Array(firstDay === 0 ? 6 : firstDay - 1).fill(null);
for (let i = 1; i <= daysInMonth; i++) {
days.push(i);
}
return days;
};
const handleApply = () => {
if (selectedIsos.length === 0) {
onApply({
selectedDates: [],
hasMultiMonth: false,
pdfFormatted: "",
});
onClose();
return;
}
// Convertir en format input avec année (DD/MM/YYYY)
const inputFormat = selectedIsos.map(iso => formatDateFrWithYear(iso)).join(", ");
// Détecter multi-mois
const isMultiMonth = hasMultipleMoons(selectedIsos);
// Générer les groupes et formatter pour PDFMonkey
const groups = convertIsoDatesToGroups(selectedIsos);
const pdfFormatted = groups.map(g => g.displayFr).join(" ; ") + ".";
onApply({
selectedDates: inputFormat.split(", "),
hasMultiMonth: isMultiMonth,
pdfFormatted,
});
onClose();
};
const handleReset = () => {
setSelectedIsos([]);
};
if (!isOpen) return null;
const monthName = currentMonth.toLocaleDateString("fr-FR", { month: "long", year: "numeric" });
const daysArray = renderCalendar();
const dayNames = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];
return (
<>
{/* Overlay semi-transparent */}
<div
className="fixed inset-0 bg-black/50 z-[100]"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
/>
{/* Modale draggable */}
<div
ref={modalRef}
className="fixed z-[101] w-[420px] bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
style={{
left: position?.x || 0,
top: position?.y || 0,
cursor: isDragging ? "grabbing" : "grab",
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header draggable */}
<div
onMouseDown={handleMouseDown}
className="flex items-center justify-between p-4 bg-gradient-to-r from-indigo-50 to-purple-50 border-b cursor-grab active:cursor-grabbing"
>
<h2 className="text-lg font-semibold text-slate-900">{title}</h2>
<button
onClick={onClose}
className="p-1 hover:bg-white/50 rounded-lg transition"
>
<X className="w-5 h-5 text-slate-500" />
</button>
</div>
{/* Contenu */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Navigation mois */}
<div className="flex items-center justify-between">
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))}
className="p-2 hover:bg-slate-100 rounded-lg transition"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-semibold text-slate-700 capitalize">{monthName}</span>
<button
onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))}
className="p-2 hover:bg-slate-100 rounded-lg transition"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
{/* Calendrier */}
<div className="grid grid-cols-7 gap-1">
{/* Headers des jours */}
{dayNames.map(day => (
<div key={day} className="text-center text-xs font-semibold text-slate-500 py-2">
{day}
</div>
))}
{/* Jours */}
{daysArray.map((day, idx) => (
<div key={idx}>
{day === null ? (
<div />
) : (
<button
onClick={() => handleDateClick(day)}
disabled={isDateDisabled(day)}
className={`
w-full aspect-square rounded-lg text-sm font-medium transition
${isDateDisabled(day)
? "bg-slate-100 text-slate-300 cursor-not-allowed"
: isDateSelected(day)
? "bg-indigo-600 text-white hover:bg-indigo-700"
: "bg-white text-slate-700 hover:bg-indigo-50 border border-slate-200"
}
`}
>
{day}
</button>
)}
</div>
))}
</div>
{/* Sélection actuelle */}
{selectedIsos.length > 0 && (
<div className="bg-slate-50 rounded-lg p-3 border border-slate-200">
<p className="text-xs font-semibold text-slate-600 mb-2">Dates sélectionnées :</p>
<div className="text-sm text-slate-700 leading-relaxed">
{selectedIsos.map(iso => formatDateFr(iso)).join(", ")}
</div>
{hasMultipleMoons(selectedIsos) && (
<p className="text-xs text-amber-700 mt-2 bg-amber-50 p-2 rounded border border-amber-200">
Attention : multi-mois détecté (le champ sera auto-set à "Oui")
</p>
)}
</div>
)}
</div>
{/* Footer avec boutons */}
<div className="border-t p-4 bg-slate-50 flex items-center gap-2">
{selectedIsos.length > 0 && (
<button
onClick={handleReset}
className="px-3 py-2 text-sm rounded-lg border border-slate-300 text-slate-700 hover:bg-slate-100 transition"
>
Réinitialiser
</button>
)}
<button
onClick={onClose}
className="px-3 py-2 text-sm rounded-lg border border-slate-300 text-slate-700 hover:bg-slate-100 transition ml-auto"
>
Annuler
</button>
<button
onClick={handleApply}
disabled={selectedIsos.length === 0}
className={`
px-4 py-2 text-sm font-semibold rounded-lg transition
${selectedIsos.length === 0
? "bg-slate-200 text-slate-400 cursor-not-allowed"
: "bg-indigo-600 text-white hover:bg-indigo-700"
}
`}
>
Appliquer ({selectedIsos.length})
</button>
</div>
</div>
</>
);
}