forked from FroSteel/Planification
Version 2026.5.31 — Sarcelle absence récurrente (REJETÉ par utilisateur)
[code interpolé — version revertée par la suite]
This commit is contained in:
@@ -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}"]`);
|
||||
|
||||
Reference in New Issue
Block a user