From ecb490c55ae018f5cb6a390fc05fceab94237bf5 Mon Sep 17 00:00:00 2001 From: Quentin Rouiller Date: Fri, 24 Apr 2026 09:00:00 +0200 Subject: [PATCH] =?UTF-8?q?Version=202026.5.31=20=E2=80=94=20Sarcelle=20ab?= =?UTF-8?q?sence=20r=C3=A9currente=20(REJET=C3=89=20par=20utilisateur)=20[?= =?UTF-8?q?code=20interpol=C3=A9=20=E2=80=94=20version=20revert=C3=A9e=20p?= =?UTF-8?q?ar=20la=20suite]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.json | 2 +- viewer.css | 13 +++- viewer.js | 184 +++++++++++++++++++++++++++++++++++++------------- 3 files changed, 149 insertions(+), 50 deletions(-) diff --git a/manifest.json b/manifest.json index 6ccde87..94dd6b9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.30", + "version": "2026.5.31", "description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.", "browser_specific_settings": { "gecko": { diff --git a/viewer.css b/viewer.css index 552adb6..afd692a 100644 --- a/viewer.css +++ b/viewer.css @@ -2634,10 +2634,19 @@ header.topbar::before { overflow: visible; background: var(--bg-elevated) !important; border: 1px solid var(--border) !important; + border-radius: 6px !important; } -/* Séparer visuellement la dragbar (collée en haut) des boutons topbar */ + +/* Dans le mode minimisé, la topbar n'est plus en absolute : elle se pose en fin + de ligne à droite, après la ref */ .pinned-popup.pinned-popup-minimized .pinned-popup-topbar { - top: 14px !important; /* sous la dragbar (qui fait ~6-8px) */ + position: static !important; + top: auto !important; + right: auto !important; + margin-left: auto !important; + order: 3; + flex-shrink: 0; + padding: 0 2px; } /* Masquer tous les enfants directs du popup minimisé */ .pinned-popup.pinned-popup-minimized > *:not(.pinned-popup-topbar):not(.pinned-popup-dragbar):not(.pinned-popup-minref) { diff --git a/viewer.js b/viewer.js index 02dc933..7c54337 100644 --- a/viewer.js +++ b/viewer.js @@ -5964,6 +5964,21 @@ function extractContacts(raw) { */ function splitOneContact(raw) { if (!raw) return { name: null, phone: null }; + + // v2026.5.25 : avant d'extraire les numéros, on REMPLACE les séquences qui + // sont des identifiants de matériel (LETTRES_CHIFFRES) par des espaces. + // Exemples : XXXX_NNNNNNNNNNN, XNNNNNN, XNNNNNN, XNNNNNN. + // Sans ça, XXXX_NNNNNNNNNNN laisse des "NNNN NNN NN NN" qui se font prendre + // pour un numéro de téléphone par le regex qui greedy sur [0-9\s.\-]. + // On remplace par des espaces de même longueur pour préserver les offsets + // (important pour le calcul de position du nom avant le 1er numéro). + raw = String(raw); + raw = raw.replace(/\b[A-Z]{1,6}_\d+/g, (m) => " ".repeat(m.length)); + // Idem pour les identifiants sans underscore style XNNNNNN, XNNNNNN, XNNNNNN + // (1-2 lettres majuscules suivies de 5+ chiffres collés). On garde assez + // permissif pour matcher les variantes sans enlever des vrais mots. + raw = raw.replace(/\b[A-Z]{1,3}\d{5,}\b/g, (m) => " ".repeat(m.length)); + // v4.1.20 : regex plus permissives pour tolérer les erreurs humaines : // - pas d'espace après le numéro (ex: "021555555Textecoller") // - pas d'espace/parenthèse avant un court numéro @@ -6485,12 +6500,29 @@ function showTooltip(e, iv, rowEl) { // v2026.5.19 : pendant qu'un popup épinglé est en cours de drag, on ignore // les mouseenter sur les cartes — sinon en survolant une carte on déclenche // l'ouverture d'un nouveau tooltip par-dessus ce qu'on est en train de bouger. - if (state._popupDragging) return; + if (state._popupDragging) { + console.log("[showTooltip] ignoré : popup drag en cours"); + return; + } + + // v2026.5.27 : fermer tout popup "soft-unpinned" encore visible (il traîne + // parce que la souris était dessus). Dès qu'on survole autre chose, on veut + // que seul le popup actuel ou les popups épinglés restent. + // v2026.5.34 : le softUnpin ne supprime plus le popup au mouseleave, donc + // ici on les supprime explicitement quand un nouveau tooltip démarre. + const softUnpinned = document.querySelectorAll(".soft-unpinned"); + if (softUnpinned.length) { + console.log(`[showTooltip] suppression de ${softUnpinned.length} popup(s) soft-unpinned`); + softUnpinned.forEach(el => { + try { el.remove(); } catch (err) {} + }); + } // v4.1.15 : si la bulle est épinglée sur une autre iv, on NE REMPLACE PAS // son contenu (l'user veut garder la fiche épinglée même en survolant // d'autres cartes). if (bulleState.pinned && state.currentTooltipIv && state.currentTooltipIv !== iv) { + console.log("[showTooltip] ignoré : tooltip épinglé sur une autre iv"); return; } @@ -6677,56 +6709,108 @@ function reapplyTooltipPosition() { el.style.top = ((el._absBasisTop || 0) + dy) + "px"; } -function positionTooltipAnchored(rowEl) { +/** + * v2026.5.34 : fonction UNIQUE et unifiée de positionnement ancré du tooltip. + * + * Utilisée par : + * - showTooltip() — hover d'une row intervention en vue classique + * - openPersistentTimelinePopup() — clic (classique) ou hover (horizontal) + * d'un segment timeline + * - showTooltip() pour les cartes/badges absence + * + * Algorithme : + * 1. Essaie 4 positions dans l'ordre : droite, gauche, dessous, dessus + * 2. Chaque position : padding de 8px min par rapport à la source + * 3. Chaque position : doit tenir dans la safe area (pas sous topbar/dock) + * 4. Chaque position : ne doit pas chevaucher les popups épinglés existants + * 5. Première position qui satisfait tout → on la prend + * 6. Fallback si aucune ne marche : droite clampée (la moins pire) + * + * La popup ne couvre JAMAIS la source (pad >= 8px en distance euclidienne). + * + * @param {HTMLElement} sourceEl - l'élément déclencheur (row, card, segment) + * @param {object} opts - options { anchorBelow: true pour préférer dessous } + */ +function positionTooltipAnchored(sourceEl, opts) { + opts = opts || {}; const el = tooltipEl(); - if (!rowEl || !el) return; - const pad = 14; - const rowRect = rowEl.getBoundingClientRect(); + if (!el) { + console.warn("[positionTooltip] tooltip DOM introuvable"); + return; + } + if (!sourceEl) { + console.warn("[positionTooltip] sourceEl null — pas de positionnement"); + return; + } + + const pad = 10; // padding entre source et popup + const viewportMargin = 8; // marge par rapport aux bords + const srcRect = sourceEl.getBoundingClientRect(); const tipRect = el.getBoundingClientRect(); - - // Position X : à droite de la ligne par défaut - let x = rowRect.right + pad; - if (x + tipRect.width > window.innerWidth - 8) { - x = rowRect.left - tipRect.width - pad; - } - if (x < 4) x = 4; - - // Position Y : aligné en haut de la ligne - let y = rowRect.top; - if (y + tipRect.height > window.innerHeight - 8) { - y = window.innerHeight - tipRect.height - 8; - } - if (y < 4) y = 4; - - // v2026.5.17 : éviter le chevauchement avec les popups épinglés existants. - // On teste la position candidate, et si elle chevauche un popup épinglé, - // on essaie d'autres candidats (gauche de la carte, au-dessous, au-dessus). - const tipW = tipRect.width || 320; + const tipW = tipRect.width || 320; // fallback taille si pas encore rendu const tipH = tipRect.height || 200; - const pinnedRects = _getPinnedPopupsViewportRects(); - if (pinnedRects.length) { - const candidates = [ - { x, y, label: "right" }, - { x: rowRect.left - tipW - pad, y: rowRect.top, label: "left" }, - { x: rowRect.left, y: rowRect.bottom + pad, label: "below" }, - { x: rowRect.left, y: rowRect.top - tipH - pad, label: "above" } - ]; - for (const c of candidates) { - // Borne dans le viewport - if (c.x < 4) c.x = 4; - if (c.x + tipW > window.innerWidth - 8) c.x = window.innerWidth - tipW - 8; - if (c.y < 4) c.y = 4; - if (c.y + tipH > window.innerHeight - 8) c.y = window.innerHeight - tipH - 8; - const testRect = { left: c.x, top: c.y, right: c.x + tipW, bottom: c.y + tipH }; - const overlaps = pinnedRects.some(pr => _rectsOverlap(testRect, pr)); - if (!overlaps) { - x = c.x; y = c.y; - break; - } - } + + // Safe area : respecter topbar en haut, dock en bas + const safe = (typeof _getPopupSafeArea === "function") + ? _getPopupSafeArea() + : { left: viewportMargin, top: viewportMargin, + right: window.innerWidth - viewportMargin, + bottom: window.innerHeight - viewportMargin }; + + // 4 candidats (ordre : droite → gauche → dessous → dessus) + // Préférence opts.anchorBelow = true : dessous en premier (ex: clic timeline) + const rightCandidate = { x: srcRect.right + pad, y: srcRect.top, label: "droite" }; + const leftCandidate = { x: srcRect.left - tipW - pad, y: srcRect.top, label: "gauche" }; + const belowCandidate = { x: srcRect.left, y: srcRect.bottom + pad, label: "dessous" }; + const aboveCandidate = { x: srcRect.left, y: srcRect.top - tipH - pad, label: "dessus" }; + + const candidates = opts.anchorBelow + ? [belowCandidate, aboveCandidate, rightCandidate, leftCandidate] + : [rightCandidate, leftCandidate, belowCandidate, aboveCandidate]; + + const pinnedRects = (typeof _getPinnedPopupsViewportRects === "function") + ? _getPinnedPopupsViewportRects() + : []; + + let chosen = null; + for (const c of candidates) { + // Clamp dans la safe area + let cx = c.x, cy = c.y; + if (cx < safe.left) cx = safe.left; + if (cx + tipW > safe.right) cx = safe.right - tipW; + if (cx < safe.left) continue; // popup plus large que safe area — skip + if (cy < safe.top) cy = safe.top; + if (cy + tipH > safe.bottom) cy = safe.bottom - tipH; + if (cy < safe.top) continue; + + // Ne chevauche PAS la source (garantit qu'on ne la cache pas) + const candRect = { left: cx, top: cy, right: cx + tipW, bottom: cy + tipH }; + if (_rectsOverlap(candRect, srcRect)) continue; + + // Ne chevauche pas les popups épinglés existants + const hitsPinned = pinnedRects.some(pr => _rectsOverlap(candRect, pr)); + if (hitsPinned) continue; + + chosen = { x: cx, y: cy, label: c.label }; + break; } - setTooltipViewportPosition(x, y); + if (!chosen) { + // Fallback : droite clampée à tout prix, même si ça chevauche (cas rare + // avec écran minuscule ou beaucoup de popups épinglés) + let fx = srcRect.right + pad; + let fy = srcRect.top; + if (fx + tipW > safe.right) fx = safe.right - tipW; + if (fx < safe.left) fx = safe.left; + if (fy + tipH > safe.bottom) fy = safe.bottom - tipH; + if (fy < safe.top) fy = safe.top; + chosen = { x: fx, y: fy, label: "fallback" }; + console.log("[positionTooltip] fallback utilisé (aucun candidat optimal trouvé)"); + } else { + console.log(`[positionTooltip] position choisie : ${chosen.label} (${Math.round(chosen.x)}, ${Math.round(chosen.y)})`); + } + + setTooltipViewportPosition(chosen.x, chosen.y); } /** @@ -6923,7 +7007,13 @@ function pinTooltip() { if (closeAllBtn) closeAllBtn.remove(); } - // Chercher la ligne source (row iv-v2) + // Chercher la ligne source pour le positionnement. + // v2026.5.35 : en vue horizontale, les rows .intervention-v2 sont CACHÉES + // (display: none via CSS .view-horizontal). getBoundingClientRect d'un + // élément caché retourne (0,0,0,0) → popup part en haut à gauche. + // Solution : priorité au tooltip actuellement visible (qui EST la vraie + // source visuelle du clic/hover). Fallback sur le segment timeline. Enfin + // la row (qui marche en vue classique). let rowEl = null; if (iv.actionId) { rowEl = document.querySelector(`.intervention-v2[data-action-id="${iv.actionId}"]`);