diff --git a/family-calendar.php b/family-calendar.php index 831bf24..f978d07 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -202,20 +202,19 @@ require __DIR__ . '/header.php'; - Av. - Use - Av. - Use - Av. - Use - - - Av. - Use - Av. - Use - Av. - Use + Av. + Use + Av. + Use + Av. + Use + + Av. + Use + Av. + Use + Av. + Use diff --git a/modules/family-calendar/family-calendar.css b/modules/family-calendar/family-calendar.css index d6cf2d9..c54ddc7 100644 --- a/modules/family-calendar/family-calendar.css +++ b/modules/family-calendar/family-calendar.css @@ -38,6 +38,22 @@ text-overflow: ellipsis; } +/* Colonnes "Av." non modifiables : fond gris clair + texte un peu atténué */ +#planningHeaderTable th.col-alex-av, +#planningTable td.col-alex-av, +#planningHeaderTable th.col-laia-av, +#planningTable td.col-laia-av { + background-color: #e5e7eb; + color: #4b5563; + font-weight: 600; +} + +/* Optionnel : curseur "not-allowed" pour marquer le côté non éditable */ +#planningTable td.col-alex-av, +#planningTable td.col-laia-av { + cursor: not-allowed; +} + /* Mois / Semaine */ #planningHeaderTable .col-month, #planningTable .col-month { @@ -175,48 +191,125 @@ cursor: pointer; } -/* Le menu contextuel flottant */ +/* Conteneur du menu de sélection (hebdo & mensuel) */ .fc-selection-menu { display: none; position: absolute; z-index: 100; - background-color: white; - border: 1px solid #ccc; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - border-radius: 6px; - padding: 8px; + background-color: #ffffff; + border: 1px solid #d1d5db; /* gris clair */ + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.18); + border-radius: 8px; + padding: 8px 10px; font-size: 12px; + min-width: 220px; + max-width: 360px; } -.fc-selection-menu .fc-menu-section { +/* Sections internes avec titre */ +.fc-menu-section { margin-bottom: 8px; } -.fc-selection-menu .fc-menu-section:last-child { - margin-bottom: 0; -} - -.fc-selection-menu strong { +.fc-menu-section > strong { display: block; margin-bottom: 4px; font-weight: 600; + color: #111827; /* gris très foncé */ } -.fc-selection-menu button { +/* Grille pour disposer proprement les boutons dans une section */ +.fc-menu-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; +} + +/* Boutons génériques du menu */ +.fc-selection-menu button.fc-menu-btn, +.fc-selection-menu button.fc-menu-leave-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + border-radius: 4px; + border: 1px solid #e5e7eb; + padding: 4px 6px; + font-size: 12px; + background: #f9fafb; + color: #111827; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.fc-selection-menu button.fc-menu-btn:hover, +.fc-selection-menu button.fc-menu-leave-btn:hover { + background-color: #e5f0ff; /* léger bleu */ + border-color: #93c5fd; + box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.3); +} + +/* Variantes par type pour un léger hint visuel +.fc-menu-btn--conge { + congés Carole +} + +.fc-menu-btn--garde { + mode de garde +} + +.fc-menu-btn--pep { + Pep malade +} */ + +/* Boutons "danger" (suppression) */ +.fc-selection-menu button.fc-menu-danger { display: block; width: 100%; - background: none; - border: 1px solid #ddd; + margin-top: 6px; + padding: 4px 6px; border-radius: 4px; - padding: 4px 8px; - margin-top: 4px; - text-align: left; + border: 1px solid #fecaca; + background-color: #fef2f2; + color: #b91c1c; + font-size: 12px; cursor: pointer; + text-align: left; } -.fc-selection-menu button:hover { - background-color: #f0f4f8; - border-color: #b0c4de; +.fc-selection-menu button.fc-menu-danger:hover { + background-color: #fee2e2; + border-color: #fca5a5; +} + +/* Tableau des congés Alex / Laia dans le menu */ +.fc-menu-leaves-table { + margin-top: 4px; +} + +.fc-menu-leaves-table table { + width: 100%; + border-collapse: collapse; + font-size: 11px; +} + +.fc-menu-leaves-table th, +.fc-menu-leaves-table td { + padding: 2px; + text-align: center; +} + +.fc-menu-leaves-table thead th { + font-weight: 600; + color: #374151; + border-bottom: 1px solid #e5e7eb; +} + +.fc-menu-leaves-table tbody td:first-child { + text-align: left; + font-weight: 500; + color: #4b5563; } /* --- Couleurs de base pour la légende et les jours --- */ @@ -422,6 +515,7 @@ .fc-calendar-and-summary { display: block; + position: relative; } .fc-month-header { @@ -678,6 +772,15 @@ line-height: 1; } +/* Bordure épaisse pour délimiter les mois dans le planning hebdo */ +#planningTable tr.fc-month-first-week-row td { + border-top: 2px solid #243b53; /* couleur sombre cohérente avec ta charte */ +} + +#planningTable tr.fc-month-last-week-row td { + border-bottom: 2px solid #243b53; +} + /* === TABLEAU RÉCAPITULATIF === */ .fc-month-summary { diff --git a/modules/family-calendar/family-calendar.js b/modules/family-calendar/family-calendar.js index 2a84756..b5d7994 100644 --- a/modules/family-calendar/family-calendar.js +++ b/modules/family-calendar/family-calendar.js @@ -208,6 +208,7 @@ document.addEventListener("DOMContentLoaded", () => { } reprocessEvents() { + // Réinitialisation des semaines this.weeks.forEach((w) => { w.totals = { offCarole: 0, @@ -216,25 +217,35 @@ document.addEventListener("DOMContentLoaded", () => { avis: 0, pepSick: 0, presencePep: 0, + // Nouveaux totaux pour Alex / Laia + alexCP: 0, + alexJRA: 0, + alexJA: 0, + laiaCP: 0, + laiaJRA: 0, + laiaJA: 0, }; - Object.values(w.dayFlags).forEach((df) => (df.eventsOnDay = [])); + Object.values(w.dayFlags).forEach((df) => { + df.eventsOnDay = []; + }); }); - + // ================== Événements Carole / garde / Pep ================== this.events.forEach((evt) => { const evtDate = new Date(evt.date + "T00:00:00"); + // Trouver la semaine correspondant à la date de l'événement const week = this.weeks.find( (w) => evtDate >= w.dayDates.mon && evtDate <= w.dayDates.fri ); if (!week) return; - + // Identifier le jour (mon, tue, wed, thu, fri) const dayKey = Object.keys(week.dayDates).find( - (k) => week.dayDates[k].toDateString() === evtDate.toDateString() + (key) => week.dayDates[key].toDateString() === evtDate.toDateString() ); if (dayKey) { week.dayFlags[dayKey].eventsOnDay.push(evt); } - const dur = parseFloat(evt.duration) || 1; + // Mettre à jour les totaux hebdo selon le type switch (evt.type) { case "OFF_CAROLE": week.totals.offCarole += dur; @@ -251,6 +262,29 @@ document.addEventListener("DOMContentLoaded", () => { case "PEP_SICK": week.totals.pepSick += dur; break; + default: + break; + } + }); + // ================== Congés Alex / Laia (CP / JRA / JA) ================== + (this.leaves || []).forEach((lv) => { + const lvDate = new Date(lv.leave_date + "T00:00:00"); + const week = this.weeks.find( + (w) => lvDate >= w.dayDates.mon && lvDate <= w.dayDates.fri + ); + if (!week) return; + const isAlex = lv.person_id === 2; + const isLaia = lv.person_id === 3; + const type = lv.leave_type; // "CP", "JRA" ou "JA" + const dur = parseFloat(lv.duration) || 1; + if (isAlex) { + if (type === "CP") week.totals.alexCP += dur; + if (type === "JRA") week.totals.alexJRA += dur; + if (type === "JA") week.totals.alexJA += dur; + } else if (isLaia) { + if (type === "CP") week.totals.laiaCP += dur; + if (type === "JRA") week.totals.laiaJRA += dur; + if (type === "JA") week.totals.laiaJA += dur; } }); @@ -366,10 +400,21 @@ document.addEventListener("DOMContentLoaded", () => { const formatTotal = (total) => total > 0 ? (Number.isInteger(total) ? total : total.toFixed(1)) : ""; - this.weeks.forEach((week) => { + this.weeks.forEach((week, index) => { const tr = document.createElement("tr"); - - // Mois + // Déterminer si c'est la première ou la dernière semaine du mois + const isFirstWeekOfMonth = + index === 0 || this.weeks[index - 1].monthKey !== week.monthKey; + const isLastWeekOfMonth = + index === this.weeks.length - 1 || + this.weeks[index + 1].monthKey !== week.monthKey; + if (isFirstWeekOfMonth) { + tr.classList.add("fc-month-first-week-row"); + } + if (isLastWeekOfMonth) { + tr.classList.add("fc-month-last-week-row"); + } + // Colonne Mois (avec rowSpan) if (!monthRowRendered[week.monthKey]) { monthRowRendered[week.monthKey] = true; const tdMonth = document.createElement("td"); @@ -503,19 +548,63 @@ document.addEventListener("DOMContentLoaded", () => { tdPresencePep.classList.add("col-total"); tr.appendChild(tdPresencePep); - // 6 colonnes ALEX + // 6 colonnes ALEX : [CP Av., CP Use, JRA Av., JRA Use, JA Av., JA Use] for (let i = 0; i < 6; i++) { const td = document.createElement("td"); - td.textContent = ""; td.classList.add("col-alex-sub"); + + if (i % 2 === 0) { + // colonnes Av. (pour l'instant vides, on remplira plus tard si besoin) + td.classList.add("col-alex-av"); + td.textContent = ""; // ex: futur "solde" CP/JRA/JA + } else { + // colonnes Use : remplir selon le couple (index, type) + td.classList.add("col-alex-use"); + + let value = ""; + if (i === 1) { + // CP Use + value = formatTotal(week.totals.alexCP); + } else if (i === 3) { + // JRA Use + value = formatTotal(week.totals.alexJRA); + } else if (i === 5) { + // JA Use + value = formatTotal(week.totals.alexJA); + } + + td.textContent = value; + } + tr.appendChild(td); } - // 6 colonnes LAIA + // 6 colonnes LAIA : [CP Av., CP Use, JRA Av., JRA Use, JA Av., JA Use] for (let i = 0; i < 6; i++) { const td = document.createElement("td"); - td.textContent = ""; td.classList.add("col-laia-sub"); + + if (i % 2 === 0) { + td.classList.add("col-laia-av"); + td.textContent = ""; // futur solde + } else { + td.classList.add("col-laia-use"); + + let value = ""; + if (i === 1) { + // CP Use + value = formatTotal(week.totals.laiaCP); + } else if (i === 3) { + // JRA Use + value = formatTotal(week.totals.laiaJRA); + } else if (i === 5) { + // JA Use + value = formatTotal(week.totals.laiaJA); + } + + td.textContent = value; + } + tr.appendChild(td); } @@ -624,8 +713,8 @@ document.addEventListener("DOMContentLoaded", () => { if (this.selectedCells.length === 0) return; + // ===== Cas 1 seule cellule ===== if (this.selectedCells.length === 1) { - // Cas 1 jour : logique habituelle const date = this.selectedCells[0].dataset.date; const eventsOnDay = this.events.filter( (evt) => evt.date === date && MODIFIABLE_TYPES.includes(evt.type) @@ -634,12 +723,7 @@ document.addEventListener("DOMContentLoaded", () => { const garde = eventsOnDay.find((ev) => GUARDE_TYPES.includes(ev.type)); const pep = eventsOnDay.find((ev) => PEP_TYPES.includes(ev.type)); - if (!conge && !garde && !pep) { - this.showAddMenu(e); - } else { - this.showEditMenuForDay(e, { conge, garde, pep, date }); - } - + this.showEditMenuForDay(e, { conge, garde, pep, date }); return; } @@ -666,83 +750,200 @@ document.addEventListener("DOMContentLoaded", () => { const allDatesHaveGarde = gardes.length === selectedDates.length && uniqueGardeTypes.size === 1; - console.log("Multi-jours:"); - console.log("selectedDates:", selectedDates); - console.log("eventsOnDates:", eventsOnDates); - console.log("conges:", conges); - console.log("gardes:", gardes); - console.log("uniqueCongeTypes:", Array.from(uniqueCongeTypes)); - console.log("uniqueGardeTypes:", Array.from(uniqueGardeTypes)); - console.log("allDatesHaveConge:", allDatesHaveConge); - console.log("allDatesHaveGarde:", allDatesHaveGarde); + // On construit quand même un bulkInfo partiel (même si pas homogène) + const bulkInfo = { + selectedDates, + conges, + gardes, + congeType: allDatesHaveConge ? conges[0].type : null, + gardeType: allDatesHaveGarde ? gardes[0].type : null, + }; - if (allDatesHaveConge || allDatesHaveGarde) { - const bulkInfo = { - selectedDates, - conges, - gardes, - congeType: allDatesHaveConge ? conges[0].type : null, - gardeType: allDatesHaveGarde ? gardes[0].type : null, - }; - this.showBulkMenu(e, bulkInfo); - } else { - this.showAddMenu(e); - } + // On affiche toujours le menu bulk pour permettre Alex/Laia, + // et éventuellement les actions Carole/garde si congeType/gardeType sont définis. + this.showBulkMenu(e, bulkInfo); } showBulkMenu(e, bulkInfo) { - const { selectedDates, conges, gardes, congeType, gardeType } = bulkInfo; + const { selectedDates } = bulkInfo; const nbDays = selectedDates.length; - let html = - '
Actions multi-jours
'; + let html = ` +
+ Actions multi-jours +
+ `; - if (congeType) { - const oppositeConge = - congeType === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; - html += ` -
Congés Carole (${nbDays} jours)
+ // Section Congés Carole + html += ` +
+ Congés Carole (${nbDays} jours) +
- `; - } +
+ +
+`; - if (gardeType) { - const oppositeGarde = gardeType === "CENTRE" ? "AVIS" : "CENTRE"; - html += ` -
Mode de garde (${nbDays} jours)
+ // Section Mode de garde – toujours visible + html += ` +
+ Mode de garde (${nbDays} jours) +
- `; - } +
+ +
+`; + + // Section bulk congés Alex / Laia – tableau identique au single + html += ` +
+ Congés Alex / Laia (${nbDays} jours) +
+ + + + + + + + + + + + + + + + + + + + + +
AlexLaia
+ + + +
+ + + +
+ + + +
+
+
+`; + + // Après le tableau CP/JRA/JA + html += ` + + +`; this.selectionMenu.innerHTML = html; - // On stocke bulkInfo pour réutilisation dans handleMenuClick this._currentBulkInfo = bulkInfo; this.positionAndShowMenu(e); } @@ -778,15 +979,39 @@ document.addEventListener("DOMContentLoaded", () => { showAddMenu(e) { this.selectionMenu.innerHTML = ` -
Ajouter
- - -
Mode de Garde
- - -
Pep
- - `; +
+ Ajouter +
+ + +
+
+ +
+ Mode de garde +
+ + +
+
+ +
+ Pep +
+ +
+
+ `; this.positionAndShowMenu(e); } @@ -806,25 +1031,52 @@ document.addEventListener("DOMContentLoaded", () => { const oppositeConge = conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; congeSection = ` -
Congé Carole
- - +
+ Congé Carole +
+ + +
+
`; } else { congeSection = ` -
Ajouter un congé
- - +
+ Ajouter un congé +
+ + +
+
`; } @@ -833,23 +1085,50 @@ document.addEventListener("DOMContentLoaded", () => { if (garde) { const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; gardeSection = ` -
Mode de garde
- - +
+ Mode de garde +
+ + +
+
`; } else { gardeSection = ` -
Ajouter mode de garde
- - +
+ Ajouter mode de garde +
+ + +
+
`; } @@ -857,17 +1136,30 @@ document.addEventListener("DOMContentLoaded", () => { let pepSection = ""; if (pep) { pepSection = ` -
Pep
- +
+ Pep + +
`; } else { pepSection = ` -
Pep
- +
+ Pep + +
`; } @@ -883,30 +1175,114 @@ document.addEventListener("DOMContentLoaded", () => { console.log("[EDIT MENU] leavesOnDay =", leavesOnDay); let leavesSection = ` -
Congés Alex / Laia
- - - - - - +
+ Congés Alex / Laia +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
AlexLaia
+ + + +
+ + + +
+ + + +
+
+
`; + // Bouton de suppression si congés existants ce jour-là if (leavesOnDay.length > 0) { leavesSection += ` - `; @@ -921,121 +1297,267 @@ document.addEventListener("DOMContentLoaded", () => { positionAndShowMenu(e) { const wrapper = document.getElementById("planningTable-wrapper"); + if (!wrapper) { + // fallback sécurité + this.selectionMenu.style.display = "block"; + this.selectionMenu.style.left = `${e.clientX + 5}px`; + this.selectionMenu.style.top = `${e.clientY + 5}px`; + return; + } + const rect = wrapper.getBoundingClientRect(); this.selectionMenu.style.display = "block"; - // Position relative au wrapper, pas à la page - this.selectionMenu.style.left = `${e.clientX - rect.left + 5}px`; - this.selectionMenu.style.top = `${e.clientY - rect.top + 5}px`; + // Position relative au wrapper (qui scrolle) + const x = e.clientX - rect.left + wrapper.scrollLeft; + const y = e.clientY - rect.top + wrapper.scrollTop; + + this.selectionMenu.style.left = `${x + 5}px`; + this.selectionMenu.style.top = `${y + 5}px`; - // Empêcher la fermeture immédiate par handleClickOutsideMenu this.menuJustOpened = true; setTimeout(() => { this.menuJustOpened = false; }, 0); } + // ===== Helpers Carole / garde ===== + + // Single day – Carole + async setCaroleSingle(date, type) { + try { + // 1) supprimer OFF_CAROLE / EXTRA_OFF_CAROLE sur ce jour + await this.manageEvent({ + action: "delete_day_types", + date, + types: CONGE_TYPES, // ["OFF_CAROLE", "EXTRA_OFF_CAROLE"] + }); + + // 2) ajouter le nouveau congé + await this.manageEvent({ + action: "add_multiple", + events: [ + { + date, + type, + person: "Carole", + duration: 1.0, + }, + ], + }); + } catch (err) { + console.error("setCaroleSingle error:", err); + alert("Erreur lors de la mise à jour du congé Carole : " + err.message); + } + } + + // Single day – Garde + async setGardeSingle(date, type) { + try { + await this.manageEvent({ + action: "delete_day_types", + date, + types: GUARDE_TYPES, // ["CENTRE","AVIS"] + }); + + await this.manageEvent({ + action: "add_multiple", + events: [ + { + date, + type, + duration: 1.0, + }, + ], + }); + } catch (err) { + console.error("setGardeSingle error:", err); + alert( + "Erreur lors de la mise à jour du mode de garde : " + err.message + ); + } + } + + // Bulk – Carole + async setCaroleBulk(dates, type) { + try { + await this.manageEvent({ + action: "bulk_delete_day_types", + dates, + types: CONGE_TYPES, + }); + + await this.manageEvent({ + action: "add_multiple", + events: dates.map((date) => ({ + date, + type, + person: "Carole", + duration: 1.0, + })), + }); + } catch (err) { + console.error("setCaroleBulk error:", err); + alert("Erreur bulk congés Carole : " + err.message); + } + } + + // Bulk – Garde + async setGardeBulk(dates, type) { + try { + await this.manageEvent({ + action: "bulk_delete_day_types", + dates, + types: GUARDE_TYPES, + }); + + await this.manageEvent({ + action: "add_multiple", + events: dates.map((date) => ({ + date, + type, + duration: 1.0, + })), + }); + } catch (err) { + console.error("setGardeBulk error:", err); + alert("Erreur bulk mode de garde : " + err.message); + } + } + async handleMenuClick(e) { const button = e.target.closest("button[data-action]"); if (!button) return; const { action, eventId, newType, type, person, date } = button.dataset; - // === BULK ACTIONS (multi-jours) ============================================ - if ( - action === "bulk-update-conge" || - action === "bulk-delete-conge" || - action === "bulk-update-garde" || - action === "bulk-delete-garde" - ) { - if (!this._currentBulkInfo) { - this.clearSelection(); - return; - } + // ===================== SINGLE DAY – Carole / Garde / Pep ===================== - const bulkInfo = this._currentBulkInfo; - const target = button.dataset.target; // "conge" ou "garde" + // Single day Carole/garde (add-single depuis le menu d’un jour) + if (action === "add-single") { + const targetDate = date; - let events = []; - if (target === "conge") { - events = bulkInfo.conges; - } else if (target === "garde") { - events = bulkInfo.gardes; - } + // Switch congé Carole (OFF_CAROLE / EXTRA_OFF_CAROLE) + if (CONGE_TYPES.includes(type)) { + try { + // Récupérer les events de ce jour + const existingOnDate = this.dbEvents.filter( + (evt) => evt.date === targetDate && CONGE_TYPES.includes(evt.type) + ); - const eventIds = events.map((ev) => ev.id); - this.clearSelection(); - this._currentBulkInfo = null; - - try { - let payload; - if ( - action === "bulk-delete-conge" || - action === "bulk-delete-garde" - ) { - payload = { - action: "bulk_delete", - event_ids: eventIds, - }; - } else { - const bulkNewType = button.dataset.newType; - payload = { - action: "bulk_update", - event_ids: eventIds, - new_type: bulkNewType, - }; - } - - const res = await fetch( - "/modules/family-calendar/includes/api/manage-event.php", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), + // Supprimer tous les congés Carole ce jour-là + for (const evt of existingOnDate) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); } - ); - if (!res.ok) { - const errData = await res.json().catch(() => ({})); - throw new Error(errData.message || "Erreur HTTP " + res.status); + + // Ajouter le nouveau type + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { + date: targetDate, + type, + person: "Carole", + duration: 1.0, + }, + ]), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + } catch (err) { + console.error("Erreur congé Carole:", err); + alert("Erreur congé Carole : " + err.message); } - const resEvents = await fetch( - "/modules/family-calendar/includes/api/get-events.php" - ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } - const dataEvents = await resEvents.json(); - - this.dbEvents = dataEvents.events || []; - this.events = [...this.dbEvents, ...this.fixedEvents]; - - this.reprocessAndRender(); - this.renderMonthCalendar(); - } catch (err) { - console.error("Erreur bulk:", err); - alert("Erreur lors de l'action multi-jours: " + err.message); - } - - return; - } - - // === AJOUT D'UN CONGÉ ALEX/LAIA SUR UNE SEULE DATE ========================== - if (action === "add-leave") { - const leaveDate = date; - const personId = parseInt(button.dataset.personId, 10); - const leaveType = button.dataset.leaveType; - - if (!leaveDate || !personId || !leaveType) { this.clearSelection(); return; } - const newLeave = { - date: leaveDate, - person_id: personId, - leave_type: leaveType, + // Switch mode de garde (CENTRE / AVIS) + if (GUARDE_TYPES.includes(type)) { + try { + const existingOnDate = this.dbEvents.filter( + (evt) => + evt.date === targetDate && GUARDE_TYPES.includes(evt.type) + ); + + // Supprimer tous les modes de garde ce jour-là + for (const evt of existingOnDate) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); + } + + // Ajouter le nouveau type + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { + date: targetDate, + type, + duration: 1.0, + }, + ]), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + } catch (err) { + console.error("Erreur mode de garde:", err); + alert("Erreur mode de garde : " + err.message); + } + + this.clearSelection(); + return; + } + + // Pep (comportement existant) + const newEvent = { + date: targetDate, + type, + person, duration: 1.0, }; @@ -1043,30 +1565,244 @@ document.addEventListener("DOMContentLoaded", () => { try { const response = await fetch( - "/modules/family-calendar/includes/api/save-leaves.php", + "/modules/family-calendar/includes/api/save-events.php", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify([newLeave]), + body: JSON.stringify([newEvent]), } ); - if (!response.ok) { - throw new Error("Erreur HTTP " + response.status); - } + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; - // Recharger les congés - this.leaves = await this.fetchLeaves(); this.reprocessAndRender(); - this.renderMonthCalendar(); } catch (err) { - console.error(err); - alert("Erreur lors de l'ajout de congé Alex/Laia."); + console.error("Erreur ajout Pep:", err); + alert("Erreur lors de l'ajout."); + } + + return; + } + + // ===================== BULK – Carole / Garde / Pep =========================== + + if (action === "add") { + const selectedDates = this.selectedCells.map((c) => c.dataset.date); + + // Bulk congés Carole + if (CONGE_TYPES.includes(type)) { + try { + // Supprimer tous les OFF/EXTRA_OFF sur ces dates + const existingOnDates = this.dbEvents.filter( + (evt) => + selectedDates.includes(evt.date) && + CONGE_TYPES.includes(evt.type) + ); + for (const evt of existingOnDates) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); + } + + // Ajouter le type choisi sur chaque date + const newEvents = selectedDates.map((d) => ({ + date: d, + type, + person: "Carole", + duration: 1.0, + })); + + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newEvents), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + } catch (err) { + console.error("Erreur bulk congés Carole:", err); + alert("Erreur bulk congés Carole : " + err.message); + } + + this.clearSelection(); + return; + } + + // Bulk mode de garde + if (GUARDE_TYPES.includes(type)) { + try { + const existingOnDates = this.dbEvents.filter( + (evt) => + selectedDates.includes(evt.date) && + GUARDE_TYPES.includes(evt.type) + ); + for (const evt of existingOnDates) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); + } + + const newEvents = selectedDates.map((d) => ({ + date: d, + type, + duration: 1.0, + })); + + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newEvents), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + } catch (err) { + console.error("Erreur bulk mode de garde:", err); + alert("Erreur bulk mode de garde : " + err.message); + } + + this.clearSelection(); + return; + } + + // Bulk Pep (comportement existant) + const newEvents = this.selectedCells.map((cell) => ({ + date: cell.dataset.date, + type, + person, + duration: 1.0, + })); + + this.clearSelection(); + + try { + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newEvents), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + } catch (err) { + console.error("Erreur bulk add:", err); + alert("Erreur lors de l'ajout."); + } + + return; + } + + // ===================== ALEX / LAIA (single / bulk) ========================== + + // Single day leaves Alex/Laia + if (action === "add-leave") { + const leaveDate = date; + const personId = parseInt(button.dataset.personId, 10); + const leaveType = button.dataset.leaveType; // "CP", "JRA", "JA" + + if (!leaveDate || !personId || !leaveType) { + this.clearSelection(); + return; + } + + this.clearSelection(); + + try { + // 1) Supprimer tous les congés Alex/Laia pour cette personne ce jour-là + await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete_day_person", + date: leaveDate, + person_id: personId, + }), + }); + + // 2) Ajouter le type choisi + const responseAdd = await fetch( + "/modules/family-calendar/includes/api/save-leaves.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { + date: leaveDate, + person_id: personId, + leave_type: leaveType, + duration: 1.0, + }, + ]), + } + ); + if (!responseAdd.ok) { + throw new Error("Erreur HTTP " + responseAdd.status); + } + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur set leave single:", err); + alert("Erreur congé Alex/Laia : " + err.message); } return; } - // === SUPPRIMER TOUS LES CONGÉS ALEX/LAIA D'UN JOUR ========================== if (action === "delete-leaves-day") { const leaveDate = date; if (!leaveDate) { @@ -1074,8 +1810,6 @@ document.addEventListener("DOMContentLoaded", () => { return; } - this.clearSelection(); - try { const response = await fetch( "/modules/family-calendar/includes/api/manage-leaf.php", @@ -1096,169 +1830,80 @@ document.addEventListener("DOMContentLoaded", () => { this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { - console.error(err); + console.error("Erreur delete leaves day:", err); alert( "Erreur lors de la suppression des congés Alex/Laia pour ce jour: " + err.message ); } - return; - } - - // === AJOUT MULTI-JOURS (sélection de plusieurs cellules) =================== - if (action === "add") { - const selectedDates = this.selectedCells.map((c) => c.dataset.date); - const existingOnDates = this.dbEvents.filter((evt) => - selectedDates.includes(evt.date) - ); - - // Contrôles métier : pas de double congé / pas de double mode de garde - if (CONGE_TYPES.includes(type)) { - const hasCongeConflict = existingOnDates.some((evt) => - CONGE_TYPES.includes(evt.type) - ); - if (hasCongeConflict) { - alert( - "Impossible d'ajouter un congé : au moins une date a déjà un congé (Off/Extra Off)." - ); - this.clearSelection(); - return; - } - } else if (GUARDE_TYPES.includes(type)) { - const hasGardeConflict = existingOnDates.some((evt) => - GUARDE_TYPES.includes(evt.type) - ); - if (hasGardeConflict) { - alert( - "Impossible d'ajouter un mode de garde : au moins une date a déjà Centre/Avis." - ); - this.clearSelection(); - return; - } - } - - const newEvents = this.selectedCells.map((cell) => ({ - date: cell.dataset.date, - type, - person, - duration: 1.0, - })); - this.clearSelection(); - - try { - const response = await fetch( - "/modules/family-calendar/includes/api/save-events.php", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newEvents), - } - ); - if (!response.ok) { - throw new Error("Erreur HTTP " + response.status); - } - - // Resync DB events - const resEvents = await fetch( - "/modules/family-calendar/includes/api/get-events.php" - ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } - const dataEvents = await resEvents.json(); - - this.dbEvents = dataEvents.events || []; - this.events = [...this.dbEvents, ...this.fixedEvents]; - - this.reprocessAndRender(); - } catch (err) { - console.error(err); - alert("Erreur lors de l'ajout."); - } - return; } - // === AJOUT SUR UNE SEULE DATE (menu combiné d'un jour) ===================== - if (action === "add-single") { - const targetDate = date; - const existingOnDate = this.dbEvents.filter( - (evt) => evt.date === targetDate - ); - - if (CONGE_TYPES.includes(type)) { - const hasCongeConflict = existingOnDate.some((evt) => - CONGE_TYPES.includes(evt.type) - ); - if (hasCongeConflict) { - alert("Un congé existe déjà ce jour-là."); - this.clearSelection(); - return; - } - } else if (GUARDE_TYPES.includes(type)) { - const hasGardeConflict = existingOnDate.some((evt) => - GUARDE_TYPES.includes(evt.type) - ); - if (hasGardeConflict) { - alert("Un mode de garde existe déjà ce jour-là."); - this.clearSelection(); - return; - } - } - - const newEvent = { - date: targetDate, - type, - person, - duration: 1.0, - }; - - this.clearSelection(); - - try { - const response = await fetch( - "/modules/family-calendar/includes/api/save-events.php", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify([newEvent]), - } - ); - if (!response.ok) { - throw new Error("Erreur HTTP " + response.status); - } - - const resEvents = await fetch( - "/modules/family-calendar/includes/api/get-events.php" - ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } - const dataEvents = await resEvents.json(); - - this.dbEvents = dataEvents.events || []; - this.events = [...this.dbEvents, ...this.fixedEvents]; - - this.reprocessAndRender(); - } catch (err) { - console.error(err); - alert("Erreur lors de l'ajout."); - } - - return; - } - - // === SUPPRESSION SIMPLE ==================================================== - if (action === "delete") { - if (!eventId) { - console.warn("Bouton delete sans eventId, action ignorée."); + // Bulk leaves Alex/Laia via bulk-add-leave + if (action === "bulk-add-leave") { + if (!this._currentBulkInfo) { this.clearSelection(); return; } + const selectedDates = this._currentBulkInfo.selectedDates || []; + const personId = parseInt(button.dataset.personId, 10); + const leaveType = button.dataset.leaveType; + + this._currentBulkInfo = null; + + try { + // Supprimer pour cette personne sur toutes les dates + await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "bulk_delete_day_person", + dates: selectedDates, + person_id: personId, + }), + }); + + // Ajouter sur toutes les dates + const newLeaves = selectedDates.map((d) => ({ + date: d, + person_id: personId, + leave_type: leaveType, + duration: 1.0, + })); + + const responseAdd = await fetch( + "/modules/family-calendar/includes/api/save-leaves.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newLeaves), + } + ); + if (!responseAdd.ok) + throw new Error("Erreur HTTP " + responseAdd.status); + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur bulk-add-leave:", err); + alert("Erreur bulk congés Alex/Laia : " + err.message); + } + this.clearSelection(); + return; + } + + // ===================== SUPPRESSION SIMPLE EVENT ============================= + + if (action === "delete") { + if (!eventId) { + this.clearSelection(); + return; + } try { const response = await fetch( @@ -1279,34 +1924,64 @@ document.addEventListener("DOMContentLoaded", () => { const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } const dataEvents = await resEvents.json(); - this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { - console.error(err); + console.error("Erreur delete:", err); alert("Erreur lors de la suppression : " + err.message); } + this.clearSelection(); return; } - // === MODIFICATION SIMPLE =================================================== - if (action === "update") { - console.log("[UPDATE] click sur bouton update", { eventId, newType }); + // ===================== SUPPRESSION BULK LEAVE ============================= - if (!eventId) { - console.warn("Bouton update sans eventId, action ignorée."); + if (action === "bulk-clear-leave") { + if (!this._currentBulkInfo) { this.clearSelection(); return; } + const selectedDates = this._currentBulkInfo.selectedDates || []; + const personId = parseInt(button.dataset.personId, 10); + try { + await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "bulk_delete_day_person", + dates: selectedDates, + person_id: personId, + }), + }); + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur bulk-clear-leave:", err); + alert( + "Erreur lors de la suppression des congés sur ces jours : " + + err.message + ); + } + + this._currentBulkInfo = null; this.clearSelection(); + return; + } + + // ===================== UPDATE SIMPLE (utilisé par les boutons "Remplacer par") ===== + + if (action === "update") { + if (!eventId) { + this.clearSelection(); + return; + } try { const response = await fetch( @@ -1331,20 +2006,17 @@ document.addEventListener("DOMContentLoaded", () => { const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } const dataEvents = await resEvents.json(); - this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); } catch (err) { - console.error("[UPDATE] Erreur lors de la modification :", err); + console.error("Erreur update:", err); alert("Erreur lors de la modification : " + err.message); } + this.clearSelection(); return; } } @@ -1906,7 +2578,6 @@ document.addEventListener("DOMContentLoaded", () => { if (this.monthSelectedCells.length === 0) return; if (this.monthSelectedCells.length === 1) { - // Cas 1 jour : logique habituelle const date = this.monthSelectedCells[0].dataset.date; const eventsOnDay = this.events.filter( (evt) => evt.date === date && MODIFIABLE_TYPES.includes(evt.type) @@ -1914,15 +2585,13 @@ document.addEventListener("DOMContentLoaded", () => { const conge = eventsOnDay.find((e) => CONGE_TYPES.includes(e.type)); const garde = eventsOnDay.find((e) => GUARDE_TYPES.includes(e.type)); const pep = eventsOnDay.find((e) => PEP_TYPES.includes(e.type)); - if (!conge && !garde && !pep) { - this.showMonthAddMenu(e); - } else { - this.showMonthEditMenuForDay(e, { conge, garde, pep, date }); - } + + // Toujours ouvrir le menu complet qui inclut Alex/Laia + this.showMonthEditMenuForDay(e, { conge, garde, pep, date }); return; } - // ===== Multi-jours : tentative de bulk edit ===== + // ===== Multi-jours : toujours ouvrir le menu bulk ===== const selectedDates = this.monthSelectedCells.map((c) => c.dataset.date); const eventsOnDates = this.events.filter( (evt) => @@ -1945,18 +2614,16 @@ document.addEventListener("DOMContentLoaded", () => { const allDatesHaveGarde = gardes.length === selectedDates.length && uniqueGardeTypes.size === 1; - if (allDatesHaveConge || allDatesHaveGarde) { - const bulkInfo = { - selectedDates, - conges, - gardes, - congeType: allDatesHaveConge ? conges[0].type : null, - gardeType: allDatesHaveGarde ? gardes[0].type : null, - }; - this.showMonthBulkMenu(e, bulkInfo); - } else { - this.showMonthAddMenu(e); - } + const bulkInfo = { + selectedDates, + conges, + gardes, + congeType: allDatesHaveConge ? conges[0].type : null, + gardeType: allDatesHaveGarde ? gardes[0].type : null, + }; + + // Peu importe qu'il y ait déjà des congés ou non, on ouvre le bulk + this.showMonthBulkMenu(e, bulkInfo); } clearMonthSelection() { @@ -1971,16 +2638,64 @@ document.addEventListener("DOMContentLoaded", () => { showMonthAddMenu(e) { if (!this.monthSelectionMenu) return; + this.monthSelectionMenu.innerHTML = ` -
Ajouter
- - -
Mode de Garde
- - -
Pep
- - `; +
+ Ajouter +
+ + +
+
+ +
+ Mode de garde +
+ + +
+
+ +
+ Pep +
+ +
+
+ `; + this.positionAndShowMonthMenu(e); } @@ -1993,25 +2708,52 @@ document.addEventListener("DOMContentLoaded", () => { const oppositeConge = conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; congeSection = ` -
Congé Carole
- - +
+ Congé Carole +
+ + +
+
`; } else { congeSection = ` -
Ajouter un congé
- - +
+ Ajouter un congé +
+ + +
+
`; } @@ -2020,23 +2762,50 @@ document.addEventListener("DOMContentLoaded", () => { if (garde) { const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; gardeSection = ` -
Mode de garde
- - +
+ Mode de garde +
+ + +
+
`; } else { gardeSection = ` -
Ajouter mode de garde
- - +
+ Ajouter mode de garde +
+ + +
+
`; } @@ -2044,17 +2813,30 @@ document.addEventListener("DOMContentLoaded", () => { let pepSection = ""; if (pep) { pepSection = ` -
Pep
- +
+ Pep + +
`; } else { pepSection = ` -
Pep
- +
+ Pep + +
`; } @@ -2064,36 +2846,118 @@ document.addEventListener("DOMContentLoaded", () => { ); let leavesSection = ` -
Congés Alex / Laia
- - - - - - +
+ Congés Alex / Laia +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
AlexLaia
+ + + +
+ + + +
+ + + +
+
+
`; if (leavesOnDay.length > 0) { leavesSection += ` - `; } - // --- Assemblage du menu --- this.monthSelectionMenu.innerHTML = congeSection + gardeSection + pepSection + leavesSection; this.positionAndShowMonthMenu(e); @@ -2102,69 +2966,195 @@ document.addEventListener("DOMContentLoaded", () => { showMonthBulkMenu(e, bulkInfo) { if (!this.monthSelectionMenu) return; - const { selectedDates, conges, gardes, congeType, gardeType } = bulkInfo; + const { selectedDates } = bulkInfo; const nbDays = selectedDates.length; - let html = - '
Actions multi-jours
'; + let html = ` +
+ Actions multi-jours +
+ `; - if (congeType) { - const oppositeConge = - congeType === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; - html += ` -
Congés Carole (${nbDays} jours)
+ // === Congés Carole (toujours visible) === + html += ` +
+ Congés Carole (${nbDays} jours) +
+ + +
- - `; - } +
+ `; - if (gardeType) { - const oppositeGarde = gardeType === "CENTRE" ? "AVIS" : "CENTRE"; - html += ` -
Mode de garde (${nbDays} jours)
+ // === Mode de garde (toujours visible) === + html += ` +
+ Mode de garde (${nbDays} jours) +
+ + +
+
+ `; + + // === Congés Alex / Laia (tableau identique au single) === + html += ` +
+ Congés Alex / Laia (${nbDays} jours) +
+ + + + + + + + + + + + + + + + + + + + + +
AlexLaia
+ + + +
+ + + +
+ + + +
+
+ - `; - } +
+ `; this.monthSelectionMenu.innerHTML = html; - // On réutilise la même propriété que pour le planning hebdo : this._currentBulkInfo = bulkInfo; this.positionAndShowMonthMenu(e); } positionAndShowMonthMenu(e) { if (!this.monthSelectionMenu) return; - const wrapper = - document.querySelector(".fc-month-calendar-wrapper") || document.body; + + // parent direct qui contient le calendrier + le menu + const wrapper = this.monthSelectionMenu.parentElement; // .fc-calendar-and-summary const rect = wrapper.getBoundingClientRect(); this.monthSelectionMenu.style.display = "block"; + + // Position relative au wrapper, comme pour l’hebdo this.monthSelectionMenu.style.left = `${e.clientX - rect.left + 5}px`; this.monthSelectionMenu.style.top = `${e.clientY - rect.top + 5}px`; @@ -2180,39 +3170,126 @@ document.addEventListener("DOMContentLoaded", () => { const { action, eventId, newType, type, person, date } = button.dataset; - // Réutiliser la même logique que handleMenuClick mais avec monthSelectedCells + // === AJOUT MULTI-JOURS (Carole / garde / Pep) ============================ if (action === "add") { const selectedDates = this.monthSelectedCells.map( (c) => c.dataset.date ); - const existingOnDates = this.dbEvents.filter((evt) => - selectedDates.includes(evt.date) - ); + // Bulk congés Carole if (CONGE_TYPES.includes(type)) { - const hasCongeConflict = existingOnDates.some((evt) => - CONGE_TYPES.includes(evt.type) - ); - if (hasCongeConflict) { - alert( - "Impossible d'ajouter un congé : au moins une date a déjà un congé (Off/Extra Off)." + try { + const existingOnDates = this.dbEvents.filter( + (evt) => + selectedDates.includes(evt.date) && + CONGE_TYPES.includes(evt.type) ); - this.clearMonthSelection(); - return; - } - } else if (GUARDE_TYPES.includes(type)) { - const hasGardeConflict = existingOnDates.some((evt) => - GUARDE_TYPES.includes(evt.type) - ); - if (hasGardeConflict) { - alert( - "Impossible d'ajouter un mode de garde : au moins une date a déjà Centre/Avis." + for (const evt of existingOnDates) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); + } + + const newEvents = selectedDates.map((d) => ({ + date: d, + type, + person: "Carole", + duration: 1.0, + })); + + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newEvents), + } ); - this.clearMonthSelection(); - return; + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur bulk Carole (month):", err); + alert("Erreur bulk congés Carole : " + err.message); } + + this.clearMonthSelection(); + return; } + // Bulk mode de garde + if (GUARDE_TYPES.includes(type)) { + try { + const existingOnDates = this.dbEvents.filter( + (evt) => + selectedDates.includes(evt.date) && + GUARDE_TYPES.includes(evt.type) + ); + for (const evt of existingOnDates) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); + } + + const newEvents = selectedDates.map((d) => ({ + date: d, + type, + duration: 1.0, + })); + + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newEvents), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur bulk garde (month):", err); + alert("Erreur bulk mode de garde : " + err.message); + } + + this.clearMonthSelection(); + return; + } + + // Bulk Pep const newEvents = this.monthSelectedCells.map((cell) => ({ date: cell.dataset.date, type, @@ -2231,56 +3308,139 @@ document.addEventListener("DOMContentLoaded", () => { body: JSON.stringify(newEvents), } ); - if (!response.ok) { - throw new Error("Erreur HTTP " + response.status); - } + if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } const dataEvents = await resEvents.json(); - this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { - console.error(err); + console.error("Erreur bulk add (month):", err); alert("Erreur lors de l'ajout."); } return; } + // === AJOUT SUR UNE SEULE DATE (Carole / garde / Pep) ===================== if (action === "add-single") { const targetDate = date; - const existingOnDate = this.dbEvents.filter( - (evt) => evt.date === targetDate - ); + // Switch congé Carole if (CONGE_TYPES.includes(type)) { - const hasCongeConflict = existingOnDate.some((evt) => - CONGE_TYPES.includes(evt.type) - ); - if (hasCongeConflict) { - alert("Un congé existe déjà ce jour-là."); - this.clearMonthSelection(); - return; - } - } else if (GUARDE_TYPES.includes(type)) { - const hasGardeConflict = existingOnDate.some((evt) => - GUARDE_TYPES.includes(evt.type) - ); - if (hasGardeConflict) { - alert("Un mode de garde existe déjà ce jour-là."); - this.clearMonthSelection(); - return; + try { + const existingOnDate = this.dbEvents.filter( + (evt) => evt.date === targetDate && CONGE_TYPES.includes(evt.type) + ); + for (const evt of existingOnDate) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); + } + + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { + date: targetDate, + type, + person: "Carole", + duration: 1.0, + }, + ]), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur Carole (month single):", err); + alert("Erreur congé Carole : " + err.message); } + + this.clearMonthSelection(); + return; } + // Switch mode de garde + if (GUARDE_TYPES.includes(type)) { + try { + const existingOnDate = this.dbEvents.filter( + (evt) => + evt.date === targetDate && GUARDE_TYPES.includes(evt.type) + ); + for (const evt of existingOnDate) { + await fetch( + "/modules/family-calendar/includes/api/manage-event.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete", + event_id: evt.id, + }), + } + ); + } + + const response = await fetch( + "/modules/family-calendar/includes/api/save-events.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { + date: targetDate, + type, + duration: 1.0, + }, + ]), + } + ); + if (!response.ok) throw new Error("Erreur HTTP " + response.status); + + const resEvents = await fetch( + "/modules/family-calendar/includes/api/get-events.php" + ); + const dataEvents = await resEvents.json(); + this.dbEvents = dataEvents.events || []; + this.events = [...this.dbEvents, ...this.fixedEvents]; + + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur garde (month single):", err); + alert("Erreur mode de garde : " + err.message); + } + + this.clearMonthSelection(); + return; + } + + // Pep const newEvent = { date: targetDate, type, @@ -2299,30 +3459,25 @@ document.addEventListener("DOMContentLoaded", () => { body: JSON.stringify([newEvent]), } ); - if (!response.ok) { - throw new Error("Erreur HTTP " + response.status); - } + if (!response.ok) throw new Error("Erreur HTTP " + response.status); const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } const dataEvents = await resEvents.json(); - this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { - console.error(err); + console.error("Erreur add (month single):", err); alert("Erreur lors de l'ajout."); } return; } + // === SUPPRESSION SIMPLE (Carole / garde / Pep) =========================== if (action === "delete") { if (!eventId) { this.clearMonthSelection(); @@ -2350,23 +3505,56 @@ document.addEventListener("DOMContentLoaded", () => { const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } const dataEvents = await resEvents.json(); - this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { - console.error(err); + console.error("Erreur delete (month):", err); alert("Erreur lors de la suppression : " + err.message); } return; } + // === SUPPRESSION BULK ALEX / LAIA =========================== + if (action === "bulk-clear-leave") { + if (!this._currentBulkInfo) { + this.clearSelection(); + return; + } + const selectedDates = this._currentBulkInfo.selectedDates || []; + const personId = parseInt(button.dataset.personId, 10); + + try { + await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "bulk_delete_day_person", + dates: selectedDates, + person_id: personId, + }), + }); + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur bulk-clear-leave:", err); + alert( + "Erreur lors de la suppression des congés sur ces jours : " + + err.message + ); + } + + this._currentBulkInfo = null; + this.clearSelection(); + return; + } + + // === MODIFICATION SIMPLE =================================================== if (action === "update") { if (!eventId) { this.clearMonthSelection(); @@ -2398,22 +3586,166 @@ document.addEventListener("DOMContentLoaded", () => { const resEvents = await fetch( "/modules/family-calendar/includes/api/get-events.php" ); - if (!resEvents.ok) { - throw new Error("Erreur lors du rechargement des événements."); - } const dataEvents = await resEvents.json(); - this.dbEvents = dataEvents.events || []; this.events = [...this.dbEvents, ...this.fixedEvents]; this.reprocessAndRender(); this.renderMonthCalendar(); } catch (err) { - console.error("[UPDATE] Erreur lors de la modification :", err); + console.error("Erreur update (month):", err); alert("Erreur lors de la modification : " + err.message); } return; } + + // === ALEX / LAIA (logique identique à handleMenuClick) =================== + + if (action === "add-leave") { + const leaveDate = date; + const personId = parseInt(button.dataset.personId, 10); + const leaveType = button.dataset.leaveType; // "CP", "JRA", "JA" + + if (!leaveDate || !personId || !leaveType) { + this.clearMonthSelection(); + return; + } + + this.clearMonthSelection(); + + try { + // 1) Supprimer tous les congés Alex/Laia pour cette personne ce jour-là + await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "delete_day_person", + date: leaveDate, + person_id: personId, + }), + }); + + // 2) Ajouter le type choisi + const responseAdd = await fetch( + "/modules/family-calendar/includes/api/save-leaves.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify([ + { + date: leaveDate, + person_id: personId, + leave_type: leaveType, + duration: 1.0, + }, + ]), + } + ); + if (!responseAdd.ok) { + throw new Error("Erreur HTTP " + responseAdd.status); + } + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur set leave single:", err); + alert("Erreur congé Alex/Laia : " + err.message); + } + + return; + } + + if (action === "delete-leaves-day") { + const leaveDate = date; + if (!leaveDate) { + this.clearMonthSelection(); + return; + } + + try { + const response = await fetch( + "/modules/family-calendar/includes/api/manage-leaf.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "delete_day", date: leaveDate }), + } + ); + if (!response.ok) { + const errData = await response.json().catch(() => ({})); + throw new Error( + errData.message || "Erreur HTTP " + response.status + ); + } + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur delete_leaves_day (month):", err); + alert( + "Erreur lors de la suppression des congés Alex/Laia pour ce jour: " + + err.message + ); + } + + this.clearMonthSelection(); + return; + } + + if (action === "bulk-add-leave") { + if (!this._currentBulkInfo) { + this.clearMonthSelection(); + return; + } + + const selectedDates = this._currentBulkInfo.selectedDates || []; + const personId = parseInt(button.dataset.personId, 10); + const leaveType = button.dataset.leaveType; + + this._currentBulkInfo = null; + + try { + await fetch("/modules/family-calendar/includes/api/manage-leaf.php", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + action: "bulk_delete_day_person", + dates: selectedDates, + person_id: personId, + }), + }); + + const newLeaves = selectedDates.map((d) => ({ + date: d, + person_id: personId, + leave_type: leaveType, + duration: 1.0, + })); + + const responseAdd = await fetch( + "/modules/family-calendar/includes/api/save-leaves.php", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newLeaves), + } + ); + if (!responseAdd.ok) + throw new Error("Erreur HTTP " + responseAdd.status); + + this.leaves = await this.fetchLeaves(); + this.reprocessAndRender(); + this.renderMonthCalendar(); + } catch (err) { + console.error("Erreur bulk-add-leave (month):", err); + alert("Erreur bulk congés Alex/Laia : " + err.message); + } + + this.clearMonthSelection(); + return; + } } } diff --git a/modules/family-calendar/includes/api/events-debug.log b/modules/family-calendar/includes/api/events-debug.log index b80b64e..294814b 100644 --- a/modules/family-calendar/includes/api/events-debug.log +++ b/modules/family-calendar/includes/api/events-debug.log @@ -16,3 +16,11 @@ [2026-01-06T15:17:03+01:00] RAW INPUT: [{"date":"2025-09-10","type":"CENTRE","duration":1}] [2026-01-06T15:26:35+01:00] RAW INPUT: [{"date":"2025-10-06","type":"PEP_SICK","duration":1},{"date":"2025-10-07","type":"PEP_SICK","duration":1},{"date":"2025-10-08","type":"PEP_SICK","duration":1}] [2026-01-07T10:04:23+01:00] RAW INPUT: [{"date":"2025-09-19","type":"OFF_CAROLE","person":"Carole","duration":1}] +[2026-01-07T11:10:59+01:00] RAW INPUT: [{"date":"2025-09-04","type":"CENTRE","duration":1}] +[2026-01-07T13:33:21+01:00] RAW INPUT: [{"date":"2025-09-08","type":"CENTRE","duration":1}] +[2026-01-07T13:33:26+01:00] RAW INPUT: [{"date":"2025-09-08","type":"OFF_CAROLE","person":"Carole","duration":1}] +[2026-01-07T13:33:32+01:00] RAW INPUT: [{"date":"2025-09-08","type":"PEP_SICK","duration":1}] +[2026-01-07T13:37:27+01:00] RAW INPUT: [{"date":"2025-10-13","type":"CENTRE","duration":1}] +[2026-01-07T14:26:31+01:00] RAW INPUT: [{"date":"2025-09-08","type":"OFF_CAROLE","person":"Carole","duration":1}] +[2026-01-07T14:26:36+01:00] RAW INPUT: [{"date":"2025-09-08","type":"OFF_CAROLE","person":"Carole","duration":1},{"date":"2025-09-09","type":"OFF_CAROLE","person":"Carole","duration":1}] +[2026-01-07T14:30:24+01:00] RAW INPUT: [{"date":"2025-10-06","type":"CENTRE","duration":1},{"date":"2025-10-07","type":"CENTRE","duration":1}] diff --git a/modules/family-calendar/includes/api/manage-leaf.php b/modules/family-calendar/includes/api/manage-leaf.php index d50f97c..5ec9896 100644 --- a/modules/family-calendar/includes/api/manage-leaf.php +++ b/modules/family-calendar/includes/api/manage-leaf.php @@ -5,16 +5,23 @@ require __DIR__ . '/../../../../includes/db.php'; $input = json_decode(file_get_contents('php://input'), true); -if (!isset($input['action'])) { +if (!is_array($input)) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'JSON invalide.']); + exit; +} + +$action = $input['action'] ?? null; +if (!$action) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Action manquante.']); exit; } -$action = $input['action']; - try { + // ===================== DELETE TOUS LES LEAVES D'UN JOUR (EXISTANT) ===================== if ($action === 'delete_day') { + if (empty($input['date'])) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Date manquante pour delete_day.']); @@ -33,12 +40,99 @@ try { $stmt->execute([$leaveDate]); echo json_encode(['status' => 'success', 'message' => 'Congés supprimés pour ce jour.']); - - } else { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Action non valide.']); + exit; } + // ===================== DELETE POUR UNE PERSONNE / JOUR (SINGLE) ===================== + if ($action === 'delete_day_person') { + $date = $input['date'] ?? null; + $personId = $input['person_id'] ?? null; + + if (empty($date) || empty($personId)) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'date ou person_id manquant pour delete_day_person.']); + exit; + } + + $dateObj = date_create($date); + if (!$dateObj) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Date invalide pour delete_day_person.']); + exit; + } + $leaveDate = $dateObj->format('Y-m-d'); + + $stmt = $pdo->prepare(" + DELETE FROM pf_leaves + WHERE leave_date = :date + AND person_id = :person_id + "); + $stmt->execute([ + ':date' => $leaveDate, + ':person_id' => $personId, + ]); + + echo json_encode([ + 'status' => 'success', + 'message' => 'Congés supprimés pour cette personne et ce jour.', + 'deleted' => $stmt->rowCount(), + ]); + exit; + } + + // ===================== BULK DELETE POUR UNE PERSONNE / PLUSIEURS JOURS ===================== + if ($action === 'bulk_delete_day_person') { + $dates = $input['dates'] ?? null; + $personId = $input['person_id'] ?? null; + + if (!is_array($dates) || empty($dates) || empty($personId)) { + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'dates ou person_id manquant pour bulk_delete_day_person.']); + exit; + } + + // Normaliser les dates en 'Y-m-d' + $normalizedDates = []; + foreach ($dates as $d) { + $dateObj = date_create($d); + if ($dateObj) { + $normalizedDates[] = $dateObj->format('Y-m-d'); + } + } + + if (empty($normalizedDates)) { + echo json_encode([ + 'status' => 'success', + 'message' => 'Aucune date valide fournie.', + 'deleted' => 0, + ]); + exit; + } + + // Construire le IN (...) + $placeholders = implode(',', array_fill(0, count($normalizedDates), '?')); + $sql = " + DELETE FROM pf_leaves + WHERE person_id = ? + AND leave_date IN ($placeholders) + "; + $stmt = $pdo->prepare($sql); + + $params = array_merge([$personId], $normalizedDates); + $stmt->execute($params); + + echo json_encode([ + 'status' => 'success', + 'message' => 'Congés supprimés pour cette personne sur les dates fournies.', + 'deleted' => $stmt->rowCount(), + ]); + exit; + } + + // ===================== ACTION NON RECONNUE ===================== + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Action non valide.']); + } catch (PDOException $e) { http_response_code(500); echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);