fix: Afficher toutes les dates intermédiaires lors de la saisie par date dans les contrats CDDU
This commit is contained in:
parent
b2b3cf9ed3
commit
3435581761
1 changed files with 176 additions and 127 deletions
|
|
@ -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 */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue