diff --git a/family-calendar.php b/family-calendar.php index 2d4a1d4..0686738 100644 --- a/family-calendar.php +++ b/family-calendar.php @@ -1,5 +1,7 @@ query("SELECT * FROM pf_events"); -$dbEvents = $stmt_events->fetchAll(); +$dbEvents = $stmt_events->fetchAll(PDO::FETCH_ASSOC); $pageTitle = "PachaFamily - Family Calendar"; $activePage = "family-calendar"; @@ -18,75 +21,47 @@ $bodyClass = "pf-family-calendar"; $pageCss = "/modules/family-calendar/family-calendar.css"; require __DIR__ . '/header.php'; - ?> - - - - -
| Période | @@ -96,8 +71,7 @@ require __DIR__ . '/header.php';
|---|
| Mois | -Semaine | -Lundi | -Mardi | -Mercredi | -Jeudi | -Vendredi | -# Off Carole | -# Extra off Carole | -# Centre | -# Avis | -# Pep malade | -# Pep Présence | +Sem. | +Lun | +Mar | +Mer | +Jeu | +Ven | +Off Carole | +Extra Off | +Centre | +Avis | +Pep Malade | +Présence | - -ALEX | -LAIA | +ALEX | +LAIA | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CP | JRA | JA | @@ -208,15 +163,12 @@ require __DIR__ . '/header.php';JA | |||||||||||||||||||||||||||||||||||||||||||||
| Av. | +Av. | Use | Av. | Use | Av. | Use | -Av. | Use | Av. | @@ -225,54 +177,16 @@ require __DIR__ . '/header.php';Use | ||||||||||||||||||||||||||||||||||||||
No hi ha cap regal registrat per a = htmlspecialchars($year) ?> en aquesta vista.
- +$people = $baseAdults; +$adultsInDb = array_column($gifts, 'adult_name'); +$payersInDb = array_column($gifts, 'payer_name'); // Peut contenir des NULL si colonne vide, mais array_unique gère +// Nettoyage et fusion des participants +$people = array_values(array_unique(array_merge($people, $adultsInDb, $payersInDb))); +$people = array_filter($people); // Enlève les vides éventuels - -|
-
- = htmlspecialchars($adultName) ?>
-
- = number_format($totals[$adultName], 0, ',', ' ') ?> €
-
-
- |
-
-
|---|
| — | - -
|
-
-
-
-
-
-
- (pagat per = htmlspecialchars($payer) ?>)
-
-
-
-
- —
-
- |
-
-
| Adult | Infant | Festa | Total |
|---|---|---|---|
| = htmlspecialchars($row['adult_name']) ?> | -= htmlspecialchars($row['child_name']) ?> | -= htmlspecialchars($allOccasionLabels[$row['occasion']] ?? $row['occasion']) ?> | -= number_format($row['total'], 0, ',', ' ') ?> € | -
| - Deutor ↓Creditor → - | -= htmlspecialchars($p) ?> | -
|---|---|
| = htmlspecialchars($debtor) ?> | - - 0 ? 'cl-mtx-owe' : 'cl-mtx-empty'); - ?> -= $display ?> | - -
Cap deute pendent.
- -| Adult | Infant | Festa | Regal | € | Enllaç |
|---|---|---|---|---|---|
| = htmlspecialchars($g['adult_name']) ?> | -= htmlspecialchars($g['child_name']) ?> | -= htmlspecialchars($allOccasionLabels[$g['occasion']] ?? $g['occasion']) ?> | -= htmlspecialchars($g['gift_description']) ?> | -= number_format($g['amount'], 0, ',', ' ') ?> | -- - 🔗 - - | -
No hi ha cap regal registrat per a = htmlspecialchars($year) ?> en aquesta vista.
+ + + +|
+
+ = htmlspecialchars($adultName) ?>
+ = number_format($totals[$adultName], 0, ',', ' ') ?> €
+
+ |
+
+
|---|
| — | + +
|
+
+
+
+
+
+
+ (pagat per = htmlspecialchars($payer) ?>)
+
+
+
+ —
+
+ |
+
+
| Adult | Infant | Festa | Total |
|---|---|---|---|
| = htmlspecialchars($row['adult_name']) ?> | += htmlspecialchars($row['child_name']) ?> | += htmlspecialchars($allOccasionLabels[$row['occasion']] ?? $row['occasion']) ?> | += number_format($row['total'], 0, ',', ' ') ?> € | +
| Deutor ↓Creditor → | += htmlspecialchars($p) ?> | +
|---|---|
| = htmlspecialchars($debtor) ?> | + + 0 ? 'cl-mtx-owe' : 'cl-mtx-empty'); + $display = $isDiag || $val == 0 ? '—' : number_format($val, 0, ',', ' ') . ' €'; + ?> += $display ?> | + +
Cap deute pendent.
+ +| Adult | Infant | Festa | Regal | € | Enllaç |
|---|---|---|---|---|---|
| = htmlspecialchars($g['adult_name']) ?> | += htmlspecialchars($g['child_name']) ?> | += htmlspecialchars($allOccasionLabels[$g['occasion']] ?? $g['occasion']) ?> | += htmlspecialchars($g['gift_description']) ?> | += number_format($g['amount'], 0, ',', ' ') ?> | ++ + 🔗 + + | +
Off Carole : ${totalOff} jours
-Extra Off Carole : ${totalExtraOff} jours
-Pep malade : ${totalPepSick} jours
-Jours potentiels d'accueil Pep (année scolaire, hors fériés) : ${totalWorkingDays}
-Présence Pep (année scolaire) : ${totalPresencePep} jours
- `; - } - - renderSchoolHolidaysTable(holidayRecords) { - if (!this.schoolHolidaysTableBody) return; - this.schoolHolidaysTableBody.innerHTML = ""; - - const uniqueHolidays = new Map(); - holidayRecords.forEach((r) => - uniqueHolidays.set(`${r.start_date}|${r.end_date}`, r) - ); - - [...uniqueHolidays.values()] - .sort((a, b) => new Date(a.start_date) - new Date(b.start_date)) - .forEach((record) => { - const tr = document.createElement("tr"); - const startDate = new Date(record.start_date); - const endDate = new Date(record.end_date); - tr.innerHTML = ` -| Mois | -Jours ouvrés | -# Off Carole | -# Extra off Carole | -# Pep malade | -# Présence Pep | -
|---|---|---|---|---|---|
| ${monthLabel} | -${formatTotal(totals.workingDays)} | -${formatTotal(totals.offCarole)} | -${formatTotal(totals.extraOffCarole)} | -${formatTotal(totals.pepSick)} | -${formatTotal(totals.presencePep)} | -
| Mois | "; - html += "# Off Carole | "; - html += "# Extra off Carole | "; - html += "# Centre | "; - html += "# Avis | "; - html += "
|---|---|---|---|---|
| ${getMonthNameFr(month)} ${monthYear} | `; - html += `${formatTotal(totals.offCarole)} | `; - html += `${formatTotal(totals.extraOffCarole)} | `; - html += `${formatTotal(totals.centre)} | `; - html += `${formatTotal(totals.avis)} | `; - html += "
| Total ${year}-${year + 1} | `; - html += `${formatTotal( - yearTotals.offCarole - )} | `; - html += `${formatTotal( - yearTotals.extraOffCarole - )} | `; - html += `${formatTotal(yearTotals.centre)} | `; - html += `${formatTotal(yearTotals.avis)} | `; - html += "
| ${getMonthNameFr(month)} ${ - month >= 8 ? year : year + 1 - } | `; - html += `${formatTotal(totals1.offCarole)} | `; - html += `${formatTotal(totals1.extraOffCarole)} | `; - html += `${formatTotal(totals1.centre)} | `; - html += `${formatTotal(totals1.avis)} | `; - html += "
| ${getMonthNameFr(nextMonthIndex)} ${ - nextMonthIndex >= 8 ? nextYear : nextYear + 1 - } | `; - html += `${formatTotal(totals2.offCarole)} | `; - html += `${formatTotal(totals2.extraOffCarole)} | `; - html += `${formatTotal(totals2.centre)} | `; - html += `${formatTotal(totals2.avis)} | `; - html += "
| Total | "; - html += `${formatTotal( - combinedTotals.offCarole - )} | `; - html += `${formatTotal( - combinedTotals.extraOffCarole - )} | `; - html += `${formatTotal( - combinedTotals.centre - )} | `; - html += `${formatTotal(combinedTotals.avis)} | `; - html += "
| ${getMonthNameFr(month)} ${ - month >= 8 ? year : year + 1 - } | `; - html += `${formatTotal(totals.offCarole)} | `; - html += `${formatTotal(totals.extraOffCarole)} | `; - html += `${formatTotal(totals.centre)} | `; - html += `${formatTotal(totals.avis)} | `; - html += "
| ${d} | `)); + html += `||||||
|---|---|---|---|---|---|---|
| `; + } else if (row === 0 && startDay >= 5) { + html += ` | `; + } else { + const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(dayCounter).padStart(2, "0")}`; + let cls = "fc-month-day"; + const dayEvts = this.events.filter((e) => e.date === iso); + if (dayEvts.some((e) => e.type === "VACANCES_SCOLAIRES")) + cls += " fc-day--school-holiday"; + if (dayEvts.some((e) => e.type === "PUBLIC_HOLIDAY")) + cls += " fc-day--public-holiday"; + if (dayEvts.some((e) => e.type === "OFF_CAROLE")) + cls += " fc-day--off-carole"; + if (dayEvts.some((e) => e.type === "EXTRA_OFF_CAROLE")) + cls += " fc-day--extra-off-carole"; + if (dayEvts.some((e) => ["CENTRE", "AVIS"].includes(e.type))) { + cls += " fc-day--has-guard"; + if (dayEvts.some((e) => e.type === "CENTRE")) + cls += " fc-day--centre"; + if (dayEvts.some((e) => e.type === "AVIS")) + cls += " fc-day--avis"; + } - let html = ' |
| ${day} | `; - }); - html += "|
|---|---|
| '; - dayCount++; - } - - // Jours du mois actuel (uniquement lundi à vendredi) - for (let day = 1; day <= daysInMonth; day++) { - const date = new Date(year, month, day); - const dayOfWeek = date.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi - - // Ignorer les samedi (6) et dimanche (0) - if (dayOfWeek === 0 || dayOfWeek === 6) { - continue; - } - - // Nouvelle ligne chaque lundi (quand dayCount est un multiple de 5) - if (dayCount > 0 && dayCount % 5 === 0) { - html += " | |
| ${content} | `; + dayCounter++; } - - if (GUARDE_TYPES.includes(evt.type)) { - hasGarde = true; - gardeType = evt.type; - } - - if (evt.type === "PEP_SICK") { - hasPepSick = true; - } - }); - - if (hasGarde) { - classes += " fc-day--has-guard"; - if (gardeType === "CENTRE") classes += " fc-day--centre"; - if (gardeType === "AVIS") classes += " fc-day--avis"; } - - // Construire la cellule avec indicateur congés Alex/Laia - // (on génère un TD "manuel" pour pouvoir inclure le conteneur) - const leavesOnDay = this.leaves.filter( - (lv) => lv.leave_date === isoDate - ); - - let cellInnerHTML = `${day}`; - - if (hasPepSick) { - cellInnerHTML += `🤒`; - } - - if (leavesOnDay.length > 0) { - const hasAlex = leavesOnDay.some((lv) => lv.person_id === 2); // Alex - const hasLaia = leavesOnDay.some((lv) => lv.person_id === 3); // Laia - - let leavesHtml = `${cellInnerHTML} | `; - dayCount++; + html += `'; - } - } - html += " |
Off Carole : ${stats.off} jours
+Extra Off Carole : ${stats.extra} jours
+Pep malade : ${stats.sick} jours
+Présence Pep : ${stats.pep} jours
+(Sur jours ouvrés totaux : ${stats.totalWorking})
+ `; } - handleMonthMouseUp(e) { - if (!this.isMonthSelecting) return; - this.isMonthSelecting = false; - if (this.monthSelectedCells.length === 0) return; + updateSchoolYearLabel() { + const lbl = document.getElementById("fc-current-school-year-label"); + if (lbl) + lbl.textContent = `${this.currentSchoolYearStart} – ${this.currentSchoolYearStart + 1}`; + } - if (this.monthSelectedCells.length === 1) { - const date = this.monthSelectedCells[0].dataset.date; - const eventsOnDay = this.events.filter( - (evt) => evt.date === date && MODIFIABLE_TYPES.includes(evt.type) - ); - 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)); + // ================== INTERACTIONS ================== - // Toujours ouvrir le menu complet qui inclut Alex/Laia - this.showMonthEditMenuForDay(e, { conge, garde, pep, date }); - return; + setupEventListeners() { + // Planning Hebdo + this.planningBody.addEventListener("mousedown", (e) => + this.handleMouseDown(e), + ); + document.addEventListener("mousemove", (e) => this.handleMouseMove(e)); + document.addEventListener("mouseup", (e) => this.handleMouseUp(e)); + + // Calendrier Mensuel (Clic simple pour ouvrir le menu) + if (this.monthCalendar) { + this.monthCalendar.addEventListener("click", (e) => { + const td = e.target.closest("td[data-date]"); + if (td) { + this.monthSelectedCells = [td]; // Simuler sélection unique + this.showMenu(e.pageX, e.pageY, [td.dataset.date], true); + } + }); } - // ===== Multi-jours : toujours ouvrir le menu bulk ===== - const selectedDates = this.monthSelectedCells.map((c) => c.dataset.date); - const eventsOnDates = this.events.filter( - (evt) => - selectedDates.includes(evt.date) && - MODIFIABLE_TYPES.includes(evt.type) - ); - - const conges = eventsOnDates.filter((ev) => - CONGE_TYPES.includes(ev.type) - ); - const gardes = eventsOnDates.filter((ev) => - GUARDE_TYPES.includes(ev.type) - ); - - const uniqueCongeTypes = new Set(conges.map((c) => c.type)); - const uniqueGardeTypes = new Set(gardes.map((g) => g.type)); - - const allDatesHaveConge = - conges.length === selectedDates.length && uniqueCongeTypes.size === 1; - const allDatesHaveGarde = - gardes.length === selectedDates.length && uniqueGardeTypes.size === 1; - - const bulkInfo = { - selectedDates, - conges, - gardes, - congeType: allDatesHaveConge ? conges[0].type : null, - gardeType: allDatesHaveGarde ? gardes[0].type : null, + // Menus Contextuels + document.addEventListener("click", (e) => this.closeMenusIfOutside(e)); + const handleMenu = (e) => { + const btn = e.target.closest("button"); + if (btn) this.handleMenuAction(btn.dataset); }; + if (this.selectionMenu) + this.selectionMenu.addEventListener("click", handleMenu); + if (this.monthSelectionMenu) + this.monthSelectionMenu.addEventListener("click", handleMenu); - // Peu importe qu'il y ait déjà des congés ou non, on ouvre le bulk - this.showMonthBulkMenu(e, bulkInfo); + // Boutons Navigation Mensuelle + document + .getElementById("fc-prev-month") + ?.addEventListener("click", () => { + this.currentMonth.setMonth(this.currentMonth.getMonth() - 1); + this.renderMonthCalendar(); + }); + document + .getElementById("fc-next-month") + ?.addEventListener("click", () => { + this.currentMonth.setMonth(this.currentMonth.getMonth() + 1); + this.renderMonthCalendar(); + }); + + // Boutons Navigation Année Scolaire + document + .getElementById("fc-prev-school-year") + ?.addEventListener("click", () => this.changeSchoolYear(-1)); + document + .getElementById("fc-next-school-year") + ?.addEventListener("click", () => this.changeSchoolYear(1)); + + // Boutons de Vue (1 mois / 2 mois / Année) + document.querySelectorAll(".fc-view-button").forEach((btn) => { + btn.addEventListener("click", (e) => { + document + .querySelectorAll(".fc-view-button") + .forEach((b) => b.classList.remove("fc-view-button--active")); + e.target.classList.add("fc-view-button--active"); + this.viewMode = e.target.dataset.view; + this.renderMonthCalendar(); + }); + }); } - clearMonthSelection() { - if (this.monthSelectionMenu) { + handleMouseDown(e) { + const td = e.target.closest("#planningTable td[data-date]"); + if (!td) return; + e.preventDefault(); + this.clearSelection(); + this.isSelecting = true; + this.selectCell(td); + } + + handleMouseMove(e) { + if (!this.isSelecting) return; + const td = e.target.closest("#planningTable td[data-date]"); + if (td && !this.selectedCells.includes(td)) this.selectCell(td); + } + + handleMouseUp(e) { + if (!this.isSelecting) return; + + // FIX CRITIQUE : Si on relâche la souris sur une case qu'on n'a pas eu le temps de "survoler" (mouvement rapide) + // on l'ajoute maintenant pour être sûr qu'elle est dans la sélection. + const td = e.target.closest("#planningTable td[data-date]"); + if (td && !this.selectedCells.includes(td)) { + this.selectCell(td); + } + + this.isSelecting = false; + if (!this.selectedCells.length) return; + + const dates = this.selectedCells.map((c) => c.dataset.date); + this.showMenu(e.pageX, e.pageY, dates, false); + } + + selectCell(cell) { + cell.classList.add("fc-day--selected"); + this.selectedCells.push(cell); + } + + clearSelection() { + this.selectedCells.forEach((c) => c.classList.remove("fc-day--selected")); + this.selectedCells = []; + this.selectionMenu.style.display = "none"; + if (this.monthSelectionMenu) this.monthSelectionMenu.style.display = "none"; - } - this.monthSelectedCells.forEach((cell) => - cell.classList.remove("fc-day--selected") - ); - this.monthSelectedCells = []; } - showMonthAddMenu(e) { - if (!this.monthSelectionMenu) return; - - this.monthSelectionMenu.innerHTML = ` - - - - - - `; - - this.positionAndShowMonthMenu(e); + closeMenusIfOutside(e) { + if ( + !this.selectionMenu.contains(e.target) && + !this.monthSelectionMenu?.contains(e.target) && + !this.menuJustOpened + ) { + this.clearSelection(); + } } - showMonthEditMenuForDay(e, { conge, garde, pep, date }) { - if (!this.monthSelectionMenu) return; + showMenu(x, y, dates, isMonthView) { + const menu = isMonthView ? this.monthSelectionMenu : this.selectionMenu; + if (!menu) return; - // --- Section congé Carole --- - let congeSection = ""; - if (conge) { - const oppositeConge = - conge.type === "OFF_CAROLE" ? "EXTRA_OFF_CAROLE" : "OFF_CAROLE"; - congeSection = ` - - `; - } else { - congeSection = ` - - `; - } + this._currentBulkInfo = { dates }; + const dateLabel = + dates.length > 1 + ? `${dates.length} jours` + : new Date(dates[0]).toLocaleDateString("fr-FR"); - // --- Section mode de garde --- - let gardeSection = ""; - if (garde) { - const oppositeGarde = garde.type === "CENTRE" ? "AVIS" : "CENTRE"; - gardeSection = ` - - `; - } else { - gardeSection = ` - - `; - } + let html = ``; - // --- Section Pep malade --- - let pepSection = ""; - if (pep) { - pepSection = ` - - `; - } else { - pepSection = ` - - `; - } + // Carole + html += ``; - // --- Section congés Alex / Laia --- - const leavesOnDay = (this.leaves || []).filter( - (lv) => lv.leave_date === date - ); + // Garde + html += ``; - let leavesSection = ` - - `; + // Pep + html += ``; - if (leavesOnDay.length > 0) { - leavesSection += ` - - `; - } + // Alex/Laia + html += ``; - this.monthSelectionMenu.innerHTML = - congeSection + gardeSection + pepSection + leavesSection; - this.positionAndShowMonthMenu(e); - } + menu.innerHTML = html; - showMonthBulkMenu(e, bulkInfo) { - if (!this.monthSelectionMenu) return; + const menuWidth = 240; + let left = x + 10; + if (left + menuWidth > window.innerWidth) left = x - menuWidth - 10; - const { selectedDates } = bulkInfo; - const nbDays = selectedDates.length; - - let html = ` - - `; - - // === Congés Carole (toujours visible) === - html += ` - - `; - - // === Mode de garde (toujours visible) === - html += ` - - `; - - // === Congés Alex / Laia (tableau identique au single) === - html += ` - - `; - - this.monthSelectionMenu.innerHTML = html; - this._currentBulkInfo = bulkInfo; - this.positionAndShowMonthMenu(e); - } - - positionAndShowMonthMenu(e) { - if (!this.monthSelectionMenu) return; - - // 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`; + menu.style.left = `${left}px`; + menu.style.top = `${y + 10}px`; + menu.style.display = "block"; this.menuJustOpened = true; - setTimeout(() => { - this.menuJustOpened = false; - }, 0); + setTimeout(() => (this.menuJustOpened = false), 100); } - async handleMonthMenuClick(e) { - const button = e.target.closest("button[data-action]"); - if (!button) return; + async handleMenuAction(dataset) { + this.selectionMenu.style.display = "none"; + if (this.monthSelectionMenu) + this.monthSelectionMenu.style.display = "none"; - const { action, eventId, newType, type, person, date } = button.dataset; + const { action, type, pid, cat } = dataset; + const dates = this._currentBulkInfo.dates; - // === AJOUT MULTI-JOURS (Carole / garde / Pep) ============================ - if (action === "add") { - const selectedDates = this.monthSelectedCells.map( - (c) => c.dataset.date - ); + try { + if (action === "add") { + let typesToClear = []; + if (["OFF_CAROLE", "EXTRA_OFF_CAROLE"].includes(type)) + typesToClear = CONGE_TYPES; + if (["CENTRE", "AVIS"].includes(type)) typesToClear = GUARDE_TYPES; + if (type === "PEP_SICK") typesToClear = PEP_TYPES; - // Bulk congés Carole - if (CONGE_TYPES.includes(type)) { - try { - 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, - }), - } - ); - } - - 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", + if (typesToClear.length) { + await this.postApi( + "/modules/family-calendar/includes/api/manage-event.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newEvents), - } + action: "bulk_delete_day_types", + dates, + types: typesToClear, + }, ); - 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, - person, - duration: 1.0, - })); - - this.clearMonthSelection(); - - try { - const response = await fetch( + const payload = dates.map((d) => ({ + date: d, + type: type, + duration: 1, + person: "Carole", + })); + await this.postApi( "/modules/family-calendar/includes/api/save-events.php", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newEvents), - } + payload, ); - if (!response.ok) throw new Error("Erreur HTTP " + response.status); + } else if (action === "clear-type") { + let typesToClear = []; + if (cat === "CONGE") typesToClear = CONGE_TYPES; + if (cat === "GARDE") typesToClear = GUARDE_TYPES; + if (cat === "PEP") typesToClear = PEP_TYPES; - 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 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; - - // Switch congé Carole - if (CONGE_TYPES.includes(type)) { - 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, - person, - duration: 1.0, - }; - - this.clearMonthSelection(); - - 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" - ); - const dataEvents = await resEvents.json(); - this.dbEvents = dataEvents.events || []; - this.events = [...this.dbEvents, ...this.fixedEvents]; - - this.reprocessAndRender(); - this.renderMonthCalendar(); - } catch (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(); - return; - } - - this.clearMonthSelection(); - - try { - const response = await fetch( + await this.postApi( "/modules/family-calendar/includes/api/manage-event.php", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "delete", event_id: eventId }), - } + action: "bulk_delete_day_types", + dates, + types: typesToClear, + }, ); - if (!response.ok) { - const errData = await response.json().catch(() => ({})); - throw new Error( - errData.message || "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 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(); - return; - } - - this.clearMonthSelection(); - - try { - const response = await fetch( - "/modules/family-calendar/includes/api/manage-event.php", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - action: "update", - event_id: eventId, - new_type: newType, - }), - } - ); - if (!response.ok) { - const errData = await response.json().catch(() => ({})); - throw new Error( - errData.message || "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 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; - } - - // Bloquer tous les types de congés (CP, JRA, JA) pour Alex (2) et Laia (3) - if (personId === 2 || personId === 3) { - const available = this.getAvailableAtMonthStart( - personId, - leaveType, - leaveDate - ); - // On pose 1 jour - if (available != null && available < 1) { - const disp = available.toFixed(2); - alert( - `Impossible d'ajouter ${leaveType} pour ${ - personId === 2 ? "Alex" : "Laia" - } : il reste ${disp} jour(s) disponible(s) au début de ce mois.` - ); - this.clearMonthSelection(); - return; - } - } - - this.clearMonthSelection(); - - try { - // 1) supprimer les congés existants 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 nouveau type - const newLeave = { - date: leaveDate, - 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([newLeave]), - } - ); - if (!responseAdd.ok) { - throw new Error("Erreur HTTP " + responseAdd.status); - } - - this.leaves = await this.fetchLeaves(); - this.reprocessAndRender(); - this.renderMonthCalendar(); - } catch (err) { - console.error("Erreur add-leave (month):", 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( + } else if (action === "add-leave") { + await this.postApi( "/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; // "CP", "JRA", "JA" - - if (personId === 2 || personId === 3) { - // Regrouper les dates par mois - const datesByMonth = {}; // ymKey -> dates[] - selectedDates.forEach((d) => { - const dateObj = new Date(d + "T00:00:00"); - const ymKey = `${dateObj.getFullYear()}-${String( - dateObj.getMonth() + 1 - ).padStart(2, "0")}`; - if (!datesByMonth[ymKey]) datesByMonth[ymKey] = []; - datesByMonth[ymKey].push(d); - }); - - // Pour chaque mois, vérifier Av. >= nb jours demandés dans ce mois - for (const ymKey of Object.keys(datesByMonth)) { - const datesInMonth = datesByMonth[ymKey]; - const anyDate = datesInMonth[0]; - const available = this.getAvailableAtMonthStart( - personId, - leaveType, - anyDate - ); - const needed = datesInMonth.length; - - if (available != null && available < needed) { - const disp = available.toFixed(2); - alert( - `Impossible d'ajouter ${leaveType} pour ${ - personId === 2 ? "Alex" : "Laia" - } sur ${needed} jour(s) dans ${ymKey} : il reste ${disp} jour(s) disponible(s).` - ); - this.clearMonthSelection(); - this._currentBulkInfo = null; - return; - } - } - } - - this._currentBulkInfo = null; - - try { - // delete & add (ton code existant) - 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) => ({ + dates, + person_id: pid, + }, + ); + const payload = dates.map((d) => ({ date: d, - person_id: personId, - leave_type: leaveType, - duration: 1.0, + person_id: pid, + leave_type: type, + duration: 1, })); - - const responseAdd = await fetch( + await this.postApi( "/modules/family-calendar/includes/api/save-leaves.php", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newLeaves), - } + payload, + ); + } else if (action === "clear-leaves") { + await this.postApi( + "/modules/family-calendar/includes/api/manage-leaf.php", + { action: "bulk_delete_day_person", dates, person_id: 2 }, + ); + await this.postApi( + "/modules/family-calendar/includes/api/manage-leaf.php", + { action: "bulk_delete_day_person", dates, person_id: 3 }, ); - 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.clearMonthSelection(); - return; + await this.refreshAllData(); + } catch (e) { + alert("Erreur action: " + e.message); } } - } - function getWeekOfYear(date) { - const d = new Date( - Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - ); - d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); - const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); - return Math.ceil(((d - yearStart) / 86400000 + 1) / 7); - } + async fetchApi(url) { + return fetch(url).then((r) => r.json()); + } - function getMonthNameFr(monthIndex) { - return ( - [ - "Janvier", - "Fevrier", - "Mars", - "Avril", - "Mai", - "Juin", - "Juillet", - "Aout", - "Septembre", - "Octobre", - "Novembre", - "Decembre", - ][monthIndex] || "" - ); + async postApi(url, data) { + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); + } + + async changeSchoolYear(delta) { + this.currentSchoolYearStart += delta; + this.updateSchoolYearLabel(); + await this.refreshAllData(); + } } new FamilyCalendar(); diff --git a/modules/family-calendar/includes/api/events-debug.log b/modules/family-calendar/includes/api/events-debug.log index 0e6de9e..f845bd8 100644 --- a/modules/family-calendar/includes/api/events-debug.log +++ b/modules/family-calendar/includes/api/events-debug.log @@ -31,3 +31,11 @@ [2026-01-08T09:23:59+01:00] RAW INPUT: [{"date":"2025-12-22","type":"CENTRE","duration":1}] [2026-01-08T09:24:00+01:00] RAW INPUT: [{"date":"2025-12-23","type":"CENTRE","duration":1}] [2026-01-08T09:24:02+01:00] RAW INPUT: [{"date":"2025-12-24","type":"CENTRE","duration":1}] +[2026-01-30T22:49:07+01:00] RAW INPUT: [{"date":"2025-10-24","type":"AVIS","duration":1,"person":null}] +[2026-01-30T22:49:50+01:00] RAW INPUT: [{"date":"2025-10-16","type":"OFF_CAROLE","duration":1,"person":null}] +[2026-01-30T22:50:00+01:00] RAW INPUT: [{"date":"2025-09-11","type":"OFF_CAROLE","duration":1,"person":null}] +[2026-01-30T22:51:16+01:00] RAW INPUT: [{"date":"2025-09-05","type":"AVIS","duration":1,"person":null}] +[2026-01-30T22:53:23+01:00] RAW INPUT: [{"date":"2025-09-03","type":"AVIS","duration":1,"person":null}] +[2026-01-30T23:16:56+01:00] RAW INPUT: [{"date":"2025-09-28","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-09-29","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-09-30","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-01","type":"OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-02","type":"OFF_CAROLE","duration":1,"person":"Carole"}] +[2026-01-30T23:17:02+01:00] RAW INPUT: [{"date":"2025-10-05","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-06","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-07","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-08","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"},{"date":"2025-10-09","type":"EXTRA_OFF_CAROLE","duration":1,"person":"Carole"}] +[2026-01-30T23:17:09+01:00] RAW INPUT: [{"date":"2025-10-05","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-06","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-07","type":"AVIS","duration":1,"person":"Carole"},{"date":"2025-10-08","type":"AVIS","duration":1,"person":"Carole"}] diff --git a/modules/family-calendar/includes/api/manage-event.php b/modules/family-calendar/includes/api/manage-event.php index e422735..cb744c3 100644 --- a/modules/family-calendar/includes/api/manage-event.php +++ b/modules/family-calendar/includes/api/manage-event.php @@ -1,103 +1,94 @@ 'error', 'message' => 'Action manquante.']); exit; } -$action = $input['action']; - try { + // --- SUPPRESSION UNITAIRE --- if ($action === 'delete') { - // Suppression d'un seul événement - if (!isset($input['event_id'])) { + $eventId = (int)($input['event_id'] ?? 0); + if ($eventId <= 0) { http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'ID manquant pour la suppression.']); + echo json_encode(['status' => 'error', 'message' => 'ID manquant.']); exit; } - - $eventId = (int)$input['event_id']; $stmt = $pdo->prepare("DELETE FROM pf_events WHERE id = ?"); $stmt->execute([$eventId]); + echo json_encode(['status' => 'success']); + exit; + } - echo json_encode(['status' => 'success', 'message' => 'Événement supprimé.']); - - } elseif ($action === 'update') { - // Mise à jour d'un seul événement - if (!isset($input['event_id'], $input['new_type'])) { + // --- MISE À JOUR UNITAIRE --- + if ($action === 'update') { + $eventId = (int)($input['event_id'] ?? 0); + $newType = $input['new_type'] ?? ''; + if ($eventId <= 0 || !$newType) { http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'ID ou nouveau type manquant pour la mise à jour.']); + echo json_encode(['status' => 'error', 'message' => 'Données manquantes.']); exit; } - - $eventId = (int)$input['event_id']; - $newType = $input['new_type']; - $stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id = ?"); $stmt->execute([$newType, $eventId]); - - echo json_encode(['status' => 'success', 'message' => 'Événement mis à jour.']); - - } elseif ($action === 'bulk_delete') { - // Suppression en masse - if (empty($input['event_ids']) || !is_array($input['event_ids'])) { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Liste event_ids manquante pour bulk_delete.']); - exit; - } - - $eventIds = array_map('intval', $input['event_ids']); - $eventIds = array_filter($eventIds, fn($id) => $id > 0); - - if (empty($eventIds)) { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_delete.']); - exit; - } - - $placeholders = implode(',', array_fill(0, count($eventIds), '?')); - $stmt = $pdo->prepare("DELETE FROM pf_events WHERE id IN ($placeholders)"); - $stmt->execute($eventIds); - - echo json_encode(['status' => 'success', 'message' => 'Événements supprimés en masse.']); - - } elseif ($action === 'bulk_update') { - // Mise à jour en masse - if (empty($input['event_ids']) || !is_array($input['event_ids']) || !isset($input['new_type'])) { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'event_ids ou new_type manquant pour bulk_update.']); - exit; - } - - $eventIds = array_map('intval', $input['event_ids']); - $eventIds = array_filter($eventIds, fn($id) => $id > 0); - $newType = $input['new_type']; - - if (empty($eventIds)) { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Aucun ID valide pour bulk_update.']); - exit; - } - - $placeholders = implode(',', array_fill(0, count($eventIds), '?')); - $params = array_merge([$newType], $eventIds); - - $stmt = $pdo->prepare("UPDATE pf_events SET event_type = ? WHERE id IN ($placeholders)"); - $stmt->execute($params); - - echo json_encode(['status' => 'success', 'message' => 'Événements mis à jour en masse.']); - - } else { - http_response_code(400); - echo json_encode(['status' => 'error', 'message' => 'Action non valide.']); + echo json_encode(['status' => 'success']); + exit; } + // --- SUPPRESSION DE MASSE (Par date et type) --- + // Utilisé quand on ajoute un événement pour nettoyer les doublons potentiels (ex: Off vs Extra) + if ($action === 'bulk_delete_day_types') { + $dates = $input['dates'] ?? []; + $types = $input['types'] ?? []; + + if (empty($dates) || empty($types)) { + echo json_encode(['status' => 'success']); // Rien à faire + exit; + } + + // Création des placeholders IN (?,?,?) + $datePlaceholders = implode(',', array_fill(0, count($dates), '?')); + $typePlaceholders = implode(',', array_fill(0, count($types), '?')); + + $sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders) AND event_type IN ($typePlaceholders)"; + $stmt = $pdo->prepare($sql); + + // Fusion des tableaux pour l'exécution + $stmt->execute(array_merge($dates, $types)); + + echo json_encode(['status' => 'success']); + exit; + } + + // --- SUPPRESSION TOTALE SUR DES DATES --- + if ($action === 'bulk_delete_all') { + $dates = $input['dates'] ?? []; + if (empty($dates)) { + echo json_encode(['status' => 'success']); + exit; + } + + $datePlaceholders = implode(',', array_fill(0, count($dates), '?')); + $sql = "DELETE FROM pf_events WHERE event_date IN ($datePlaceholders)"; + $stmt = $pdo->prepare($sql); + $stmt->execute($dates); + + echo json_encode(['status' => 'success']); + exit; + } + + // Action inconnue + http_response_code(400); + echo json_encode(['status' => 'error', 'message' => 'Action non reconnue : ' . $action]); + } catch (PDOException $e) { http_response_code(500); echo json_encode(['status' => 'error', 'message' => $e->getMessage()]); -} +} \ No newline at end of file diff --git a/modules/gift-list/gift-list.css b/modules/gift-list/gift-list.css index e60df7e..73c0363 100644 --- a/modules/gift-list/gift-list.css +++ b/modules/gift-list/gift-list.css @@ -1,186 +1,308 @@ -/* ========================================================= */ -/* Styles généraux page gift list */ -/* ========================================================= */ +/* modules/gift-list/gift-list.css */ + +/* --- 1. VARIABLES & BASE --- */ +:root { + --primary: #2563eb; + --bg-page: #f8fafc; + --text-main: #1e293b; + --text-muted: #64748b; + --border-light: #e2e8f0; + + --radius-card: 12px; + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.pf-gift-list { + background-color: var(--bg-page); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif; + color: var(--text-main); + font-size: 13px; +} .pf-gift-list h1 { - margin-bottom: 4px; + font-size: 1.6rem; + font-weight: 700; + color: #0f172a; + margin: 0; } -.pf-gift-list p { - margin-top: 0; - margin-bottom: 12px; - font-size: 13px; - color: #4b5563; -} - -/* Petit texte explicatif sous les titres */ .cl-legend { - font-size: 12px; - color: #6b7280; - margin-bottom: 8px; + font-size: 0.85rem; + color: var(--text-muted); + font-style: italic; + margin-top: 4px; } -/* ========================================================= */ -/* Vue par fête: mini-tableaux par enfant */ -/* ========================================================= */ +/* --- 2. HEADER & SWITCH --- */ +.cl-titlebar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-light); +} +.cl-view-switch { + display: flex; + background: white; + padding: 4px; + border-radius: 8px; + border: 1px solid #cbd5e1; +} + +.cl-view-btn { + padding: 6px 16px; + border-radius: 6px; + text-decoration: none; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); +} + +.cl-view-btn.is-active { + background: #334155; + color: white; +} + +/* --- 3. BLOCS OCCASIONS & GRILLE --- */ .cl-occasion-block { - margin-top: 2rem; - border-top: 2px solid #e5e5e5; + margin-bottom: 40px; } .cl-occasion-title { font-size: 1.2rem; - margin-bottom: 0.5rem; + font-weight: 700; + color: #334155; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 10px; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 2px solid #e2e8f0; + padding-bottom: 8px; } +.cl-occasion-icon { + width: 24px; + height: 24px; + object-fit: contain; +} + +/* --- GRILLE INTELLIGENTE (Le cœur de la mise en page) --- */ + .cl-occasion-children-tables { display: grid; - grid-template-columns: repeat(3, minmax(280px, 1fr)); - gap: 7px; + gap: 20px; align-items: start; - margin-top: 8px; } -/* Désactiver la grille uniquement pour la vue Anniversary */ -.pf-gift-list .cl-view-anniversary .cl-occasion-children-tables { - display: block; /* remplace la grid */ +/* VUE STANDARD (Nadal) : 3 par ligne */ +.cl-view-nadal .cl-occasion-children-tables { + grid-template-columns: repeat(3, 1fr); } -.pf-gift-list .cl-view-anniversary .cl-child-table { - width: 100%; - min-width: 720px; /* ajuste si nécessaire avec 6 colonnes */ - margin-bottom: 12px; /* espacement entre tables */ - overflow-x: auto; /* sécurité si ça déborde sur petit écran */ +/* VUE ANNIVERSARY : Layout spécifique demandé */ +.cl-view-anniversary .cl-occasion-children-tables { + grid-template-columns: repeat(6, 1fr); /* Grille de 6 colonnes */ } -@media (max-width: 1200px) { - .cl-occasion-children-tables { - grid-template-columns: repeat(2, minmax(280px, 1fr)); - } +/* Pol et Pep : Prennent 3 colonnes sur 6 (donc 50% de largeur => 2 par ligne) */ +.cl-view-anniversary .child-pol, +.cl-view-anniversary .child-pep { + grid-column: span 3; } -@media (max-width: 780px) { - .cl-occasion-children-tables { + +/* Les autres : Prennent 2 colonnes sur 6 (donc 33% de largeur => 3 par ligne) */ +.cl-view-anniversary .child-elna, +.cl-view-anniversary .child-bru, +.cl-view-anniversary .child-guim { + grid-column: span 2; +} + +/* Responsive Mobile (tous sur 1 colonne) */ +@media (max-width: 1000px) { + .cl-view-nadal .cl-occasion-children-tables, + .cl-view-anniversary .cl-occasion-children-tables { grid-template-columns: 1fr; } + .cl-view-anniversary .cl-child-table { + grid-column: span 1; + } } -/* ========================================================= */ -/* Mini-tableau par enfant */ -/* ========================================================= */ +/* --- 4. TABLEAU PAR ENFANT (Card Unifiée) --- */ .cl-child-table { - display: table !important; /* robustesse même avec resets */ - table-layout: fixed; + background: white; + + /* L'ombre est sur le tableau global */ + box-shadow: var(--shadow-card); + + /* Arrondi uniquement en BAS (le caption gère le haut) */ + border-radius: 0 0 var(--radius-card) var(--radius-card); + + /* La bordure du tableau ne fait que gauche/droite/bas */ + border: 1px solid var(--border-light); + border-top: none; + + /* Structure tableau stricte pour garder le caption en haut */ + display: table; width: 100%; - border-collapse: collapse; - background: #fff; - border: 1px solid #d9e2ec; - overflow: hidden; -} -.cl-child-table thead { - display: table-header-group !important; -} -.cl-child-table tbody { - display: table-row-group !important; -} -.cl-child-table tr { - display: table-row !important; -} -.cl-child-table th, -.cl-child-table td { - display: table-cell !important; - box-sizing: border-box; + table-layout: fixed; + border-collapse: separate; + border-spacing: 0; } -/* Nom de l'enfant (caption) + bouton + */ +/* CAPTION (Nom de l'enfant - Haut de la carte) */ .cl-child-table caption { + /* Force l'affichage en haut */ caption-side: top; - position: relative; /* pour positionner le + */ - text-align: center; + display: table-caption; + + padding: 10px 16px; + text-align: left; font-weight: 700; - padding: 8px 10px; - background: #f8fafc; - border-bottom: 1px solid #e5e7eb; - font-size: 13px; - padding-right: 64px; + font-size: 1.1rem; + + /* Arrondi uniquement en HAUT */ + border-radius: var(--radius-card) var(--radius-card) 0 0; + + /* Bordure complète pour le caption */ + border: 1px solid var(--border-light); + border-bottom: 2px solid; /* Le trait de séparation coloré */ + + /* Colle le caption au tableau */ + margin-bottom: 0; + + position: relative; } -/* Bouton + dans le caption */ +/* Bouton Ajout (+) Rotatif */ .cl-child-add-btn { + /* Position absolue par rapport au caption */ position: absolute; - right: 8px; + right: 12px; top: 50%; transform: translateY(-50%); - width: 48px; - height: 48px; + + background: rgba(255, 255, 255, 0.6); + color: inherit; + border: 1px solid rgba(0, 0, 0, 0.1); + width: 28px; + height: 28px; + border-radius: 50%; + font-size: 18px; + line-height: 1; + cursor: pointer; display: flex; align-items: center; justify-content: center; - color: #243b53; - font-size: 15px; - font-weight: 700; - line-height: 1; - background: transparent; - border: none; - cursor: pointer; - isolation: isolate; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + z-index: 2; } -.cl-child-add-btn::before { - content: ""; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 24px; - height: 24px; - border-radius: 50%; - border: 1px solid #d9e2ec; - background: #f0f4f8; - pointer-events: none; - z-index: -1; +.cl-child-add-btn:hover { + background: white; + /* La rotation que tu aimais */ + transform: translateY(-50%) rotate(90deg) scale(1.1); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + border-color: currentColor; } -/* Hover/focus: même feedback visuel */ -.cl-child-add-btn:hover::before, -.cl-child-add-btn:focus-visible::before { - background: #d9e2ec; +/* --- 5. COULEURS PAR ENFANT (Appliquées au Caption ET au Tableau) --- */ + +/* POL (Bleu) */ +.child-pol { + border-color: #bcd3ff; +} /* Bordure du tableau (bas/cotés) */ +.child-pol caption { + background: #eaf2ff; + color: #1e3a8a; + border-color: #bcd3ff; /* Bordure du caption (haut/cotés) */ + border-bottom-color: #93c5fd; /* Trait de séparation un peu plus foncé */ } -/* Accessibilité focus clavier sur le hit-area */ -.cl-child-add-btn:focus-visible { - outline: 2px solid #2563eb; - outline-offset: 2px; - border-radius: 8px; /* outline carré autour du hit-area */ +.child-pol thead th { + background: #f5f9ff; } -@media (pointer: coarse) { - .cl-child-add-btn { - width: 56px; - height: 56px; - font-size: 22px; - } - .cl-child-table caption { - padding-right: 72px; - } +/* PEP (Vert) */ +.child-pep { + border-color: #b9e3b9; +} +.child-pep caption { + background: #eaf7ea; + color: #14532d; + border-color: #b9e3b9; + border-bottom-color: #86efac; +} +.child-pep thead th { + background: #f6fdf6; } -/* Colonnes égales (3 adultes) via colgroup */ -.cl-child-table col.cl-col { - width: 33.3333%; +/* ELNA (Rose) */ +.child-elna { + border-color: #f3bfd7; +} +.child-elna caption { + background: #fdeaf3; + color: #831843; + border-color: #f3bfd7; + border-bottom-color: #f9a8d4; +} +.child-elna thead th { + background: #fff5f9; } -/* En-têtes adultes: séparateurs + total à droite */ +/* BRU (Orange) */ +.child-bru { + border-color: #ffd0a8; +} +.child-bru caption { + background: #fff3e6; + color: #7c2d12; + border-color: #ffd0a8; + border-bottom-color: #fdba74; +} +.child-bru thead th { + background: #fffaf5; +} + +/* GUIM (Violet) */ +.child-guim { + border-color: #d3c6ff; +} +.child-guim caption { + background: #f0eaff; + color: #4c1d95; + border-color: #d3c6ff; + border-bottom-color: #c4b5fd; +} +.child-guim thead th { + background: #fbf9ff; +} + +/* --- 6. CONTENU DU TABLEAU --- */ + +/* En-têtes Adultes */ .cl-child-table thead th { - background: #f3f4f6; - font-weight: 600; - padding: 6px 8px; - border-bottom: 1px solid #e5e7eb; - font-size: 12px; - text-align: left; - vertical-align: middle; - white-space: nowrap; - border-right: 1px solid #e5e7eb; + padding: 8px; + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 700; + text-transform: uppercase; + border-bottom: 1px solid var(--border-light); + border-right: 1px solid var(--border-light); + + /* Pas de largeur fixe, table-layout: fixed gère le partage équitable */ + white-space: normal; + overflow: hidden; + text-overflow: ellipsis; } .cl-child-table thead th:last-child { border-right: none; @@ -188,482 +310,338 @@ .cl-th-inner { display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; + align-items: flex-start; + gap: 2px; } -.cl-th-label { - overflow: hidden; - text-overflow: ellipsis; -} - -/* Total adulte (réutilise le style global) */ .cl-summary-adult-total { - font-size: 12px; - font-weight: 600; - color: #1d4ed8; + font-size: 0.7rem; + color: var(--primary); + background: rgba(37, 99, 235, 0.1); + padding: 1px 4px; + border-radius: 4px; } -/* Corps: séparateurs + gestion des noms longs */ +/* Corps du tableau */ .cl-child-table tbody td { + padding: 8px; vertical-align: top; - padding: 6px 4px; - border-top: 1px solid #edf2f7; - border-right: 1px solid #e5e7eb; - font-size: 12px; - line-height: 1.35; + border-bottom: 1px solid #f1f5f9; + border-right: 1px solid #f1f5f9; + font-size: 0.85rem; } .cl-child-table tbody td:last-child { border-right: none; } +.cl-child-table tr:last-child td { + border-bottom: none; +} -/* Cellules vides: un “—” par adulte, centré, pleine largeur */ -.cl-child-table .cl-empty { - display: inline-block; - width: 100%; +/* Cellule vide */ +.cl-empty { + color: #e2e8f0; text-align: center; - color: #9ca3af; + display: block; + font-size: 1.2rem; + line-height: 1; } +/* --- 7. CADEAUX & INTERACTION (Overlay) --- */ +.cl-gift-item { + display: flex; + flex-direction: column; + gap: 2px; +} + +.cl-gift-line { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 4px; + position: relative; /* Pour l'overlay */ + min-height: 20px; /* Hauteur min pour éviter le saut si vide */ +} + +/* Texte du cadeau */ +.cl-gift-desc, +.cl-gift-link { + font-weight: 500; + color: #0f172a; + line-height: 1.2; + font-size: 0.85rem; + + /* Césure forcée pour éviter le scroll */ + word-wrap: break-word; + word-break: break-word; + hyphens: auto; + + flex: 1; /* Prend toute la place dispo à gauche */ +} + +.cl-gift-link { + color: var(--primary); + text-decoration: none; +} +.cl-gift-link:hover { + text-decoration: underline; +} + +/* Zone Droite (Prix OU Actions) */ +.cl-gift-right { + display: flex; + align-items: center; + justify-content: flex-end; + /* Largeur fixe pour que le switch ne décale pas le texte de gauche */ + width: 50px; + flex-shrink: 0; +} + +/* Le Prix (Visible par défaut) */ .cl-gift-amount { - margin-left: auto; - text-align: right; - color: #627d98; + font-size: 0.75rem; + font-weight: 700; + color: #059669; + background: #ecfdf5; + padding: 1px 4px; + border-radius: 4px; white-space: nowrap; - min-width: fit-content; - transition: opacity 120ms ease; + + transition: + opacity 0.2s, + transform 0.2s; + opacity: 1; + transform: scale(1); } -/* Code couleur par enfant (bordure + header/caption) */ -.cl-child-table.child-pol { - border-color: #bcd3ff; -} -.cl-child-table.child-pol caption, -.cl-child-table.child-pol thead th { - background: #eaf2ff; - border-bottom-color: #bcd3ff; +/* Les Actions (Masquées par défaut, Absolute pour superposition) */ +.cl-gift-actions { + display: flex; + gap: 4px; + position: absolute; + right: 0; + top: 0; + + opacity: 0; + transform: scale(0.8); + pointer-events: none; /* Empêche le clic quand invisible */ + transition: + opacity 0.2s, + transform 0.2s; } -.cl-child-table.child-pep { - border-color: #b9e3b9; +/* === L'EFFET SWAP === */ +/* Au survol de la ligne cadeau... */ +.cl-gift-line:hover .cl-gift-amount { + opacity: 0; + transform: scale(0.8); } -.cl-child-table.child-pep caption, -.cl-child-table.child-pep thead th { - background: #eaf7ea; - border-bottom-color: #b9e3b9; +.cl-gift-line:hover .cl-gift-actions { + opacity: 1; + transform: scale(1); + pointer-events: auto; } -.cl-child-table.child-elna { - border-color: #f3bfd7; +/* Boutons d'action */ +.cl-gift-action-btn { + background: #f1f5f9; + border: 1px solid #cbd5e1; + width: 22px; + height: 22px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #475569; + font-size: 14px; + padding: 0; } -.cl-child-table.child-elna caption, -.cl-child-table.child-elna thead th { - background: #fdeaf3; - border-bottom-color: #f3bfd7; +.cl-gift-action-btn:hover { + background: white; + border-color: var(--text-main); +} +.cl-gift-delete:hover { + color: #dc2626; + border-color: #dc2626; + background: #fef2f2; } -.cl-child-table.child-bru { - border-color: #ffd0a8; +/* --- 8. BUDGET & TRICOUNT --- */ +.pf-section--panel { + margin-bottom: 32px; } -.cl-child-table.child-bru caption, -.cl-child-table.child-bru thead th { - background: #fff3e6; - border-bottom-color: #ffd0a8; +.pf-section--panel h2 { + font-size: 1.3rem; + margin-bottom: 16px; + border-left: 5px solid var(--primary); + padding-left: 12px; + color: var(--text-main); + background: white; + padding: 12px; + border-radius: 0 8px 8px 0; } -.cl-child-table.child-guim { - border-color: #d3c6ff; -} -.cl-child-table.child-guim caption, -.cl-child-table.child-guim thead th { - background: #f0eaff; - border-bottom-color: #d3c6ff; +.cl-budget-wrapper { + background: white; + border-radius: var(--radius-card); + box-shadow: var(--shadow-card); + border: 1px solid var(--border-light); + overflow-x: auto; } -/* ========================================================= */ -/* Modale d’ajout de cadeau */ -/* ========================================================= */ +.pf-table { + width: 100%; + border-collapse: collapse; +} +.pf-table th, +.pf-table td { + padding: 10px 14px; + border-bottom: 1px solid var(--border-light); + font-size: 0.9rem; +} +.pf-table th { + background: #f8fafc; + font-weight: 600; + text-align: left; + color: var(--text-muted); +} +.cl-debt-matrix td { + text-align: right; +} + +.cl-mtx-owe { + color: #dc2626; + background: #fef2f2; + font-weight: 700; +} + +/* --- 9. MODALES --- */ .cl-modal { display: none; } .cl-modal.cl-open { - display: block; + display: flex; + align-items: center; + justify-content: center; + position: fixed; + inset: 0; + z-index: 9999; } .cl-modal-backdrop { - position: fixed; + position: absolute; inset: 0; - background: rgba(17, 24, 39, 0.35); - z-index: 999; + background: rgba(15, 23, 42, 0.5); + backdrop-filter: blur(2px); } .cl-modal-dialog { - position: fixed; - z-index: 1000; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: min(520px, calc(100% - 24px)); - background: #fff; - border: 1px solid #d9e2ec; - border-radius: 8px; - box-shadow: 0 10px 30px rgba(17, 24, 39, 0.25); + position: relative; + background: white; + width: min(450px, 90vw); + border-radius: 12px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + z-index: 10; + padding: 0; + animation: modalPop 0.2s ease-out; +} + +@keyframes modalPop { + from { + transform: scale(0.95); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } } .cl-modal-form { - display: grid; - gap: 10px; - padding: 14px; + padding: 24px; + display: flex; + flex-direction: column; + gap: 12px; } .cl-modal-form h3 { - margin: 0 0 4px; - font-size: 16px; - color: #243b53; + margin: 0 0 12px; + font-size: 1.2rem; + color: var(--text-main); } .clm-label { - display: grid; - gap: 6px; - font-size: 12px; - color: #374151; + display: flex; + flex-direction: column; + gap: 4px; + font-weight: 600; + font-size: 0.85rem; + color: #475569; } .clm-label input, .clm-label select { - font-size: 12px; - padding: 6px 8px; - border: 1px solid #d9e2ec; + padding: 8px; + border: 1px solid #cbd5e1; border-radius: 6px; + font-size: 0.95rem; + background: #f8fafc; +} +.clm-label input:focus, +.clm-label select:focus { + background: white; + border-color: var(--primary); + outline: 2px solid rgba(37, 99, 235, 0.2); } .cl-modal-actions { display: flex; justify-content: flex-end; - gap: 8px; - margin-top: 4px; + gap: 10px; + margin-top: 10px; } -.clm-cancel, -.clm-ok { - font-size: 12px; - padding: 6px 12px; - border: 1px solid #d9e2ec; - border-radius: 6px; - background: #f0f4f8; - cursor: pointer; -} -.clm-ok { - font-weight: 600; - color: #1f2933; -} .clm-cancel { - color: #d03050; -} -.clm-cancel:hover, -.clm-ok:hover { - background: #d9e2ec; -} - -/* Prix: sans spinners, largeur fixe */ -#clm-amount { - width: 150px; - font-size: 11px; - padding: 2px 3px; - -moz-appearance: textfield; /* Firefox legacy */ - appearance: textfield; /* Standard property (modern browsers) */ -} -/* Chrome/Safari */ -#clm-amount::-webkit-outer-spin-button, -#clm-amount::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* Icônes d’action (éditer / supprimer) à côté du nom du cadeau */ -/* Conteneur de la ligne cadeau: réserve un espace à gauche pour les icônes overlay */ -.cl-gift-item { - position: relative; - padding-left: 2px; -} -/* Icônes en overlay coin haut-gauche */ -.cl-gift-actions--overlay { - position: absolute; - top: 2px; - left: 2px; - display: flex; - gap: 4px; - opacity: 0.65; /* discrètes */ - transition: opacity 120ms ease; -} -.cl-gift-item:hover .cl-gift-actions--overlay { - opacity: 1; /* plus visibles au survol */ -} - -.cl-gift-action-btn { - border: none; - background: transparent; - padding: 0; + background: white; + border: 1px solid #cbd5e1; + padding: 8px 16px; + border-radius: 6px; cursor: pointer; - color: #374151; - line-height: 1; + color: var(--text-muted); } -.cl-gift-action-btn svg { - width: 12px; - height: 12px; - display: block; -} -.cl-gift-delete { - color: #b91c1c; -} /* poubelle rouge */ - -/* ========================================================= */ -/* Résumé du budget et Liste détaillée */ -/* ========================================================= */ - -.cl-budget-wrapper { - margin-top: 4px; -} -.cl-budget-wrapper .pf-table { - font-size: 11px; -} -.cl-budget-wrapper .pf-table th, -.cl-budget-wrapper .pf-table td { - padding: 4px 4px; -} - -.cl-detail-wrapper .pf-table { - font-size: 11px; -} -.cl-detail-wrapper .pf-table th, -.cl-detail-wrapper .pf-table td { - padding: 4px 4px; -} - -/* Icône devant le titre de fête */ -.cl-occasion-title { - display: flex; - align-items: center; - gap: 8px; -} -.cl-occasion-icon { - width: 20px; - height: 20px; - object-fit: contain; - flex: 0 0 20px; - filter: none; /* garde les couleurs des SVG/PNG */ -} -/* clarté de lecture */ -.cl-debt-matrix .cl-matrix-corner { - font-size: 11px; - color: #6b7280; - text-align: left; - line-height: 1.2; - min-width: 92px; -} - -.cl-debt-matrix td.cl-mtx-diag { - background-image: repeating-linear-gradient( - 45deg, - #111827 0, - #111827 6px, - #1f2937 6px, - #1f2937 12px - ); - color: transparent; /* masque le “—” pour un rendu propre */ -} - -.cl-debt-matrix td.cl-mtx-owe { - background: #fff7ed; /* léger orange */ - color: #7c2d12; /* brun/orange foncé */ +.clm-ok { + background: var(--primary); + color: white; + border: none; + padding: 8px 20px; + border-radius: 6px; + cursor: pointer; font-weight: 600; } -.cl-debt-matrix td.cl-mtx-empty { - color: #9ca3af; -} - -/* Ligne cadeau: gauche (nom) / droite (prix ou icônes) */ -.cl-gift-line { - display: flex; - align-items: baseline; - width: 100%; -} -.cl-gift-desc, -.cl-gift-link { - flex: 1 1 auto; - min-width: 40px; -} - -/* Zone droite: prix visible par défaut; icônes masquées */ -.cl-gift-right { - position: relative; - min-width: 48px; /* réserve un petit espace */ - display: flex; - justify-content: flex-end; -} - -/* Prix */ -.cl-gift-amount { - margin-left: auto; - text-align: right; - color: #627d98; - white-space: nowrap; - transition: opacity 120ms ease; -} - -/* Icônes (même zone, positionnées à droite) */ -.cl-gift-actions { - position: absolute; - right: 0; - top: 50%; - transform: translateY(-50%); - display: flex; - gap: 6px; - opacity: 0; - pointer-events: none; - transition: opacity 120ms ease; -} - -/* Survol (desktop) ou état actif (mobile): cacher prix, montrer icônes */ -.cl-gift-right:hover .cl-gift-amount, -.cl-gift-right.is-active .cl-gift-amount, -.cl-gift-right:focus-within .cl-gift-amount { - opacity: 0; -} -.cl-gift-right:hover .cl-gift-actions, -.cl-gift-right.is-active .cl-gift-actions, -.cl-gift-right:focus-within .cl-gift-actions { - opacity: 1; - pointer-events: auto; -} - -/* Boutons icônes compacts et couleurs */ -.cl-gift-action-btn { - border: none; - background: transparent; - padding: 0; - cursor: pointer; - color: #374151; - line-height: 1; -} -.cl-gift-action-btn svg { - width: 14px; - height: 14px; - display: block; - color: inherit; - fill: currentColor; -} -.cl-gift-delete { - color: #b91c1c !important; -} -.cl-gift-action-btn:focus-visible { - outline: 2px solid #2563eb; - outline-offset: 2px; -} - -.cl-titlebar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; -} -.cl-view-switch { - display: flex; - gap: 8px; -} -.cl-view-btn { - padding: 6px 10px; - border: 1px solid #ddd; - border-radius: 6px; - text-decoration: none; - color: #333; - background: #f7f7f7; -} -.cl-view-btn.is-active { - background: #333; - color: #fff; - border-color: #333; -} - -/* Centrage fiable du + dans le cercle (micro-nudge du cercle) */ -.cl-child-add-btn { - display: flex; - align-items: center; - justify-content: center; - line-height: 1; -} - -/* On décale très légèrement le cercle vers le bas pour compenser l’optique du glyphe + */ -.cl-child-add-btn::before { - transform: translate(-50%, -50%) translateY(0.5px); -} - -/* Si nécessaire sur certains mobiles, augmente le nudge */ -@media (pointer: coarse) { - .cl-child-add-btn::before { - transform: translate(-50%, -50%) translateY(1px); +/* --- 10. MOBILE --- */ +@media (max-width: 768px) { + .cl-view-nadal .cl-occasion-children-tables, + .cl-view-anniversary .cl-occasion-children-tables { + grid-template-columns: 1fr; } -} -/* === Gift List: Mobile Comfort v2 === */ -@media (pointer: coarse), (max-width: 780px) { - .pf-gift-list h1 { - font-size: clamp(24px, 7vw, 30px); - margin: 10px 0 6px; - line-height: 1.2; - } - .pf-gift-list h2 { - font-size: clamp(20px, 5.6vw, 24px); - line-height: 1.25; - margin: 10px 0 6px; - } - .pf-gift-list h3 { - font-size: clamp(17px, 4.8vw, 20px); - line-height: 1.3; - margin: 8px 0 4px; + .cl-view-anniversary .child-pol, + .cl-view-anniversary .child-pep, + .cl-view-anniversary .child-elna, + .cl-view-anniversary .child-bru, + .cl-view-anniversary .child-guim { + grid-column: span 1; } - /* Switch de vue plus gros et confortable */ - .cl-view-btn { - font-size: 18px; - padding: 12px 14px; - min-height: 48px; - border-radius: 10px; + /* Force le scroll horizontal sur mobile uniquement */ + .cl-child-table { + display: block; + overflow-x: auto; } - - /* Grille/listes: un peu plus d’air */ - .cl-occasion-children-tables { - gap: 12px; - } - - /* Tableaux enfant: en-têtes et cellules plus lisibles */ - .cl-child-table thead th { - padding: 10px 10px; - font-size: 13.5px; - } - .cl-child-table tbody td { - padding: 10px 8px; - font-size: 13.5px; - } - .cl-summary-adult-total { - font-size: 13px; - } - - /* Bouton + : nous avons déjà agrandi le hit-area; on garde les réglages précédents */ - .cl-child-table caption { - padding-right: 72px; - } - - /* Tables Budget/Tricount/Detail: un cran au‑dessus aussi */ - .cl-budget-wrapper .pf-table, - .cl-detail-wrapper .pf-table { - font-size: 13.5px; - } - .cl-budget-wrapper .pf-table th, - .cl-budget-wrapper .pf-table td, - .cl-detail-wrapper .pf-table th, - .cl-detail-wrapper .pf-table td { - padding: 8px 8px; + /* On redonne de la largeur aux colonnes sur mobile */ + .cl-child-table th, + .cl-child-table td { + min-width: 120px; } } diff --git a/modules/gift-list/save-gift.php b/modules/gift-list/save-gift.php index 93f8f86..d2289be 100644 --- a/modules/gift-list/save-gift.php +++ b/modules/gift-list/save-gift.php @@ -1,73 +1,95 @@ 0) { - $stmt = $pdo->prepare("DELETE FROM pf_gift_gifts WHERE id = :id"); + $stmt = $pdo->prepare("DELETE FROM {$tableName} WHERE id = :id"); $stmt->execute(['id' => $gift_id]); } - - header('Location: /gift-list.php'); + header("Location: $redirectUrl"); exit; } // Champs communs - $year = (int)($_POST['year'] ?? date('Y')); - $adult_name = trim($_POST['adult_name'] ?? ''); - $payer_name = trim($_POST['payer_name'] ?? ''); // nouveau champ - $child_name = trim($_POST['child_name'] ?? ''); - $occasion = trim($_POST['occasion'] ?? ''); - $gift_desc = trim($_POST['gift_description'] ?? ''); - $product_link = trim($_POST['product_link'] ?? ''); - $amount = $_POST['amount'] !== '' ? (float)$_POST['amount'] : 0.0; + $year = (int)($_POST['year'] ?? date('Y')); + $adult_name = trim($_POST['adult_name'] ?? ''); + $payer_name = trim($_POST['payer_name'] ?? ''); + $child_name = trim($_POST['child_name'] ?? ''); + $occasion = trim($_POST['occasion'] ?? ''); + $gift_desc = trim($_POST['gift_description'] ?? ''); + $prod_link = trim($_POST['product_link'] ?? ''); + $amount = ($_POST['amount'] !== '') ? (float)$_POST['amount'] : 0.0; + // Si payeur vide, c'est l'adulte responsable qui paye if ($payer_name === '') { $payer_name = $adult_name; } - if ($action === 'update') { - $gift_id = (int)($_POST['gift_id'] ?? 0); - if ($gift_id > 0 && $adult_name && $payer_name && $child_name && $occasion && $gift_desc) { - $stmt = $pdo->prepare(" - UPDATE pf_gift_gifts - SET year = :year, - adult_name = :adult_name, - payer_name = :payer_name, - child_name = :child_name, - occasion = :occasion, - gift_description = :gift_description, - product_link = :product_link, - amount = :amount - WHERE id = :id - "); - $stmt->execute([ - 'id' => $gift_id, - 'year' => $year, - 'adult_name' => $adult_name, - 'payer_name' => $payer_name, - 'child_name' => $child_name, - 'occasion' => $occasion, - 'gift_description' => $gift_desc, - 'product_link' => $product_link ?: null, - 'amount' => $amount, - ]); - } - - header('Location: /gift-list.php'); + // Validation minimale + if (!$adult_name || !$child_name || !$occasion || !$gift_desc) { + // En cas d'erreur, on redirige sans rien faire (ou on pourrait gérer une erreur) + header("Location: $redirectUrl"); exit; } - // create (par défaut) - if ($adult_name && $payer_name && $child_name && $occasion && $gift_desc) { + // --- UPDATE --- + if ($action === 'update' && $gift_id > 0) { $stmt = $pdo->prepare(" - INSERT INTO pf_gift_gifts + UPDATE {$tableName} + SET year = :year, + adult_name = :adult_name, + payer_name = :payer_name, + child_name = :child_name, + occasion = :occasion, + gift_description = :gift_description, + product_link = :product_link, + amount = :amount + WHERE id = :id + "); + $stmt->execute([ + 'id' => $gift_id, + 'year' => $year, + 'adult_name' => $adult_name, + 'payer_name' => $payer_name, + 'child_name' => $child_name, + 'occasion' => $occasion, + 'gift_description' => $gift_desc, + 'product_link' => $prod_link ?: null, + 'amount' => $amount, + ]); + } + // --- CREATE --- + else { + $stmt = $pdo->prepare(" + INSERT INTO {$tableName} (year, adult_name, payer_name, child_name, occasion, gift_description, product_link, amount) VALUES (:year, :adult_name, :payer_name, :child_name, :occasion, :gift_description, :product_link, :amount) @@ -79,11 +101,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 'child_name' => $child_name, 'occasion' => $occasion, 'gift_description' => $gift_desc, - 'product_link' => $product_link ?: null, + 'product_link' => $prod_link ?: null, 'amount' => $amount, ]); } - header('Location: /gift-list.php'); - exit; +} catch (PDOException $e) { + // Log l'erreur si besoin } + +header("Location: $redirectUrl"); +exit; \ No newline at end of file diff --git a/modules/holidays/geocode.php b/modules/holidays/geocode.php index c2843fb..493d7b4 100644 --- a/modules/holidays/geocode.php +++ b/modules/holidays/geocode.php @@ -6,100 +6,142 @@ require __DIR__ . '/../../includes/db.php'; header('Content-Type: application/json; charset=utf-8'); +// 1. Validation de l'entrée $q = trim($_GET['q'] ?? ''); if ($q === '') { - http_response_code(400); - echo json_encode(['error' => 'missing q']); - exit; + http_response_code(400); + echo json_encode(['error' => 'missing_q']); + exit; } -// borne la limite entre 1 et 5 (usage perso) +// Borner la limite $limit = (int)($_GET['limit'] ?? 1); if ($limit < 1) $limit = 1; -if ($limit > 5) $limit = 5; +if ($limit > 10) $limit = 10; // Nominatim bloque souvent au-dessus de 10-50 -// Cache local (silencieux si table absente) +// 2. Normalisation pour le cache $qNorm = mb_strtolower($q); $qHash = hash('sha256', $qNorm); + +// 3. Création automatique de la table cache si elle n'existe pas (Sécurité) +try { + $pdo->exec(" + CREATE TABLE IF NOT EXISTS pf_geocode_cache ( + q_hash CHAR(64) PRIMARY KEY, + q VARCHAR(255), + lat DECIMAL(10, 7), + lng DECIMAL(10, 7), + display_name TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + "); +} catch (Throwable $e) { + // On continue même si ça échoue (l'admin devra créer la table manuellement) +} + +// 4. Vérification du cache (Même si limit > 1) +// Stratégie : Si on a déjà cherché exactement "Paris", on renvoie le résultat stocké +// pour économiser l'API et aller plus vite. Le JS gérera le résultat unique. try { - if ($limit === 1) { $st = $pdo->prepare("SELECT lat, lng, display_name FROM pf_geocode_cache WHERE q_hash = ?"); $st->execute([$qHash]); if ($row = $st->fetch(PDO::FETCH_ASSOC)) { - echo json_encode([ - 'lat' => (float)$row['lat'], - 'lng' => (float)$row['lng'], - 'display_name' => $row['display_name'], - 'cached' => true - ]); - exit; + echo json_encode([ + 'lat' => (float)$row['lat'], + 'lng' => (float)$row['lng'], + 'display_name' => $row['display_name'], + 'cached' => true + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; } - } } catch (Throwable $e) { - // pas bloquant + // Erreur SQL silencieuse sur le cache } -// Appel Nominatim (respect des règles d'usage) +// 5. Appel Nominatim (Si pas en cache) $endpoint = 'https://nominatim.openstreetmap.org/search'; $params = http_build_query([ - 'format' => 'jsonv2', - 'addressdetails' => 1, - 'limit' => $limit, - 'q' => $q, + 'format' => 'jsonv2', + 'addressdetails' => 1, + 'limit' => $limit, + 'q' => $q, ], '', '&', PHP_QUERY_RFC3986); $url = $endpoint . '?' . $params; $ch = curl_init($url); curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_CONNECTTIMEOUT => 5, - CURLOPT_TIMEOUT => 10, - CURLOPT_HTTPHEADER => [ - // Contact fourni: ferlan.alexandre@gmail.com - 'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)' - ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => [ + // Ton User-Agent est correct. + // Important : Nominatim demande une identification claire. + 'User-Agent: PachaFamily-Holidays/1.0 (+contact: ferlan.alexandre@gmail.com)' + ], ]); $body = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); +// Gestion erreur cURL / HTTP if ($body === false || $http !== 200) { - http_response_code(502); - echo json_encode(['error' => 'geocode_failed', 'details' => $err ?: ('HTTP '.$http)]); - exit; + http_response_code(502); // Bad Gateway + echo json_encode([ + 'error' => 'geocode_failed', + 'details' => $err ?: ('HTTP ' . $http) + ]); + exit; } $data = json_decode($body, true); + +// Gestion erreur JSON ou vide if (!is_array($data) || empty($data)) { - http_response_code(404); - echo json_encode(['error' => 'not_found']); - exit; + // On renvoie 200 avec une liste vide ou 404, le JS gère les deux. + // 404 est plus sémantique "Not Found". + http_response_code(404); + echo json_encode(['error' => 'not_found']); + exit; } +// 6. Mise en cache du PREMIER résultat (Le "meilleur") +// On ne cache que le top result pour simplifier la structure de la DB. +if (isset($data[0])) { + $r = $data[0]; + $lat = round((float)$r['lat'], 6); + $lng = round((float)$r['lon'], 6); + $display = $r['display_name'] ?? ''; + + try { + $st = $pdo->prepare(" + INSERT INTO pf_geocode_cache (q_hash, q, lat, lng, display_name, updated_at) + VALUES (?, ?, ?, ?, ?, NOW()) + ON DUPLICATE KEY UPDATE lat=VALUES(lat), lng=VALUES(lng), display_name=VALUES(display_name), updated_at=NOW() + "); + $st->execute([$qHash, $q, $lat, $lng, $display]); + } catch (Throwable $e) {} +} + +// 7. Retour des résultats +// Si limit=1, on renvoie format plat (pour compatibilité stricte) +// Si limit>1, on renvoie format liste if ($limit === 1) { - $r = $data[0]; - $lat = round((float)$r['lat'], 6); - $lng = round((float)$r['lon'], 6); - $display = $r['display_name'] ?? null; + $r = $data[0]; + echo json_encode([ + 'lat' => round((float)$r['lat'], 6), + 'lng' => round((float)$r['lon'], 6), + 'display_name' => $r['display_name'] ?? '' + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +} else { + $results = array_map(function ($r) { + return [ + 'lat' => round((float)$r['lat'], 6), + 'lng' => round((float)$r['lon'], 6), + 'display_name' => (string)($r['display_name'] ?? ''), + ]; + }, $data); - try { - $st = $pdo->prepare("REPLACE INTO pf_geocode_cache (q_hash, q, lat, lng, display_name) VALUES (?, ?, ?, ?, ?)"); - $st->execute([$qHash, $q, $lat, $lng, $display]); - } catch (Throwable $e) {} - - echo json_encode(['lat' => $lat, 'lng' => $lng, 'display_name' => $display]); - exit; -} - -// Multi-résultats -$results = array_map(function ($r) { - return [ - 'lat' => round((float)$r['lat'], 6), - 'lng' => round((float)$r['lon'], 6), - 'display_name' => (string)($r['display_name'] ?? ''), - ]; -}, $data); - -echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + echo json_encode(['results' => $results], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); +} \ No newline at end of file diff --git a/modules/holidays/holidays.css b/modules/holidays/holidays.css index fb533f9..705bc6f 100644 --- a/modules/holidays/holidays.css +++ b/modules/holidays/holidays.css @@ -1,604 +1,570 @@ -/* === Holidays === */ +/* modules/holidays/holidays.css */ -/* Titres et paragraphes (alignés sur pf-gift-list) */ +/* --- 1. VARIABLES & BASE --- */ +:root { + --primary: #2563eb; /* Bleu vibrant */ + --primary-hover: #1d4ed8; + --bg-page: #f1f5f9; /* Gris-bleu très pâle */ + --bg-card: #ffffff; + --text-main: #1e293b; /* Gris très foncé (pas noir) */ + --text-muted: #64748b; + --radius-l: 16px; + --radius-m: 10px; + --radius-s: 6px; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --shadow-lg: + 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --shadow-hover: + 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); +} + +.pf-holidays { + background-color: var(--bg-page); + font-family: + "Segoe UI", + system-ui, + -apple-system, + sans-serif; + color: var(--text-main); +} + +/* Titres */ .pf-holidays h1 { - margin-bottom: 4px; -} -.pf-holidays p { - margin-top: 0; - margin-bottom: 12px; - font-size: 13px; - color: #4b5563; -} -/* Petit texte explicatif (même classe que gift-list pour cohérence) */ -.cl-legend { - font-size: 12px; - color: #6b7280; - margin-bottom: 8px; + font-size: 1.8rem; + font-weight: 800; + letter-spacing: -0.025em; + color: #0f172a; + margin-bottom: 0.5rem; } -/* Titlebar (cohérent avec gift-list .cl-titlebar) */ +.pf-holidays p { + font-size: 0.95rem; + color: var(--text-muted); + line-height: 1.5; +} + +/* --- 2. EN-TÊTE (TITLEBAR) --- */ .pf-holidays__titlebar { + background: transparent; + padding-bottom: 1rem; + margin-bottom: 2rem; + border-bottom: 1px solid #e2e8f0; display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; - gap: 12px; - flex-wrap: wrap; + gap: 1rem; } + .hol-title-actions { display: flex; - gap: 8px; + gap: 12px; } -/* Boutons (proches de .cl-view-btn) */ +/* --- 3. BOUTONS MODERNES --- */ .btn, .hol-add-btn, .hol-map-toggle { - padding: 8px 12px; - border: 1px solid #d9e2ec; - border-radius: 8px; - background: #f0f4f8; - color: #243b53; - text-decoration: none; + border: none; + border-radius: 50px; /* Pill shape */ + padding: 10px 20px; + font-size: 0.9rem; + font-weight: 600; cursor: pointer; - font-size: 13px; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + box-shadow: var(--shadow-sm); +} + +/* Bouton principal (Ajouter) */ +.hol-add-btn { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: white; +} +.hol-add-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); +} + +/* Bouton Carte */ +.hol-map-toggle { + background: white; + color: #0f766e; + border: 1px solid #ccfbf1; } -.btn:hover, -.hol-add-btn:hover, .hol-map-toggle:hover { - background: #e5eef5; + background: #f0fdfa; + border-color: #99f6e4; + color: #0d9488; +} + +/* Boutons classiques */ +.btn { + background: white; + color: var(--text-main); + border: 1px solid #cbd5e1; +} +.btn:hover { + background: #f8fafc; + border-color: #94a3b8; +} + +/* Actions contextuelles (edit/delete) dans les cards */ +.btn-edit, +.btn-delete { + border-radius: var(--radius-s); + padding: 6px 12px; + font-size: 0.8rem; + box-shadow: none; } .btn-edit { - background: #e0f2fe; - border-color: #93c5fd; - color: #0f4c81; -} -.btn-delete { - background: #fee2e2; - border-color: #fca5a5; - color: #7c2d12; -} - -/* Cards */ -.hol-idea-card { - background: #fff; - border: 1px solid #d9e2ec; - border-radius: 8px; - padding: 12px; -} -.hol-idea-card__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; -} -.hol-card-actions { - display: flex; - gap: 6px; - margin-top: 8px; -} - -/* Statuts (match gift-list badges ton léger) */ -.hol-status { - font-size: 11px; - padding: 3px 6px; - border-radius: 12px; - background: #e5e7eb; -} -.hol-status--favorite { - background: #fde68a; -} -.hol-status--shortlist { - background: #bfdbfe; -} -.hol-status--planned { - background: #bbf7d0; -} - -/* === Modales (alignement visuel avec cl-modal de gift-list) === */ -.hol-modal { - display: none; -} -.hol-modal.open { - display: block; -} - -.hol-backdrop { - position: fixed; - inset: 0; - background: rgba(17, 24, 39, 0.35); /* même teinte que gift-list */ - z-index: 999; -} - -.hol-dialog { - position: fixed; - z-index: 1000; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: min( - 560px, - calc(100% - 24px) - ); /* légèrement plus large que gift-list */ - background: #fff; - border: 1px solid #d9e2ec; - border-radius: 8px; - box-shadow: 0 10px 30px rgba(17, 24, 39, 0.25); /* même shadow */ -} - -/* Modale carte (grande largeur) */ -.hol-dialog--map { - width: min(1200px, calc(100% - 24px)); -} - -.hol-map-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - border-bottom: 1px solid #e5e7eb; -} - -/* Formulaire modale (alignement gift-list) */ -.hol-form { - display: grid; - gap: 10px; - padding: 14px; -} -.hol-form h3 { - margin: 0 0 4px; - font-size: 16px; - color: #243b53; -} - -.hol-inline { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.hol-form label { - display: grid; - gap: 6px; - font-size: 12px; - color: #374151; -} - -.hol-form input, -.hol-form select, -.hol-form textarea { - font-size: 12px; - padding: 6px 8px; - border: 1px solid #d9e2ec; - border-radius: 6px; - background: #ffffff; - color: #111827; -} - -/* Actions modale */ -.hol-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 4px; -} -.hol-cancel, -.hol-ok { - font-size: 12px; - padding: 6px 12px; - border: 1px solid #d9e2ec; - border-radius: 6px; - background: #f0f4f8; - cursor: pointer; -} -.hol-ok { - font-weight: 600; - color: #1f2933; -} -.hol-cancel { - color: #d03050; -} -.hol-cancel:hover, -.hol-ok:hover { - background: #d9e2ec; -} - -/* Focus accessible */ -.hol-form input:focus-visible, -.hol-form select:focus-visible, -.hol-form textarea:focus-visible, -.hol-cancel:focus-visible, -.hol-ok:focus-visible { - outline: 2px solid #2563eb; - outline-offset: 2px; -} - -/* === Picker multi-résultats Géocode (harmonisé) === */ -.hol-geocode-picker { - margin-top: 8px; - background: #fff; - border: 1px solid #d9e2ec; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08); - overflow: hidden; -} -.hol-geocode-picker__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 10px; - background: #f8fafc; - border-bottom: 1px solid #e5e7eb; - font-size: 13px; - font-weight: 600; - color: #243b53; -} -.hol-geocode-picker__close { + background: #f1f5f9; + color: #475569; border: none; - background: transparent; - cursor: pointer; - font-size: 18px; - line-height: 1; - padding: 2px 6px; } -.hol-geocode-picker__list { - list-style: none; - margin: 0; - padding: 6px; - max-height: 260px; - overflow: auto; -} -.hol-geocode-picker__item { - display: grid; - grid-template-columns: 1fr auto auto; - align-items: center; - gap: 8px; - padding: 6px; - border-radius: 6px; -} -.hol-geocode-picker__item + .hol-geocode-picker__item { - margin-top: 4px; -} -.hol-geocode-picker__label { - font-size: 12px; - color: #111827; -} -.hol-geocode-picker__coords { - font-size: 11px; - color: #6b7280; -} -.hol-geocode-picker__pick { - padding: 6px 10px; - border: 1px solid #d9e2ec; - border-radius: 6px; - background: #f0f4f8; - cursor: pointer; - font-size: 12px; +.btn-edit:hover { + background: #e2e8f0; + color: #1e293b; } -/* === Mobile Comfort (aligné gift-list) === */ -@media (pointer: coarse), (max-width: 780px) { - .pf-holidays h1 { - font-size: clamp(24px, 7vw, 30px); - margin: 10px 0 6px; - line-height: 1.2; - } - .pf-holidays h2 { - font-size: clamp(20px, 5.6vw, 24px); - line-height: 1.25; - margin: 10px 0 6px; - } - .pf-holidays h3 { - font-size: clamp(17px, 4.8vw, 20px); - line-height: 1.3; - margin: 8px 0 4px; - } - - .btn, - .hol-add-btn, - .hol-map-toggle { - font-size: 18px; - padding: 12px 14px; - min-height: 48px; - border-radius: 10px; - } - - .hol-form input, - .hol-form select, - .hol-form textarea, - .hol-cancel, - .hol-ok { - font-size: 16px; /* évite zoom iOS */ - min-height: 44px; - } - - .hol-dialog { - width: min(640px, calc(100% - 24px)); - } +.btn-delete { + background: #fef2f2; + color: #dc2626; + border: none; +} +.btn-delete:hover { + background: #fee2e2; + color: #b91c1c; } -/* Cards: style proche gift-list */ +/* --- 4. GRILLE & CARTES (Le cœur du design) --- */ .hol-ideas-grid { display: grid; - grid-template-columns: repeat(3, minmax(260px, 1fr)); - gap: 10px; -} -@media (max-width: 1100px) { - .hol-ideas-grid { - grid-template-columns: repeat(2, minmax(240px, 1fr)); - } -} -@media (max-width: 700px) { - .hol-ideas-grid { - grid-template-columns: 1fr; - } + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 24px; + margin-bottom: 40px; } .hol-idea-card { - background: #fff; - border: 1px solid #d9e2ec; - border-radius: 8px; - padding: 12px; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + background: var(--bg-card); + border-radius: var(--radius-l); + box-shadow: var(--shadow-md); + padding: 20px; + display: flex; + flex-direction: column; transition: - box-shadow 120ms ease, - transform 120ms ease; + transform 0.25s ease, + box-shadow 0.25s ease; + border: 1px solid transparent; /* Pour éviter le saut au hover */ + position: relative; + overflow: hidden; } + +/* Effet au survol : la carte "flotte" */ .hol-idea-card:hover { - box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08); - transform: translateY(-1px); + transform: translateY(-5px); + box-shadow: var(--shadow-hover); + border-color: #e2e8f0; } + +/* Bande de couleur décorative en haut de carte selon statut (Optionnel mais joli) */ +.hol-idea-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: #e2e8f0; /* Default */ +} +.hol-idea-card:has(.hol-status--planned)::before { + background: #22c55e; +} +.hol-idea-card:has(.hol-status--favorite)::before { + background: #f59e0b; +} +.hol-idea-card:has(.hol-status--shortlist)::before { + background: #3b82f6; +} + .hol-idea-card__head { display: flex; - align-items: center; justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.hol-idea-card h3 { + margin: 0; + font-size: 1.15rem; + font-weight: 700; + line-height: 1.3; + color: var(--text-main); + margin-right: 8px; +} + +.hol-idea-meta { + font-size: 0.9rem; + color: var(--text-muted); + margin-bottom: 12px; + display: flex; + flex-direction: column; + gap: 4px; +} + +/* Notes stylisées */ +.hol-notes { + background: #fffbeb; /* Jaune très pâle, style post-it */ + color: #78350f; + padding: 10px 12px; + border-radius: var(--radius-m); + font-size: 0.85rem; + font-style: italic; + margin-bottom: 16px; + flex-grow: 1; + border-left: 3px solid #fcd34d; +} + +.hol-card-actions { + margin-top: auto; + display: flex; + gap: 8px; + padding-top: 16px; + border-top: 1px solid #f1f5f9; +} + +/* Statuts (Badges Modernes) */ +.hol-status { + padding: 4px 10px; + border-radius: 20px; + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.hol-status--planned { + background: #dcfce7; + color: #15803d; +} /* Vert */ +.hol-status--favorite { + background: #fff7ed; + color: #c2410c; + border: 1px solid #ffedd5; +} /* Orange */ +.hol-status--shortlist { + background: #eff6ff; + color: #1d4ed8; +} /* Bleu */ +.hol-status--draft { + background: #f1f5f9; + color: #64748b; +} /* Gris */ +.hol-status--archived { + background: #f8fafc; + color: #94a3b8; + text-decoration: line-through; +} + +/* Style spécifique pour les archivés */ +.hol-idea-card--archived { + background: #f8fafc; + box-shadow: none; + border: 1px solid #e2e8f0; + opacity: 0.8; +} + +/* --- 5. VUE DÉTAIL (Panels) --- */ +.pf-section--panel { + background: white; + border-radius: var(--radius-l); + box-shadow: var(--shadow-sm); + padding: 24px; + margin-bottom: 24px; + border: 1px solid #f1f5f9; +} + +.pf-section--panel h2 { + font-size: 1.25rem; + color: var(--text-main); + margin-bottom: 16px; + display: flex; + align-items: center; gap: 8px; } -.hol-idea-meta, -.hol-tags, -.hol-notes { - font-size: 12px; - color: #374151; - margin: 6px 0; +/* Petite ligne décorative sous le titre h2 */ +.pf-section--panel h2::after { + content: ""; + flex: 1; + height: 1px; + background: #e2e8f0; + margin-left: 12px; } -.hol-card-actions { + +.hol-list li { + padding: 12px 0; + border-bottom: 1px solid #f1f5f9; display: flex; - gap: 6px; - margin-top: 8px; + align-items: center; + flex-wrap: wrap; + gap: 8px; } -.hol-idea-card--archived { - opacity: 0.85; +.hol-list li:last-child { + border-bottom: none; } -/* Boutons cohérents */ -.btn, -.hol-add-btn, -.hol-map-toggle { +.hol-budget-summary { + background: linear-gradient(to right, #eff6ff, #ffffff); + border: 1px solid #dbeafe; + color: #1e40af; + padding: 16px; + border-radius: var(--radius-m); + font-weight: 600; + display: flex; + justify-content: space-between; + margin-bottom: 16px; +} + +/* Formulaires Inline (Détail) */ +.hol-inline-form { + background: #f8fafc; + padding: 12px; + border-radius: var(--radius-m); + border: 1px dashed #cbd5e1; + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + margin-bottom: 20px; +} +.hol-inline-form input, +.hol-inline-form select { + border: 1px solid #cbd5e1; + border-radius: var(--radius-s); padding: 8px 12px; - border: 1px solid #d9e2ec; - border-radius: 8px; - background: #f0f4f8; - color: #243b53; - text-decoration: none; + font-size: 0.9rem; + transition: border-color 0.2s; +} +.hol-inline-form input:focus, +.hol-inline-form select:focus { + border-color: var(--primary); + outline: 2px solid rgba(37, 99, 235, 0.1); +} +.hol-inline-form button { + background: #334155; + color: white; + border: none; + padding: 8px 16px; + border-radius: var(--radius-s); cursor: pointer; - font-size: 13px; } -.btn:hover, -.hol-add-btn:hover, -.hol-map-toggle:hover { - background: #e5eef5; -} -.btn-edit { - background: #e0f2fe; - border-color: #93c5fd; - color: #0f4c81; -} -.btn-delete { - background: #fee2e2; - border-color: #fca5a5; - color: #7c2d12; +.hol-inline-form button:hover { + background: #1e293b; } -/* Modale: harmonisée avec gift-list */ +/* --- 6. MODALES MODERNES (Glassmorphism léger) --- */ .hol-modal { display: none; } .hol-modal.open { - display: block; -} -.hol-backdrop { - position: fixed; - inset: 0; - background: rgba(17, 24, 39, 0.35); - z-index: 999; -} -.hol-dialog { - position: fixed; - z-index: 1000; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: min(560px, calc(100% - 24px)); - background: #fff; - border: 1px solid #d9e2ec; - border-radius: 8px; - box-shadow: 0 10px 30px rgba(17, 24, 39, 0.25); -} -.hol-dialog--map { - width: min(1200px, calc(100% - 24px)); -} -.hol-map-header { display: flex; align-items: center; - justify-content: space-between; - padding: 8px 12px; - border-bottom: 1px solid #e5e7eb; + justify-content: center; + position: fixed; + inset: 0; + z-index: 9999; } +.hol-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.6); /* Plus sombre */ + backdrop-filter: blur(4px); /* Flou d'arrière plan */ +} + +.hol-dialog { + position: relative; + background: white; + width: min(600px, 90vw); + max-height: 90vh; + overflow-y: auto; + border-radius: 20px; /* Très arrondi */ + box-shadow: var(--shadow-lg); + z-index: 10; + animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes modalPop { + from { + transform: scale(0.95) translateY(10px); + opacity: 0; + } + to { + transform: scale(1) translateY(0); + opacity: 1; + } +} + +/* Modale Carte */ +.hol-dialog--map { + width: 95vw; + height: 90vh; + max-width: 1400px; + padding: 0; + display: flex; + flex-direction: column; +} + +/* Formulaires Modale */ .hol-form { - display: grid; - gap: 10px; - padding: 14px; + padding: 32px; + display: flex; + flex-direction: column; + gap: 16px; } .hol-form h3 { - margin: 0 0 4px; - font-size: 16px; - color: #243b53; -} -.hol-inline { - display: flex; - flex-wrap: wrap; - gap: 8px; + font-size: 1.5rem; + margin-bottom: 12px; + color: var(--text-main); } .hol-form label { - display: grid; - gap: 6px; - font-size: 12px; - color: #374151; + font-weight: 600; + font-size: 0.85rem; + color: #475569; + margin-bottom: 4px; } .hol-form input, .hol-form select, .hol-form textarea { - font-size: 12px; - padding: 6px 8px; - border: 1px solid #d9e2ec; - border-radius: 6px; - background: #fff; - color: #111827; + width: 100%; + padding: 10px 12px; + border: 1px solid #cbd5e1; + border-radius: var(--radius-m); + font-size: 1rem; + background: #f8fafc; + box-sizing: border-box; /* Important */ } -.hol-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 4px; -} -.hol-cancel, -.hol-ok { - font-size: 12px; - padding: 6px 12px; - border: 1px solid #d9e2ec; - border-radius: 6px; - background: #f0f4f8; - cursor: pointer; -} -.hol-ok { - font-weight: 600; - color: #1f2933; -} -.hol-cancel { - color: #d03050; -} -.hol-cancel:hover, -.hol-ok:hover { - background: #d9e2ec; -} -.hol-form input:focus-visible, -.hol-form select:focus-visible, -.hol-form textarea:focus-visible, -.hol-cancel:focus-visible, -.hol-ok:focus-visible { - outline: 2px solid #2563eb; - outline-offset: 2px; +.hol-form input:focus, +.hol-form select:focus, +.hol-form textarea:focus { + background: white; + border-color: var(--primary); + outline: 4px solid rgba(37, 99, 235, 0.1); } -/* Geocode picker harmonisé */ +.hol-inline { + display: flex; + gap: 16px; +} +.hol-inline > label { + flex: 1; +} + +.hol-actions { + margin-top: 16px; + display: flex; + justify-content: flex-end; + gap: 12px; +} +.hol-cancel { + background: transparent; + color: var(--text-muted); + border: none; + font-weight: 600; +} +.hol-cancel:hover { + color: var(--text-main); + background: #f1f5f9; +} + +.hol-ok { + background: var(--primary); + color: white; + border: none; + padding: 10px 24px; + border-radius: 50px; + font-weight: 600; + box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2); +} +.hol-ok:hover { + background: var(--primary-hover); + transform: translateY(-1px); +} + +/* --- 7. PICKER GEOCODE --- */ .hol-geocode-picker { + background: white; + border: 1px solid #e2e8f0; + border-radius: var(--radius-m); + box-shadow: var(--shadow-lg); margin-top: 8px; - background: #fff; - border: 1px solid #d9e2ec; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08); overflow: hidden; } .hol-geocode-picker__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 10px; background: #f8fafc; - border-bottom: 1px solid #e5e7eb; - font-size: 13px; + padding: 8px 12px; font-weight: 600; - color: #243b53; -} -.hol-geocode-picker__close { - border: none; - background: transparent; - cursor: pointer; - font-size: 18px; - line-height: 1; - padding: 2px 6px; -} -.hol-geocode-picker__list { - list-style: none; - margin: 0; - padding: 6px; - max-height: 260px; - overflow: auto; + font-size: 0.85rem; + display: flex; + justify-content: space-between; } .hol-geocode-picker__item { - display: grid; - grid-template-columns: 1fr auto auto; - align-items: center; - gap: 8px; - padding: 6px; - border-radius: 6px; -} -.hol-geocode-picker__item + .hol-geocode-picker__item { - margin-top: 4px; -} -.hol-geocode-picker__label { - font-size: 12px; - color: #111827; -} -.hol-geocode-picker__coords { - font-size: 11px; - color: #6b7280; -} -.hol-geocode-picker__pick { - padding: 6px 10px; - border: 1px solid #d9e2ec; - border-radius: 6px; - background: #f0f4f8; + padding: 10px 12px; + border-bottom: 1px solid #f1f5f9; cursor: pointer; - font-size: 12px; + display: flex; + justify-content: space-between; + align-items: center; +} +.hol-geocode-picker__item:hover { + background: #f0f9ff; +} +.hol-geocode-picker__item button { + background: white; + border: 1px solid #cbd5e1; + border-radius: 4px; + font-size: 0.75rem; + padding: 4px 8px; } -/* Mobile comfort (match gift-list) */ -@media (pointer: coarse), (max-width: 780px) { +/* --- 8. MOBILE RESPONSIVE --- */ +@media (max-width: 768px) { + .hol-inline { + flex-direction: column; + gap: 12px; + } + .hol-inline-form { + flex-direction: column; + align-items: stretch; + } + .hol-inline-form button { + margin-top: 8px; + width: 100%; + } + .pf-holidays h1 { - font-size: clamp(24px, 7vw, 30px); - margin: 10px 0 6px; - line-height: 1.2; + font-size: 1.5rem; } - .pf-holidays h2 { - font-size: clamp(20px, 5.6vw, 24px); - line-height: 1.25; - margin: 10px 0 6px; + + .hol-idea-card { + padding: 16px; } - .pf-holidays h3 { - font-size: clamp(17px, 4.8vw, 20px); - line-height: 1.3; - margin: 8px 0 4px; + .hol-ideas-grid { + gap: 16px; } .btn, .hol-add-btn, .hol-map-toggle { - font-size: 18px; - padding: 12px 14px; - min-height: 48px; - border-radius: 10px; + width: 100%; /* Boutons pleine largeur sur mobile */ + margin-bottom: 8px; } - .hol-form input, - .hol-form select, - .hol-form textarea, - .hol-cancel, - .hol-ok { - font-size: 16px; - min-height: 44px; - } - .hol-dialog { - width: min(640px, calc(100% - 24px)); + .hol-title-actions { + flex-direction: column; + width: 100%; + gap: 4px; } } diff --git a/modules/holidays/holidays.js b/modules/holidays/holidays.js index 8147a2b..85651de 100644 --- a/modules/holidays/holidays.js +++ b/modules/holidays/holidays.js @@ -1,107 +1,128 @@ // modules/holidays/holidays.js document.addEventListener("DOMContentLoaded", () => { - // --- Modale "Ajouter une idée" - const addBtn = document.getElementById("hol-add-open"); - const addModal = document.getElementById("hol-add-modal"); - if (addBtn && addModal) { - const backdrop = addModal.querySelector(".hol-backdrop"); - const cancel = addModal.querySelector(".hol-cancel"); - const open = () => addModal.classList.add("open"); - const close = () => addModal.classList.remove("open"); - addBtn.addEventListener("click", open); - backdrop.addEventListener("click", close); - cancel.addEventListener("click", close); + // --- 1. FONCTIONS UTILITAIRES --- + + /** + * Configure les comportements de fermeture d'une modale (Backdrop, Cancel, Escape) + */ + function setupModal(modalId, openAction = null) { + const modal = document.getElementById(modalId); + if (!modal) return null; + + const backdrop = modal.querySelector(".hol-backdrop"); + const cancelBtn = modal.querySelector(".hol-cancel"); + + const close = () => modal.classList.remove("open"); + const open = () => { + modal.classList.add("open"); + if (openAction) openAction(); + }; + + if (backdrop) backdrop.addEventListener("click", close); + if (cancelBtn) cancelBtn.addEventListener("click", close); + + // Fermeture avec ECHAP (uniquement si cette modale est ouverte) document.addEventListener("keydown", (e) => { - if (e.key === "Escape") close(); + if (e.key === "Escape" && modal.classList.contains("open")) { + close(); + } }); + + return { modal, open, close }; } - // --- Helpers modale "Éditer" - const editModal = document.getElementById("hol-edit-modal"); - const openEditModal = () => { - if (editModal) editModal.classList.add("open"); - }; - const closeEditModal = () => { - if (editModal) editModal.classList.remove("open"); - }; + /** + * Échappe les caractères HTML pour éviter les failles XSS simples + */ + function esc(s) { + if (s === null || s === undefined) return ""; + return String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + // --- 2. GESTION MODALE "AJOUTER" --- + const addModalCtrl = setupModal("hol-add-modal"); + const addBtn = document.getElementById("hol-add-open"); + if (addBtn && addModalCtrl) { + addBtn.addEventListener("click", addModalCtrl.open); + } + + // --- 3. GESTION MODALE "ÉDITER" --- + const editModalCtrl = setupModal("hol-edit-modal"); + + // Fonction pour charger et ouvrir l'édition via AJAX async function openEditForId(id) { + if (!editModalCtrl) return; + try { const res = await fetch( `/modules/holidays/view.php?id=${encodeURIComponent(id)}`, - { - headers: { Accept: "application/json" }, - }, + { headers: { Accept: "application/json" } }, ); - if (!res.ok) throw new Error("HTTP " + res.status); - const it = await res.json(); + if (!res.ok) throw new Error("Erreur HTTP " + res.status); - if (!editModal) { - alert("Modale édition introuvable"); - return; - } - // Scope les sélecteurs à la modale pour éviter les null - const $ = (sel) => editModal.querySelector(sel); + const it = await res.json(); + const modal = editModalCtrl.modal; + + // Helper pour remplir les champs const setVal = (sel, val) => { - const el = $(sel); + const el = modal.querySelector(sel); if (el) el.value = val ?? ""; }; + // Remplissage du formulaire setVal("#edit-id", it.id); - setVal("#edit-title", it.title || ""); - setVal("#edit-country", it.country || ""); - setVal("#edit-region", it.region || ""); - setVal("#edit-city", it.city || ""); - setVal("#edit-lat", it.lat ?? ""); - setVal("#edit-lng", it.lng ?? ""); - setVal("#edit-start", it.desired_start_date ?? ""); - setVal("#edit-end", it.desired_end_date ?? ""); - setVal("#edit-season", it.season_hint || ""); - setVal("#edit-days", it.ideal_days ?? ""); + setVal("#edit-title", it.title); + setVal("#edit-country", it.country); + setVal("#edit-region", it.region); + setVal("#edit-city", it.city); + setVal("#edit-lat", it.lat); + setVal("#edit-lng", it.lng); + setVal("#edit-start", it.desired_start_date); + setVal("#edit-end", it.desired_end_date); + setVal("#edit-season", it.season_hint); + setVal("#edit-days", it.ideal_days); setVal("#edit-status", it.status || "draft"); - setVal("#edit-notes", it.notes || ""); + setVal("#edit-notes", it.notes); - openEditModal(); - } catch { - alert("Impossible de charger l’idée."); + editModalCtrl.open(); + } catch (err) { + console.error(err); + alert("Impossible de charger les données de l'idée."); } } - // --- Modale "Éditer" (liste + page détail) - if (editModal) { - const backdrop = editModal.querySelector(".hol-backdrop"); - const cancel = editModal.querySelector(".hol-cancel"); - if (backdrop) backdrop.addEventListener("click", closeEditModal); - if (cancel) cancel.addEventListener("click", closeEditModal); - document.addEventListener("keydown", (e) => { - if (e.key === "Escape") closeEditModal(); - }); - - // Boutons "Éditer" des cards (liste/planifiées/archivées) - document.querySelectorAll(".btn-edit[data-edit-id]").forEach((btn) => { - btn.addEventListener("click", () => { - const id = btn.getAttribute("data-edit-id"); - if (id) openEditForId(id); - }); - }); - - // Bouton "Éditer" sur la page détail (en-tête) - const headerEditBtn = document.getElementById("hol-edit-open"); - if (headerEditBtn) { - headerEditBtn.addEventListener("click", () => { - const id = headerEditBtn.getAttribute("data-edit-id"); - if (id) openEditForId(id); - }); + // Écouteurs sur les boutons "Éditer" (Liste + Détail) + document.body.addEventListener("click", (e) => { + // Utilisation de la délégation d'événement pour gérer tous les boutons (même dynamiques) + const btn = e.target.closest(".btn-edit"); + if (btn && btn.hasAttribute("data-edit-id")) { + const id = btn.getAttribute("data-edit-id"); + openEditForId(id); } - } + // Cas spécifique du bouton dans le header de la vue détail + else if (e.target.id === "hol-edit-open") { + const id = e.target.getAttribute("data-edit-id"); + openEditForId(id); + } + }); - // --- Suppression (liste + planifiées + page détail) - document.querySelectorAll(".btn-delete[data-del-id]").forEach((btn) => { - btn.addEventListener("click", () => { + // --- 4. GESTION SUPPRESSION --- + document.body.addEventListener("click", (e) => { + const btn = e.target.closest(".btn-delete"); + if (btn && btn.hasAttribute("data-del-id")) { const id = btn.getAttribute("data-del-id"); - if (!id) return; - if (confirm("Supprimer cette idée ?")) { + if ( + confirm( + "Voulez-vous vraiment supprimer cette idée ?\nCette action est irréversible.", + ) + ) { + // Création d'un formulaire temporaire pour le POST const form = document.createElement("form"); form.method = "post"; form.action = "/modules/holidays/save.php"; @@ -112,45 +133,51 @@ document.addEventListener("DOMContentLoaded", () => { document.body.appendChild(form); form.submit(); } - }); + } }); - // --- Géocodage via Nominatim + UI multi-résultats + // --- 5. GÉOCODAGE (Nominatim) --- document.querySelectorAll(".hol-geocode-btn").forEach((btn) => { btn.addEventListener("click", async () => { const scope = btn.getAttribute("data-scope"); // 'add' ou 'edit' - let form, city, region, country, latInput, lngInput; + let latInput, + lngInput, + qParts = []; + // Récupération des inputs selon le scope if (scope === "edit") { - form = btn.closest("form") || document; - city = (document.getElementById("edit-city")?.value || "").trim(); - region = (document.getElementById("edit-region")?.value || "").trim(); - country = (document.getElementById("edit-country")?.value || "").trim(); + const getVal = (id) => + (document.getElementById(id)?.value || "").trim(); + qParts = [ + getVal("edit-city"), + getVal("edit-region"), + getVal("edit-country"), + ]; latInput = document.getElementById("edit-lat"); lngInput = document.getElementById("edit-lng"); } else { - form = btn.closest("form"); - city = (form?.querySelector('input[name="city"]')?.value || "").trim(); - region = ( - form?.querySelector('input[name="region"]')?.value || "" - ).trim(); - country = ( - form?.querySelector('input[name="country"]')?.value || "" - ).trim(); + // Scope 'add' : on cherche dans le formulaire parent + const form = btn.closest("form"); + const getVal = (name) => + (form?.querySelector(`input[name="${name}"]`)?.value || "").trim(); + qParts = [getVal("city"), getVal("region"), getVal("country")]; latInput = form?.querySelector('input[name="lat"]'); lngInput = form?.querySelector('input[name="lng"]'); } - const q = [city, region, country].filter(Boolean).join(", "); + const q = qParts.filter(Boolean).join(", "); if (!q) { - alert("Renseigne au moins Ville/Pays."); + alert( + "Veuillez renseigner au moins une Ville ou un Pays pour géocoder.", + ); return; } + // UI : chargement removeNearbyPicker(btn); btn.disabled = true; - const original = btn.textContent; - btn.textContent = "Recherche..."; + const originalText = btn.textContent; + btn.textContent = "⏳..."; try { const res = await fetch( @@ -160,28 +187,30 @@ document.addEventListener("DOMContentLoaded", () => { }, ); const data = await res.json(); - if (!res.ok) throw new Error(data?.error || "Erreur géocodage"); - if ("lat" in data && "lng" in data) { + if (!res.ok) throw new Error(data?.error || "Erreur inconnue"); + + // Cas 1: Résultat direct (lat/lng uniques) + if (data.lat && data.lng) { if (latInput) latInput.value = data.lat; if (lngInput) lngInput.value = data.lng; - return; } - - if (Array.isArray(data.results) && data.results.length > 0) { + // Cas 2: Liste de choix + else if (Array.isArray(data.results) && data.results.length > 0) { renderGeocodePicker(btn, data.results, (choice) => { if (latInput) latInput.value = choice.lat; if (lngInput) lngInput.value = choice.lng; removeNearbyPicker(btn); }); } else { - alert("Aucun résultat."); + alert("Aucun résultat trouvé pour : " + q); } - } catch { - alert("Impossible de trouver les coordonnées pour: " + q); + } catch (err) { + console.error(err); + alert("Erreur lors du géocodage."); } finally { btn.disabled = false; - btn.textContent = original; + btn.textContent = originalText; } }); }); @@ -192,46 +221,36 @@ document.addEventListener("DOMContentLoaded", () => { const wrapper = document.createElement("div"); wrapper.className = "hol-geocode-picker"; + // Header const header = document.createElement("div"); header.className = "hol-geocode-picker__header"; - header.textContent = "Plusieurs résultats trouvés"; - const closeBtn = document.createElement("button"); - closeBtn.type = "button"; - closeBtn.className = "hol-geocode-picker__close"; - closeBtn.textContent = "×"; - closeBtn.addEventListener("click", () => removeNearbyPicker(anchorBtn)); - header.appendChild(closeBtn); + header.innerHTML = `Choix multiples`; + header + .querySelector(".hol-close") + .addEventListener("click", () => removeNearbyPicker(anchorBtn)); + wrapper.appendChild(header); + // Liste const list = document.createElement("ul"); list.className = "hol-geocode-picker__list"; results.forEach((r) => { const li = document.createElement("li"); li.className = "hol-geocode-picker__item"; - - const label = document.createElement("div"); - label.className = "hol-geocode-picker__label"; - label.textContent = r.display_name || `${r.lat}, ${r.lng}`; - - const coords = document.createElement("div"); - coords.className = "hol-geocode-picker__coords"; - coords.textContent = `(${r.lat}, ${r.lng})`; - - const pickBtn = document.createElement("button"); - pickBtn.type = "button"; - pickBtn.className = "hol-geocode-picker__pick"; - pickBtn.textContent = "Choisir"; - pickBtn.addEventListener("click", () => onPick(r)); - - li.appendChild(label); - li.appendChild(coords); - li.appendChild(pickBtn); + li.innerHTML = ` +Idée introuvable.
'; - return; - } - $transport = hol_q($pdo, "SELECT * FROM pf_holidays_transport WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - $lodging = hol_q($pdo, "SELECT * FROM pf_holidays_lodging WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - $acts = hol_q($pdo, "SELECT * FROM pf_holidays_activities WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - $budget = hol_q($pdo, "SELECT category, label, amount, per_person FROM pf_holidays_budget_items WHERE idea_id = ? ORDER BY created_at DESC", [$ideaId]); - $sumRow = hol_q($pdo, " - SELECT - SUM(CASE WHEN per_person=0 THEN amount ELSE 0 END) AS fixed_total, - SUM(CASE WHEN per_person=1 THEN amount ELSE 0 END) AS per_person_total - FROM pf_holidays_budget_items WHERE idea_id = ? - ", [$ideaId])[0] ?? ['fixed_total' => 0, 'per_person_total' => 0]; - $fixedTotal = (float)($sumRow['fixed_total'] ?? 0); - $ppTotal = (float)($sumRow['per_person_total'] ?? 0); - ?> - - - - + -