Version 2026.5.31 — Sarcelle absence récurrente (REJETÉ par utilisateur)

[code interpolé — version revertée par la suite]
This commit is contained in:
2026-04-24 09:00:00 +02:00
parent 7e497de40e
commit 0c86c00922
3 changed files with 149 additions and 50 deletions
+1 -1
View File
@@ -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": {
+11 -2
View File
@@ -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) {
+137 -47
View File
@@ -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}"]`);