fix: Afficher toutes les dates intermédiaires lors de la saisie par date dans les contrats CDDU

This commit is contained in:
odentas 2025-11-20 17:48:53 +01:00
parent b2b3cf9ed3
commit 3435581761

View file

@ -12,7 +12,7 @@ import { useDemoMode } from "@/hooks/useDemoMode";
import Calculator from "@/components/Calculator";
import DatePickerCalendar from "@/components/DatePickerCalendar";
import DatesQuantityModal from "@/components/DatesQuantityModal";
import { parseDateString, parseFrenchedDate } from "@/lib/dateFormatter";
import { parseDateString, parseFrenchedDate, generateDateRange, formatDateFr } from "@/lib/dateFormatter";
import { Tooltip } from "@/components/ui/tooltip";
/* =========================
@ -105,6 +105,70 @@ function norm(s: string){
.trim();
}
/**
* Parse une chaîne formatée (ex: "1 représentation par jour du 24/11 au 26/11 ; 2 représentations le 28/11.")
* et extrait toutes les dates individuelles avec leurs quantités.
*
* @param dateStr - Chaîne formatée avec dates et quantités
* @param yearContext - Contexte de l'année (ex: "2025-11-01")
* @returns Array de {date: "DD/MM", quantity: number, key: string}
*/
function parseFormattedDatesWithQuantities(
dateStr: string,
yearContext: string,
prefix: "rep" | "serv" | "jour"
): Array<{ date: string; quantity: number; index: number; key: string }> {
if (!dateStr || !dateStr.trim()) return [];
const result: Array<{ date: string; quantity: number; index: number; key: string }> = [];
const groups = dateStr.split(" ; ");
groups.forEach((group, groupIdx) => {
// Extraire la quantité (ex: "1 représentation", "2 services")
const qtyMatch = group.match(/^(\d+)\s+/);
const quantity = qtyMatch ? parseInt(qtyMatch[1]) : 1;
// Vérifier si c'est une plage (contient "du ... au ...")
const rangeMatch = group.match(/du\s+(\d{2}\/\d{2})\s+au\s+(\d{2}\/\d{2})/);
if (rangeMatch) {
// C'est une plage : générer toutes les dates intermédiaires
const startFr = rangeMatch[1];
const endFr = rangeMatch[2];
// Convertir en ISO
const startIso = parseFrenchedDate(startFr, yearContext);
const endIso = parseFrenchedDate(endFr, yearContext);
// Générer toutes les dates de la plage
const allDatesIso = generateDateRange(startIso, endIso);
// Ajouter chaque date avec sa quantité
allDatesIso.forEach((iso, dateIdx) => {
const dateFr = formatDateFr(iso);
result.push({
date: dateFr,
quantity: quantity,
index: dateIdx,
key: `${prefix}_${groupIdx}_${dateIdx}`
});
});
} else {
// Date isolée (ex: "le 28/11")
const dateMatch = group.match(/(\d{2}\/\d{2})/);
if (dateMatch) {
result.push({
date: dateMatch[1],
quantity: quantity,
index: 0,
key: `${prefix}_${groupIdx}_0`
});
}
}
});
return result;
}
// Petit hook local de debounce (réutilisable)
function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState<T>(value);
@ -1366,87 +1430,73 @@ useEffect(() => {
const representations: any[] = [];
const repetitions: any[] = [];
const jours_travail: any[] = [];
const yearContext = dateDebut || new Date().toISOString().slice(0, 10);
// Parser les représentations
if (datesRep && datesRep.length > 0) {
const groups = datesRep.split(" ; ");
groups.forEach((dateStr, groupIdx) => {
const qtyMatch = dateStr.match(/(\d+)\s+représentation/);
const dateMatches = dateStr.match(/(\d{2}\/\d{2})/g) || [];
const qty = qtyMatch ? parseInt(qtyMatch[1]) : 1;
dateMatches.forEach((dateDisplay, dateIdx) => {
const items: any[] = [];
for (let i = 0; i < qty; i++) {
const key = `rep_${groupIdx}_${dateIdx}_${i}`;
const montant = salariesByDate[key];
if (montant && typeof montant === "number") {
items.push({
numero: i + 1,
montant: montant,
});
}
}
if (items.length > 0) {
representations.push({
date: dateDisplay,
items: items,
const allRepDates = parseFormattedDatesWithQuantities(datesRep, yearContext, "rep");
allRepDates.forEach((dateInfo) => {
const items: any[] = [];
for (let i = 0; i < dateInfo.quantity; i++) {
const key = `${dateInfo.key}_${i}`;
const montant = salariesByDate[key];
if (montant && typeof montant === "number") {
items.push({
numero: i + 1,
montant: montant,
});
}
});
}
if (items.length > 0) {
representations.push({
date: dateInfo.date,
items: items,
});
}
});
}
// Parser les répétitions
if (datesServ && datesServ.length > 0) {
const groups = datesServ.split(" ; ");
groups.forEach((dateStr, groupIdx) => {
const qtyMatch = dateStr.match(/(\d+)\s+service/);
const dateMatches = dateStr.match(/(\d{2}\/\d{2})/g) || [];
const qty = qtyMatch ? parseInt(qtyMatch[1]) : 1;
dateMatches.forEach((dateDisplay, dateIdx) => {
const items: any[] = [];
for (let i = 0; i < qty; i++) {
const key = `serv_${groupIdx}_${dateIdx}_${i}`;
const montant = salariesByDate[key];
if (montant && typeof montant === "number") {
items.push({
numero: i + 1,
montant: montant,
duree_heures: parseInt(durationServices) || 4,
});
}
}
if (items.length > 0) {
repetitions.push({
date: dateDisplay,
items: items,
const allServDates = parseFormattedDatesWithQuantities(datesServ, yearContext, "serv");
allServDates.forEach((dateInfo) => {
const items: any[] = [];
for (let i = 0; i < dateInfo.quantity; i++) {
const key = `${dateInfo.key}_${i}`;
const montant = salariesByDate[key];
if (montant && typeof montant === "number") {
items.push({
numero: i + 1,
montant: montant,
duree_heures: parseInt(durationServices) || 4,
});
}
});
}
if (items.length > 0) {
repetitions.push({
date: dateInfo.date,
items: items,
});
}
});
}
// Parser les jours travaillés
if (joursTravail && joursTravail.length > 0) {
const groups = joursTravail.split(" ; ");
groups.forEach((dateStr, groupIdx) => {
const dateMatches = dateStr.match(/(\d{2}\/\d{2})/g) || [];
if (dateMatches.length > 0) {
const key = `jour_${groupIdx}_0`;
const montant = salariesByDate[key];
if (montant && typeof montant === "number") {
// Extraire les heures si présentes dans la string
const heuresMatch = dateStr.match(/(\d+)\s+heures?/);
const heures = heuresMatch ? parseInt(heuresMatch[1]) : 7;
jours_travail.push({
date: dateMatches[0],
montant: montant,
heures: heures,
});
}
const allJoursDates = parseFormattedDatesWithQuantities(joursTravail, yearContext, "jour");
allJoursDates.forEach((dateInfo) => {
const key = `${dateInfo.key}_0`;
const montant = salariesByDate[key];
if (montant && typeof montant === "number") {
jours_travail.push({
date: dateInfo.date,
montant: montant,
heures: dateInfo.quantity, // La quantité représente les heures pour les jours travaillés
});
}
});
}
@ -2760,33 +2810,32 @@ useEffect(() => {
{/* Tableau des dates avec salaires - version compacte */}
<div className="space-y-3 mb-4">
{/* Représentations */}
{datesRep && datesRep.length > 0 && (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="bg-indigo-50 px-3 py-2 border-b border-indigo-100">
<span className="text-xs font-semibold text-indigo-900 uppercase tracking-wide">Représentations</span>
</div>
<div className="p-2 space-y-1.5">
{datesRep.split(" ; ").map((dateStr, groupIdx) => {
const qtyMatch = dateStr.match(/(\d+)\s+représentation/);
const dateMatches = dateStr.match(/(\d{2}\/\d{2})/g) || [];
const qty = qtyMatch ? parseInt(qtyMatch[1]) : 1;
return dateMatches.map((dateDisplay, dateIdx) => (
<div key={`rep_group_${groupIdx}_${dateIdx}`} className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-slate-50">
<div className="text-xs font-medium text-slate-700 w-14 shrink-0">{dateDisplay}</div>
{datesRep && datesRep.length > 0 && (() => {
const yearContext = dateDebut || new Date().toISOString().slice(0, 10);
const allRepDates = parseFormattedDatesWithQuantities(datesRep, yearContext, "rep");
return (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="bg-indigo-50 px-3 py-2 border-b border-indigo-100">
<span className="text-xs font-semibold text-indigo-900 uppercase tracking-wide">Représentations</span>
</div>
<div className="p-2 space-y-1.5">
{allRepDates.map((dateInfo) => (
<div key={dateInfo.key} className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-slate-50">
<div className="text-xs font-medium text-slate-700 w-14 shrink-0">{dateInfo.date}</div>
<div className="flex items-center gap-1.5 flex-wrap">
{Array.from({ length: qty }).map((_, i) => (
<div key={`rep_${groupIdx}_${dateIdx}_${i}`} className="flex items-center gap-1">
{Array.from({ length: dateInfo.quantity }).map((_, i) => (
<div key={`${dateInfo.key}_${i}`} className="flex items-center gap-1">
<span className="text-xs text-slate-500">R{i + 1}</span>
<input
type="number"
min="0.01"
step="0.01"
value={salariesByDate[`rep_${groupIdx}_${dateIdx}_${i}`] ?? ""}
value={salariesByDate[`${dateInfo.key}_${i}`] ?? ""}
onChange={(e) =>
setSalariesByDate({
...salariesByDate,
[`rep_${groupIdx}_${dateIdx}_${i}`]: e.target.value === "" ? "" : Number(e.target.value),
[`${dateInfo.key}_${i}`]: e.target.value === "" ? "" : Number(e.target.value),
})
}
placeholder="0.00"
@ -2798,40 +2847,39 @@ useEffect(() => {
))}
</div>
</div>
));
})}
))}
</div>
</div>
</div>
)}
);
})()}
{/* Répétitions */}
{datesServ && datesServ.length > 0 && (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="bg-purple-50 px-3 py-2 border-b border-purple-100">
<span className="text-xs font-semibold text-purple-900 uppercase tracking-wide">Répétitions</span>
</div>
<div className="p-2 space-y-1.5">
{datesServ.split(" ; ").map((dateStr, groupIdx) => {
const qtyMatch = dateStr.match(/(\d+)\s+service/);
const dateMatches = dateStr.match(/(\d{2}\/\d{2})/g) || [];
const qty = qtyMatch ? parseInt(qtyMatch[1]) : 1;
return dateMatches.map((dateDisplay, dateIdx) => (
<div key={`serv_group_${groupIdx}_${dateIdx}`} className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-slate-50">
<div className="text-xs font-medium text-slate-700 w-14 shrink-0">{dateDisplay}</div>
{datesServ && datesServ.length > 0 && (() => {
const yearContext = dateDebut || new Date().toISOString().slice(0, 10);
const allServDates = parseFormattedDatesWithQuantities(datesServ, yearContext, "serv");
return (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="bg-purple-50 px-3 py-2 border-b border-purple-100">
<span className="text-xs font-semibold text-purple-900 uppercase tracking-wide">Répétitions</span>
</div>
<div className="p-2 space-y-1.5">
{allServDates.map((dateInfo) => (
<div key={dateInfo.key} className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-slate-50">
<div className="text-xs font-medium text-slate-700 w-14 shrink-0">{dateInfo.date}</div>
<div className="flex items-center gap-1.5 flex-wrap">
{Array.from({ length: qty }).map((_, i) => (
<div key={`serv_${groupIdx}_${dateIdx}_${i}`} className="flex items-center gap-1">
{Array.from({ length: dateInfo.quantity }).map((_, i) => (
<div key={`${dateInfo.key}_${i}`} className="flex items-center gap-1">
<span className="text-xs text-slate-500">S{i + 1}</span>
<input
type="number"
min="0.01"
step="0.01"
value={salariesByDate[`serv_${groupIdx}_${dateIdx}_${i}`] ?? ""}
value={salariesByDate[`${dateInfo.key}_${i}`] ?? ""}
onChange={(e) =>
setSalariesByDate({
...salariesByDate,
[`serv_${groupIdx}_${dateIdx}_${i}`]: e.target.value === "" ? "" : Number(e.target.value),
[`${dateInfo.key}_${i}`]: e.target.value === "" ? "" : Number(e.target.value),
})
}
placeholder="0.00"
@ -2843,25 +2891,26 @@ useEffect(() => {
))}
</div>
</div>
));
})}
))}
</div>
</div>
</div>
)}
);
})()}
{/* Jours travaillés */}
{joursTravail && joursTravail.length > 0 && (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="bg-green-50 px-3 py-2 border-b border-green-100">
<span className="text-xs font-semibold text-green-900 uppercase tracking-wide">Jours travaillés</span>
</div>
<div className="p-2 space-y-1.5">
{joursTravail.split(" ; ").map((dateStr, groupIdx) => {
const dateMatches = dateStr.match(/(\d{2}\/\d{2})/g) || [];
return (
<div key={`jour_group_${groupIdx}`} className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-slate-50">
<div className="text-xs font-medium text-slate-700 w-14 shrink-0">{dateMatches[0]}</div>
{joursTravail && joursTravail.length > 0 && (() => {
const yearContext = dateDebut || new Date().toISOString().slice(0, 10);
const allJoursDates = parseFormattedDatesWithQuantities(joursTravail, yearContext, "jour");
return (
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="bg-green-50 px-3 py-2 border-b border-green-100">
<span className="text-xs font-semibold text-green-900 uppercase tracking-wide">Jours travaillés</span>
</div>
<div className="p-2 space-y-1.5">
{allJoursDates.map((dateInfo) => (
<div key={dateInfo.key} className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-slate-50">
<div className="text-xs font-medium text-slate-700 w-14 shrink-0">{dateInfo.date}</div>
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1">
<span className="text-xs text-slate-500">Jour</span>
@ -2869,11 +2918,11 @@ useEffect(() => {
type="number"
min="0.01"
step="0.01"
value={salariesByDate[`jour_${groupIdx}_0`] ?? ""}
value={salariesByDate[`${dateInfo.key}_0`] ?? ""}
onChange={(e) =>
setSalariesByDate({
...salariesByDate,
[`jour_${groupIdx}_0`]: e.target.value === "" ? "" : Number(e.target.value),
[`${dateInfo.key}_0`]: e.target.value === "" ? "" : Number(e.target.value),
})
}
placeholder="0.00"
@ -2883,11 +2932,11 @@ useEffect(() => {
</div>
</div>
</div>
);
})}
))}
</div>
</div>
</div>
)}
);
})()}
</div>
{/* Total du salaire */}