From 7824990fba50d04e5a7f34412799e8468eb3885f Mon Sep 17 00:00:00 2001 From: Quentin Rouiller Date: Wed, 22 Apr 2026 09:00:00 +0200 Subject: [PATCH] =?UTF-8?q?Version=202026.5.21=20=E2=80=94=20Ajustements?= =?UTF-8?q?=20[code=20interpol=C3=A9]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 20 +- manifest.json | 2 +- viewer.css | 3 - viewer.html | 7 +- viewer.js | 518 +++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 537 insertions(+), 13 deletions(-) diff --git a/background.js b/background.js index 3798c99..2fd907b 100644 --- a/background.js +++ b/background.js @@ -784,8 +784,24 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) { console.log(`[bg] → SUCCÈS confirmé par XML <...>true avec function_name=${fn}`); return { status: r.status, functionName: fn, body: trimmed }; } - console.log(`[bg] → réponse ressemble à une erreur, on tente le prochain nom`); - lastBody = body; + + // Détection d'échec : false, erreurs, html, redirect, etc. + const looksLikeError = /^<\w+>false<\/\w+>\s*$/i.test(trimmed) + || lower.includes("error") + || lower.includes("erreur") + || lower.includes("unknown function") + || lower.includes("fonction inconnue") + || lower.includes(" - -
+ +
+
+
+
diff --git a/viewer.js b/viewer.js index 172a3d8..6523a70 100644 --- a/viewer.js +++ b/viewer.js @@ -6673,7 +6673,9 @@ function pinTooltip() { e.stopPropagation(); _softUnpinPopup(popup); }); - popup.appendChild(closeBtn); + topbar.appendChild(unpinBtn); + + popup.appendChild(topbar); // v4.3.3 : barre de drag en haut, pour déplacer la popup à la souris. // Ancrée en haut à 22px de haut ; le padding-top de la popup est augmenté @@ -6727,6 +6729,9 @@ function pinTooltip() { popup.style.top = pos.docY + "px"; popup.style.visibility = "visible"; + // v2026.5.20 : clamper dans la safe area (topbar + dock) + _clampPopupInSafeArea(popup); + // Enregistrer dans la liste pinnedPopups.push({ el: popup, iv: iv, rect: pos.rect }); @@ -6753,6 +6758,44 @@ function _closePinnedPopup(el) { el.remove(); } +/** + * v2026.5.19 : re-fetch les infos d'une intervention et met à jour le contenu + * du popup épinglé correspondant. Utilise fetchAndUpdateIntervention qui fait + * xhr2 + fiche, puis régénère le HTML du tooltip avec buildTooltipHTML. + */ +async function _refreshPinnedPopupIv(popup, iv) { + if (!popup || !iv) return; + try { + // Forcer le refetch : on invalide les flags qui disent "déjà fetché" + iv.xhr2Fetched = false; + iv.xhr2Fetching = false; + iv.ficheFetched = false; + iv.ficheFetching = false; + + // Token de refresh actuel (pour que fetchAndUpdateIntervention ne soit + // pas abortée par les checks isRefreshAborted) + const token = (typeof currentRefreshToken !== "undefined") ? currentRefreshToken : 0; + + await fetchAndUpdateIntervention(iv, token); + + // Régénérer le HTML du tooltip avec les nouvelles infos. + // On doit réinjecter juste le contenu, en gardant la topbar et la dragbar + // (qui ne sont PAS dans le tooltip source, elles sont propres au popup). + const topbar = popup.querySelector(".pinned-popup-topbar"); + const dragbar = popup.querySelector(".pinned-popup-dragbar"); + const newHtml = buildTooltipHTML(iv); + popup.innerHTML = newHtml; + // Virer aussi la vieille icône 📌 si elle revient dans le rebuild + const oldPin = popup.querySelector('.tooltip-pinbtn[data-action="pin"]'); + if (oldPin) oldPin.remove(); + // Remettre topbar et dragbar + if (topbar) popup.appendChild(topbar); + if (dragbar) popup.appendChild(dragbar); + } catch (err) { + console.warn("[refresh-popup]", err); + } +} + /** * Désépinglage "mou" : la popup n'est plus considérée épinglée (elle n'est * plus dans pinnedPopups, donc le comptage pour Ctrl×2 etc. ignore) mais on @@ -6779,6 +6822,35 @@ function _softUnpinPopup(el) { if (dragbar) dragbar.remove(); const closeBtn = el.querySelector(".pinned-popup-close"); if (closeBtn) closeBtn.remove(); + // v2026.5.17 : retirer aussi la nouvelle topbar et le conteneur minimisé + const topbar = el.querySelector(".pinned-popup-topbar"); + if (topbar) topbar.remove(); + el.classList.remove("pinned-popup-minimized"); + el.classList.remove("pinned-popup-reduced"); + + // v2026.5.18 : retirer aussi la pastille du dock si elle existe + if (el._linkedPill) { + try { el._linkedPill.remove(); } catch (e) {} + el._linkedPill = null; + } + // Si le dock est vide, le cacher ; mettre à jour le bouton "Fermer tous" + const dock = document.getElementById("pinned-popups-dock"); + if (dock && dock.querySelectorAll(".pinned-popup-dock-pill").length === 0) { + dock.classList.remove("visible"); + const closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (closeAllBtn) closeAllBtn.remove(); + } else { + _ensureDockCloseAllBtn(); + } + + // v2026.5.22 : si le tooltip hover est actuellement affiché pour la même + // intervention que celle qu'on désépingle, il faut regénérer son HTML pour + // que l'icône passe de 📍 (active rouge) à 📌 (non active) — sinon l'user + // voit l'ancienne icône et croit qu'il est toujours épinglé. + const tip = tooltipEl(); + if (tip && tip.classList.contains("visible") && state.currentTooltipIv) { + tip.innerHTML = buildTooltipHTML(state.currentTooltipIv); + } // Helper qui joue l'animation de sortie puis supprime le DOM const animateAndRemove = () => { @@ -6795,6 +6867,430 @@ function _softUnpinPopup(el) { el.addEventListener("mouseleave", animateAndRemove, { once: true }); } +// ============================================================================ +// v2026.5.17 : États d'un popup épinglé +// - Normal (complet, flottant) +// - Minimisé (compact, flottant, juste la ref + topbar) +// - Réduit (docké dans la taskbar en bas de l'écran) +// ============================================================================ + +/** + * Passe un popup épinglé en mode Minimisé : on ne montre plus que la ref, + * dans un petit cadre flottant toujours drag-able. + * + * v2026.5.19 : au lieu de masquer tout le contenu via CSS et tenter de + * réafficher la ref (fragile), on crée un élément dédié `.pinned-popup-minref` + * qui contient juste la ref + la date. Cet élément est ajouté/retiré au besoin. + */ +function _minimizePinnedPopup(popup) { + if (!popup) return; + popup.classList.add("pinned-popup-minimized"); + + // Adapter les boutons topbar : [▭] devient [⬆] (agrandir) + const minBtn = popup.querySelector(".pinned-popup-minimize"); + if (minBtn) { + minBtn.innerHTML = "⬆"; + minBtn.title = "Agrandir"; + // On retire les anciens listeners en clonant l'élément + const newBtn = minBtn.cloneNode(true); + minBtn.replaceWith(newBtn); + newBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _expandPinnedPopup(popup); + }); + } + + // Créer un élément dédié pour afficher la ref en mode minimisé + let minRef = popup.querySelector(".pinned-popup-minref"); + if (!minRef) { + minRef = document.createElement("div"); + minRef.className = "pinned-popup-minref"; + const refText = popup.dataset.ref || "(sans ref)"; + minRef.textContent = refText; + minRef.title = "Cliquer pour agrandir"; + minRef.addEventListener("click", (e) => { + e.stopPropagation(); + _expandPinnedPopup(popup); + }); + popup.appendChild(minRef); + } +} + +/** + * Repasse un popup minimisé en mode Normal (complet). + */ +function _expandPinnedPopup(popup) { + if (!popup) return; + popup.classList.remove("pinned-popup-minimized"); + + // Restaurer bouton Minimiser + const minBtn = popup.querySelector(".pinned-popup-minimize"); + if (minBtn) { + minBtn.innerHTML = "▭"; + minBtn.title = "Minimiser (reste flottant mais compact)"; + const newBtn = minBtn.cloneNode(true); + minBtn.replaceWith(newBtn); + newBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _minimizePinnedPopup(popup); + }); + } + + // Retirer l'élément ref dédié (s'il existe) + const minRef = popup.querySelector(".pinned-popup-minref"); + if (minRef) minRef.remove(); +} + +/** + * Passe un popup épinglé en mode Réduit : il disparaît de son emplacement + * flottant et vient s'ajouter dans une taskbar en bas de l'écran sous forme + * de pastille cliquable. + */ +function _reducePinnedPopup(popup) { + if (!popup) return; + + // Récupérer la référence pour le label de la pastille + // v2026.5.18 : préférer le dataset.ref mémorisé à la création plutôt que + // le textContent (qui peut contenir "—" si la ref n'était pas encore + // disponible à l'épinglage) + const refEl = popup.querySelector(".iv-ref-header"); + const label = popup.dataset.ref + || (refEl ? (refEl.textContent || "").trim() : "") + || "Popup"; + const colorKey = popup.dataset.colorKey || "autre"; + + // S'assurer que la taskbar du bas existe + let dock = document.getElementById("pinned-popups-dock"); + if (!dock) { + dock = document.createElement("div"); + dock.id = "pinned-popups-dock"; + dock.className = "pinned-popups-dock"; + document.body.appendChild(dock); + } + + // Créer la pastille dock + // v2026.5.18 : le fond de la pastille prend la couleur de catégorie + // (via la classe color-XXX déjà utilisée ailleurs dans le CSS) + // v2026.5.19 : pastille à 2 lignes — ref (gras) + date origine (petit) + const pill = document.createElement("button"); + pill.type = "button"; + pill.className = "pinned-popup-dock-pill color-" + colorKey; + pill.title = "Cliquer pour agrandir"; + + const pillRef = document.createElement("span"); + pillRef.className = "pinned-popup-dock-pill-ref"; + pillRef.textContent = label; + pill.appendChild(pillRef); + + // Date d'origine (ex: "21.04") + const originDate = popup.dataset.originDate || ""; + if (originDate) { + const pillDate = document.createElement("span"); + pillDate.className = "pinned-popup-dock-pill-date"; + pillDate.textContent = _formatDateShort(originDate); + pill.appendChild(pillDate); + } + + // Mémoriser la position/taille du popup avant de le masquer + const rect = popup.getBoundingClientRect(); + popup.dataset.prevLeft = popup.style.left || (rect.left + "px"); + popup.dataset.prevTop = popup.style.top || (rect.top + "px"); + popup.dataset.prevWidth = popup.style.width || ""; + + // Cacher le popup (on le garde en DOM pour conserver son état et restaurer + // instantanément) + popup.classList.add("pinned-popup-reduced"); + + // Associer pill ↔ popup + pill._linkedPopup = popup; + popup._linkedPill = pill; + + pill.addEventListener("click", (e) => { + e.stopPropagation(); + _restorePinnedPopupFromDock(popup); + }); + + // v2026.5.20 : mini-menu au survol (Agrandir / Fermer) + pill.addEventListener("mouseenter", () => { + _showPillHoverMenu(pill, popup); + }); + pill.addEventListener("mouseleave", (e) => { + // Le menu peut être sous la souris — on ne ferme pas si on entre dans le menu + _schedulePillMenuClose(); + }); + + dock.appendChild(pill); + dock.classList.add("visible"); + + // v2026.5.18 : s'assurer qu'il y a un bouton "Fermer tous" si 2+ popups + _ensureDockCloseAllBtn(); + + // v2026.5.20 : le dock qui apparaît peut chevaucher des popups flottants — + // les reclamper pour qu'ils restent dans la safe area. + _reclampAllFloatingPopups(); +} + +/** + * v2026.5.20 : affiche un mini-menu au-dessus d'une pastille dock au survol. + * Contient 2 actions : Agrandir, Fermer. + */ +let _pillMenuCloseTimer = null; +function _showPillHoverMenu(pill, popup) { + // Annuler une fermeture en cours + if (_pillMenuCloseTimer) { + clearTimeout(_pillMenuCloseTimer); + _pillMenuCloseTimer = null; + } + // S'il existe déjà un menu pour un autre pill, le fermer + const existing = document.getElementById("pill-hover-menu"); + if (existing) { + if (existing._linkedPill === pill) return; // déjà pour ce pill + existing.remove(); + } + + const menu = document.createElement("div"); + menu.id = "pill-hover-menu"; + menu.className = "pill-hover-menu"; + menu._linkedPill = pill; + menu._linkedPopup = popup; + + const restoreBtn = document.createElement("button"); + restoreBtn.type = "button"; + restoreBtn.className = "pill-hover-menu-btn"; + restoreBtn.innerHTML = ' Agrandir'; + restoreBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _hidePillHoverMenu(); + _restorePinnedPopupFromDock(popup); + }); + menu.appendChild(restoreBtn); + + const closeBtn = document.createElement("button"); + closeBtn.type = "button"; + closeBtn.className = "pill-hover-menu-btn pill-hover-menu-close"; + closeBtn.innerHTML = ' Fermer'; + closeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _hidePillHoverMenu(); + // Retirer le popup de la liste et supprimer le DOM + const idx = pinnedPopups.findIndex(p => p.el === popup); + if (idx >= 0) pinnedPopups.splice(idx, 1); + try { popup.remove(); } catch (err) {} + try { pill.remove(); } catch (err) {} + const dock = document.getElementById("pinned-popups-dock"); + if (dock && dock.querySelectorAll(".pinned-popup-dock-pill").length === 0) { + dock.classList.remove("visible"); + const closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (closeAllBtn) closeAllBtn.remove(); + _reclampAllFloatingPopups(); + } else { + _ensureDockCloseAllBtn(); + } + }); + menu.appendChild(closeBtn); + + document.body.appendChild(menu); + + // Positionner au-dessus de la pastille + const r = pill.getBoundingClientRect(); + const menuR = menu.getBoundingClientRect(); + let left = r.left + (r.width / 2) - (menuR.width / 2); + if (left < 4) left = 4; + if (left + menuR.width > window.innerWidth - 4) left = window.innerWidth - menuR.width - 4; + menu.style.left = left + "px"; + menu.style.top = (r.top - menuR.height - 8) + "px"; + + // Garder ouvert si la souris entre dans le menu + menu.addEventListener("mouseenter", () => { + if (_pillMenuCloseTimer) { + clearTimeout(_pillMenuCloseTimer); + _pillMenuCloseTimer = null; + } + }); + menu.addEventListener("mouseleave", () => { + _schedulePillMenuClose(); + }); +} + +function _schedulePillMenuClose() { + if (_pillMenuCloseTimer) clearTimeout(_pillMenuCloseTimer); + _pillMenuCloseTimer = setTimeout(() => { + _hidePillHoverMenu(); + _pillMenuCloseTimer = null; + }, 250); +} + +function _hidePillHoverMenu() { + const existing = document.getElementById("pill-hover-menu"); + if (existing) existing.remove(); +} + +/** + * v2026.5.20 : calcule la safe area pour les popups épinglés. + * Retourne {top, bottom, left, right} en coords viewport. + * - top : hauteur de la topbar (les popups ne doivent pas passer dessous) + * - bottom : top du dock si visible, sinon hauteur viewport + */ +function _getPopupSafeArea() { + let topLimit = 4; + const topbar = document.querySelector("header.topbar"); + if (topbar) { + const r = topbar.getBoundingClientRect(); + if (r.bottom > topLimit) topLimit = r.bottom + 4; + } + let bottomLimit = window.innerHeight - 4; + const dock = document.getElementById("pinned-popups-dock"); + if (dock && dock.classList.contains("visible")) { + const r = dock.getBoundingClientRect(); + if (r.top < bottomLimit) bottomLimit = r.top - 4; + } + return { top: topLimit, bottom: bottomLimit, left: 4, right: window.innerWidth - 4 }; +} + +/** + * v2026.5.20 : contraint un popup flottant (en coords document via style.left/top) + * dans la safe area. Appelé à l'épinglage, pendant le drag, et quand le dock + * apparaît/disparaît. + */ +function _clampPopupInSafeArea(popup) { + if (!popup) return; + if (popup.classList.contains("pinned-popup-reduced")) return; // pas clamp si docké + const safe = _getPopupSafeArea(); + const rect = popup.getBoundingClientRect(); + const w = rect.width || popup.offsetWidth || 280; + const h = rect.height || popup.offsetHeight || 200; + + // Les coords viewport actuelles + const vLeft = rect.left; + const vTop = rect.top; + + // Calcul des coords viewport cibles après clamp + let newVLeft = vLeft; + let newVTop = vTop; + if (newVLeft < safe.left) newVLeft = safe.left; + if (newVLeft + w > safe.right) newVLeft = safe.right - w; + if (newVLeft < safe.left) newVLeft = safe.left; // si popup plus large que viewport + + if (newVTop < safe.top) newVTop = safe.top; + if (newVTop + h > safe.bottom) newVTop = safe.bottom - h; + if (newVTop < safe.top) newVTop = safe.top; + + if (newVLeft === vLeft && newVTop === vTop) return; // rien à faire + + // Différence = appliquer au style.left / style.top (qui sont en document coords) + const dx = newVLeft - vLeft; + const dy = newVTop - vTop; + const curLeft = parseFloat(popup.style.left) || 0; + const curTop = parseFloat(popup.style.top) || 0; + popup.style.left = (curLeft + dx) + "px"; + popup.style.top = (curTop + dy) + "px"; +} + +/** + * Réclampe tous les popups flottants (utile après apparition/disparition du dock). + */ +function _reclampAllFloatingPopups() { + document.querySelectorAll(".pinned-popup:not(.pinned-popup-reduced)").forEach(p => { + _clampPopupInSafeArea(p); + }); +} + +/** + * v2026.5.19 : réduit TOUS les popups épinglés actuellement ouverts (en mode + * normal ou minimisé) dans la taskbar du bas. Appelé au changement de date. + */ +function _reduceAllPinnedPopups() { + const popups = document.querySelectorAll(".pinned-popup:not(.pinned-popup-reduced)"); + popups.forEach(popup => { + try { _reducePinnedPopup(popup); } catch (e) {} + }); +} + +/** + * v2026.5.19 : ISO date (YYYY-MM-DD) → format court "DD.MM" pour le dock. + */ +function _formatDateShort(iso) { + if (!iso) return ""; + const m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!m) return iso; + return `${m[3]}.${m[2]}`; +} + +/** + * v2026.5.18 : ajoute (ou met à jour) le bouton "Fermer tous" dans le dock + * quand au moins 2 popups épinglés existent (réduits OU affichés). + * Le bouton est placé à droite du dock. + */ +function _ensureDockCloseAllBtn() { + const dock = document.getElementById("pinned-popups-dock"); + if (!dock) return; + const allPinned = document.querySelectorAll(".pinned-popup"); + let closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (allPinned.length >= 2) { + if (!closeAllBtn) { + closeAllBtn = document.createElement("button"); + closeAllBtn.type = "button"; + closeAllBtn.id = "pinned-popups-close-all"; + closeAllBtn.className = "pinned-popups-close-all"; + closeAllBtn.textContent = "✕ Fermer tous"; + closeAllBtn.title = "Fermer tous les popups épinglés"; + closeAllBtn.addEventListener("click", (e) => { + e.stopPropagation(); + closeAllPinnedPopups(); + }); + dock.appendChild(closeAllBtn); + } else { + // Remettre à la fin (après les pastilles éventuellement ajoutées) + dock.appendChild(closeAllBtn); + } + dock.classList.add("visible"); + } else if (closeAllBtn) { + closeAllBtn.remove(); + } +} + +/** + * Ramène un popup réduit en mode Normal : retire la pastille du dock et + * réaffiche le popup flottant à sa position d'avant réduction. + */ +function _restorePinnedPopupFromDock(popup) { + if (!popup) return; + popup.classList.remove("pinned-popup-reduced"); + // Si le popup était minimisé avant d'être réduit, on l'agrandit direct + // (la demande était : "Si la reduit et rappeller s'affiche en grand direct") + popup.classList.remove("pinned-popup-minimized"); + const minBtn = popup.querySelector(".pinned-popup-minimize"); + if (minBtn) { + minBtn.innerHTML = "▭"; + minBtn.title = "Minimiser (reste flottant mais compact)"; + const newBtn = minBtn.cloneNode(true); + minBtn.replaceWith(newBtn); + newBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _minimizePinnedPopup(popup); + }); + } + + // Supprimer la pastille associée + if (popup._linkedPill) { + popup._linkedPill.remove(); + popup._linkedPill = null; + } + + // Si le dock est vide (sauf le bouton "Fermer tous"), le masquer + const dock = document.getElementById("pinned-popups-dock"); + if (dock) { + const remainingPills = dock.querySelectorAll(".pinned-popup-dock-pill").length; + if (remainingPills === 0) { + dock.classList.remove("visible"); + const closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (closeAllBtn) closeAllBtn.remove(); + } else { + _ensureDockCloseAllBtn(); + } + } +} + /** Ferme toutes les popups épinglées (appelé par Échap ou changement de date). */ /** * v5.0.1 : helper pour déclencher la suppression d'une absence ou réservation. @@ -6855,6 +7351,13 @@ function closeAllPinnedPopups() { pinnedPopups.length = 0; // Fermer aussi les popups en état soft-unpinned qui trainent encore document.querySelectorAll(".pinned-popup.soft-unpinned").forEach(el => el.remove()); + // v2026.5.18 : supprimer aussi les éléments du dock + document.querySelectorAll(".pinned-popup").forEach(el => el.remove()); + document.querySelectorAll(".pinned-popup-dock-pill").forEach(el => el.remove()); + const closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (closeAllBtn) closeAllBtn.remove(); + const dock = document.getElementById("pinned-popups-dock"); + if (dock) dock.classList.remove("visible"); } /** @@ -6875,12 +7378,12 @@ function _attachPopupDragHandler(popup, dragbar) { let newLeft = startLeft + dx; let newTop = startTop + dy; - // Clamper dans le document (pas sortir trop à gauche/haut) - if (newLeft < 4) newLeft = 4; - if (newTop < 4) newTop = 4; - + // v2026.5.20 : clamper dans la safe area (topbar en haut, dock en bas, + // bordures viewport gauche/droite). On calcule en coords viewport puis + // on applique en coords document. popup.style.left = newLeft + "px"; popup.style.top = newTop + "px"; + _clampPopupInSafeArea(popup); }; const onMouseUp = () => { @@ -6889,6 +7392,11 @@ function _attachPopupDragHandler(popup, dragbar) { popup.classList.remove("dragging"); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); + // v2026.5.19 : retirer la classe body et le flag global après un petit + // délai pour laisser le temps au mouseleave de la carte de se propager + // sans déclencher de tooltip parasite. + document.body.classList.remove("popup-dragging"); + setTimeout(() => { state._popupDragging = false; }, 50); // Mettre à jour le rect mémorisé pour la détection de chevauchement const entry = pinnedPopups.find(p => p.el === popup);