Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f78493859 |
+30
-14
@@ -211,23 +211,31 @@ async function fetchCurrentUser(origin, phpsessid) {
|
||||
const html = await resp.text();
|
||||
if (looksLikeLoginPage(html)) return null;
|
||||
|
||||
// Essais de patterns (du plus spécifique au plus générique)
|
||||
// v4.2.2 : patterns spécifiques à la structure EasyVista réelle du Canton
|
||||
// de Vaud (identifiés à partir du HTML de la page d'accueil). L'user est
|
||||
// affiché dans un dropdown ".ev-employee-dropdown" avec ces éléments :
|
||||
// <span class="profile-info">
|
||||
// <span class="h5" title="Nom, Prénom">Nom, Prénom</span>
|
||||
// <span class="h6" title="3.3 DGNSI-ServiceDesk">3.3 DGNSI-ServiceDesk</span>
|
||||
// ...
|
||||
// </span>
|
||||
// Le title du <a> parent contient aussi "Nom, Prénom / Service / Société".
|
||||
const patterns = [
|
||||
// Attribut data-user-name (si EasyVista l'expose)
|
||||
// 1) Le plus fiable : span class="h5" dans profile-info (structure EV 2026)
|
||||
/<span\s+class=["']profile-info["'][^>]*>\s*<span\s+class=["']h5["'][^>]*title=["']([^"']{2,80})["']/i,
|
||||
// 2) Fallback : span class="h5" avec title= même hors profile-info
|
||||
/<span\s+class=["']h5["'][^>]*title=["']([^"']{2,80})["'][^>]*>\s*([^<]{2,80})<\/span>/i,
|
||||
// 3) Fallback : title= de ev-employee-dropdown (format "Nom, Prénom / Service / Société")
|
||||
/class=["'][^"']*ev-employee-dropdown[^"']*["'][^>]*title=["']([^"'\/]+?)(?:\s*\/\s*[^"']+)?["']/i,
|
||||
// 4) Anciens patterns génériques (autres instances EasyVista éventuelles)
|
||||
/data-user-name\s*=\s*["']([^"']+)["']/i,
|
||||
/data-username\s*=\s*["']([^"']+)["']/i,
|
||||
/data-user-fullname\s*=\s*["']([^"']+)["']/i,
|
||||
// Variable JS typique EasyVista
|
||||
/EV\.User\.name\s*=\s*["']([^"']+)["']/,
|
||||
/EV\.User\.fullname\s*=\s*["']([^"']+)["']/,
|
||||
/userFullName\s*[:=]\s*["']([^"']+)["']/,
|
||||
/currentUser(?:Name)?\s*[:=]\s*["']([^"']+)["']/,
|
||||
// Balises cachées ou spans avec classe "user"
|
||||
/<(?:span|div)[^>]*class=["'][^"']*(?:user[_-]?(?:name|full|display))[^"']*["'][^>]*>([^<]{2,80})<\/(?:span|div)>/i,
|
||||
// "Bienvenue" / "Welcome"
|
||||
/(?:Bienvenue|Welcome)[,\s]+(?:M\.?\s+|Mme\s+)?([A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+(?:\s*,?\s+[A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+){0,3})/,
|
||||
// Title de la page (souvent "EasyVista - Nom Prénom")
|
||||
/<title>([^<]*)<\/title>/i
|
||||
// 5) "Bienvenue" / "Welcome"
|
||||
/(?:Bienvenue|Welcome)[,\s]+(?:M\.?\s+|Mme\s+)?([A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+(?:\s*,?\s+[A-ZÀÁÂÄÇÉÈÊËÍÎÏÓÔÖÚÛÜÑ][\wÀ-ÿ'.\-]+){0,3})/
|
||||
];
|
||||
|
||||
let name = null;
|
||||
@@ -236,7 +244,6 @@ async function fetchCurrentUser(origin, phpsessid) {
|
||||
if (m && m[1]) {
|
||||
const candidate = m[1].trim()
|
||||
.replace(/\s+/g, " ")
|
||||
// Enlever des éléments du <title> type "EasyVista" / "Planning" / etc.
|
||||
.replace(/^(?:EasyVista|EV|Accueil|Home|Planning|ITSMA)[\s\-|•]+/i, "")
|
||||
.replace(/[\s\-|•]+(?:EasyVista|EV|ITSMA)$/i, "")
|
||||
.trim();
|
||||
@@ -249,7 +256,16 @@ async function fetchCurrentUser(origin, phpsessid) {
|
||||
}
|
||||
}
|
||||
|
||||
// Chercher aussi le login (ID court) — utile comme fallback secondaire
|
||||
// v4.2.2 : on extrait aussi le service/unité si disponible (h6 à côté du h5)
|
||||
let service = null;
|
||||
const serviceMatch = html.match(
|
||||
/<span\s+class=["']profile-info["'][^>]*>[\s\S]{0,500}?<span\s+class=["']h6["'][^>]*title=["']([^"']{2,80})["']/i
|
||||
);
|
||||
if (serviceMatch && serviceMatch[1]) {
|
||||
service = serviceMatch[1].trim();
|
||||
}
|
||||
|
||||
// Login / identifiant court (optionnel)
|
||||
let login = null;
|
||||
const loginPatterns = [
|
||||
/data-user-login\s*=\s*["']([^"']+)["']/i,
|
||||
@@ -265,8 +281,8 @@ async function fetchCurrentUser(origin, phpsessid) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!name && !login) return null;
|
||||
return { name, login };
|
||||
if (!name && !login && !service) return null;
|
||||
return { name, login, service };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Planning Techniciens — Vue claire",
|
||||
"version": "4.2.1",
|
||||
"description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.2.1 : messages d'erreur clairs (session expirée vs EasyVista inaccessible) avec bouton Ouvrir EasyVista et Réessayer, vouvoiement uniformisé. Inclut v4.2.0 : contact + personne de contact sur site avec anomalie rouge, parser téléphone élargi (41XXX sans +), sélection texte dans la bulle sans épingler, utilisateur EV connecté en haut, suppression auto-refresh 12h/15h.",
|
||||
"version": "4.2.3",
|
||||
"description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.2.3 : titre renommé 'Planification', pastille d'initiales utilisateur à gauche (clic = popup nom complet), timeline petite popup qui suit la souris, clic timeline = grande popup persistante sous la timeline, double-clic = ouvre fiche, Ctrl+clic = fiche en arrière-plan, 2 contacts séparés par 'et' affichés sur 2 lignes, numéros courts 5 chiffres commençant par 6/7/8 avec espaces reconnus.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"scripting",
|
||||
|
||||
+63
@@ -1453,3 +1453,66 @@ html, body {
|
||||
.current-user.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
v4.2.3 : pastille utilisateur (initiales) dans la topbar gauche
|
||||
───────────────────────────────────────────────────────────────────────── */
|
||||
.user-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
margin-right: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
color: #fff;
|
||||
background: var(--user-badge-color, #5b6372);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s, box-shadow 0.12s;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
.user-badge:hover {
|
||||
transform: scale(1.06);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.user-badge:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
.user-badge.hidden {
|
||||
display: none;
|
||||
}
|
||||
.user-badge.open {
|
||||
/* Indication visuelle quand la popup nom est ouverte */
|
||||
box-shadow: 0 0 0 2px var(--user-badge-color, #5b6372) inset,
|
||||
0 0 0 2px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Popup du nom complet, affichée juste sous la pastille au clic */
|
||||
.user-name-popup {
|
||||
position: fixed;
|
||||
z-index: 10050;
|
||||
padding: 8px 14px;
|
||||
background: var(--bg, #ffffff);
|
||||
color: var(--text, #111);
|
||||
border: 1px solid var(--border, rgba(128, 128, 128, 0.25));
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18),
|
||||
0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
animation: user-popup-in 0.12s ease-out;
|
||||
}
|
||||
.user-name-popup.hidden {
|
||||
display: none;
|
||||
}
|
||||
@keyframes user-popup-in {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
+11
-3
@@ -2,13 +2,18 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Planning techniciens</title>
|
||||
<title>Planification</title>
|
||||
<link rel="stylesheet" href="viewer.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="topbar-left">
|
||||
<h1>Planning techniciens</h1>
|
||||
<!-- v4.2.3 : pastille avec initiales de l'utilisateur connecté, avant
|
||||
le titre. Clic → popup fixe avec nom complet juste en dessous. -->
|
||||
<button id="user-badge" class="user-badge hidden"
|
||||
type="button" aria-label="Utilisateur connecté"
|
||||
title="Utilisateur connecté"></button>
|
||||
<h1>Planification</h1>
|
||||
<div class="date-nav">
|
||||
<button id="nav-prev" class="btn btn-nav" title="Jour précédent" aria-label="Jour précédent">◀</button>
|
||||
<input type="date" id="date-picker" class="date-input">
|
||||
@@ -19,7 +24,6 @@
|
||||
<span id="refresh-check" class="refresh-check hidden" title="Mise à jour terminée">✓</span>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<span id="current-user" class="current-user hidden" title="Utilisateur EasyVista connecté"></span>
|
||||
<button id="refresh-partial-btn" class="btn btn-refresh" title="Actualiser : ajoute les nouvelles interventions et retire celles qui ne sont plus dans le planning. Rapide, ne re-télécharge pas les fiches déjà connues.">
|
||||
<svg id="refresh-partial-icon" class="btn-refresh-icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2 8a6 6 0 0 1 10.2-4.24M14 3v3h-3" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span class="btn-refresh-label">Actualiser</span>
|
||||
@@ -77,6 +81,10 @@
|
||||
|
||||
<div id="tooltip" class="tooltip hidden" role="tooltip"></div>
|
||||
|
||||
<!-- v4.2.3 : popup fixe du nom de l'utilisateur connecté. S'ouvre au clic
|
||||
sur la pastille d'initiales (topbar gauche). -->
|
||||
<div id="user-name-popup" class="user-name-popup hidden" role="dialog" aria-hidden="true"></div>
|
||||
|
||||
<!-- Conteneur des toasts (notifications d'ouverture) -->
|
||||
<div id="toast-stack" class="toast-stack" aria-live="polite"></div>
|
||||
|
||||
|
||||
@@ -251,22 +251,91 @@ async function fetchAndShowCurrentUser() {
|
||||
try {
|
||||
const resp = await sendMessage({ type: "fetchCurrentUser" });
|
||||
if (!resp || !resp.ok || !resp.user) return;
|
||||
const el = document.getElementById("current-user");
|
||||
if (!el) return;
|
||||
const label = resp.user.name || resp.user.login || null;
|
||||
if (!label) return;
|
||||
el.textContent = label;
|
||||
el.title = resp.user.login
|
||||
? `Utilisateur EasyVista connecté : ${label} (${resp.user.login})`
|
||||
: `Utilisateur EasyVista connecté : ${label}`;
|
||||
el.classList.remove("hidden");
|
||||
// Exposer au reste du code pour un usage éventuel plus tard
|
||||
const badge = document.getElementById("user-badge");
|
||||
if (!badge) return;
|
||||
const fullName = resp.user.name || resp.user.login || null;
|
||||
if (!fullName) return;
|
||||
const initials = computeUserInitials(fullName);
|
||||
badge.textContent = initials;
|
||||
badge.title = fullName;
|
||||
// v4.2.3 : couleur unique dérivée du nom, dans la palette neutre du thème
|
||||
badge.style.setProperty("--user-badge-color", colorFromName(fullName));
|
||||
badge.classList.remove("hidden");
|
||||
state.currentUser = resp.user;
|
||||
} catch (err) {
|
||||
console.warn("[currentUser] fetch failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// v4.2.3 : calcule les initiales depuis un nom au format "Nom, Prénom" ou
|
||||
// "Nom Prénom" ou "Prénom Nom". On prend la 1re lettre majuscule de chaque
|
||||
// mot/segment significatif, limité à 2 caractères.
|
||||
function computeUserInitials(fullName) {
|
||||
if (!fullName) return "?";
|
||||
// Format "Nom, Prénom" → prendre initiale avant virgule et après
|
||||
let parts;
|
||||
if (fullName.includes(",")) {
|
||||
parts = fullName.split(",").map(s => s.trim()).filter(Boolean);
|
||||
} else {
|
||||
parts = fullName.split(/\s+/).filter(Boolean);
|
||||
}
|
||||
const letters = parts
|
||||
.map(p => p.charAt(0))
|
||||
.filter(c => /[A-Za-zÀ-ÿ]/.test(c))
|
||||
.slice(0, 2)
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
return letters || (fullName.charAt(0).toUpperCase() || "?");
|
||||
}
|
||||
|
||||
// v4.2.3 : couleur déterministe à partir du nom. Palette neutre et sobre
|
||||
// (tons tamisés), compatible avec les thèmes clair et sombre de l'extension.
|
||||
function colorFromName(name) {
|
||||
// Hash simple (djb2) pour dériver un index stable
|
||||
let h = 5381;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff;
|
||||
}
|
||||
const palette = [
|
||||
"#5b6372", // gris bleuté
|
||||
"#6b7280", // gris neutre
|
||||
"#4a5568", // ardoise
|
||||
"#3b5a72", // bleu profond tamisé
|
||||
"#4f6a5e", // vert sauge sombre
|
||||
"#6b5a4f", // brun taupe
|
||||
"#5d4a6b", // prune sombre
|
||||
"#6a5a3a", // kaki bronze
|
||||
"#3a5a5e", // sarcelle sombre
|
||||
"#6c5c67" // mauve grisé
|
||||
];
|
||||
return palette[Math.abs(h) % palette.length];
|
||||
}
|
||||
|
||||
// v4.2.3 : affiche/masque la popup nom complet sous la pastille
|
||||
function toggleUserNamePopup() {
|
||||
const badge = document.getElementById("user-badge");
|
||||
const popup = document.getElementById("user-name-popup");
|
||||
if (!badge || !popup) return;
|
||||
if (!popup.classList.contains("hidden")) {
|
||||
hideUserNamePopup();
|
||||
return;
|
||||
}
|
||||
if (!state.currentUser || !state.currentUser.name) return;
|
||||
popup.textContent = state.currentUser.name;
|
||||
popup.classList.remove("hidden");
|
||||
badge.classList.add("open");
|
||||
// Positionne juste en dessous de la pastille
|
||||
const r = badge.getBoundingClientRect();
|
||||
popup.style.left = Math.max(8, r.left) + "px";
|
||||
popup.style.top = (r.bottom + 6) + "px";
|
||||
}
|
||||
function hideUserNamePopup() {
|
||||
const popup = document.getElementById("user-name-popup");
|
||||
const badge = document.getElementById("user-badge");
|
||||
if (popup) popup.classList.add("hidden");
|
||||
if (badge) badge.classList.remove("open");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Thème clair/sombre
|
||||
// ============================================================================
|
||||
@@ -337,6 +406,26 @@ function bindTopbar() {
|
||||
if (e.target.value) loadForDate(e.target.value);
|
||||
});
|
||||
|
||||
// v4.2.3 : clic sur la pastille d'initiales → toggle popup nom complet
|
||||
const userBadge = document.getElementById("user-badge");
|
||||
if (userBadge) {
|
||||
userBadge.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
toggleUserNamePopup();
|
||||
});
|
||||
}
|
||||
// Clic ailleurs ou touche Escape ferme la popup
|
||||
document.addEventListener("click", (e) => {
|
||||
const popup = document.getElementById("user-name-popup");
|
||||
if (!popup || popup.classList.contains("hidden")) return;
|
||||
// Ne pas fermer si le clic est dans la popup elle-même ou sur le badge
|
||||
if (e.target.closest("#user-name-popup") || e.target.closest("#user-badge")) return;
|
||||
hideUserNamePopup();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") hideUserNamePopup();
|
||||
});
|
||||
|
||||
document.getElementById("open-ev-btn").addEventListener("click", openEasyVista);
|
||||
|
||||
// v4.2 : écran "EasyVista inaccessible"
|
||||
@@ -2322,8 +2411,152 @@ function getStatusClass(iv) {
|
||||
|
||||
function bindTimelinePopover(el) {
|
||||
el.addEventListener("mouseenter", (e) => showTimelinePopover(e, el));
|
||||
el.addEventListener("mousemove", moveTooltip);
|
||||
// v4.2.3 : la petite popup timeline SUIT la souris (différent de la grande
|
||||
// popup des lignes d'intervention qui est ancrée). On n'utilise pas
|
||||
// moveTooltip() (no-op depuis v4.1.12) mais une fonction dédiée.
|
||||
el.addEventListener("mousemove", (e) => moveTimelineTooltip(e));
|
||||
el.addEventListener("mouseleave", hideTooltip);
|
||||
|
||||
// v4.2.3 : clic / double-clic / Ctrl+clic sur un segment timeline
|
||||
// - clic simple : ferme la petite popup et ouvre la GRANDE popup
|
||||
// (ancrée juste en dessous de la timeline, persistante pour permettre
|
||||
// de sélectionner du texte / copier)
|
||||
// - double-clic : ouvre la fiche EasyVista dans un nouvel onglet actif
|
||||
// - Ctrl+clic (Cmd+clic sur Mac) : ouvre dans un onglet en arrière-plan
|
||||
const kind = el.dataset.kind;
|
||||
const ivIdxStr = el.dataset.ivIdx;
|
||||
// Seulement sur les segments avec une intervention (pas les "hole" libres
|
||||
// ni certaines absences sans ivIdx)
|
||||
if (ivIdxStr === undefined) return;
|
||||
|
||||
let singleClickTimer = null;
|
||||
el.addEventListener("click", (e) => {
|
||||
// Ctrl / Cmd / molette → ouvrir fiche en arrière-plan
|
||||
if (e.ctrlKey || e.metaKey || e.button === 1) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openInterventionFromTimeline(el, { background: true });
|
||||
return;
|
||||
}
|
||||
// Clic simple (sans Ctrl) : on attend un éventuel double-clic avant
|
||||
// d'ouvrir la grande popup persistante.
|
||||
e.stopPropagation();
|
||||
if (singleClickTimer) clearTimeout(singleClickTimer);
|
||||
singleClickTimer = setTimeout(() => {
|
||||
singleClickTimer = null;
|
||||
openPersistentTimelinePopup(el);
|
||||
}, 250);
|
||||
});
|
||||
el.addEventListener("dblclick", (e) => {
|
||||
// Annuler le clic simple en attente
|
||||
if (singleClickTimer) { clearTimeout(singleClickTimer); singleClickTimer = null; }
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openInterventionFromTimeline(el, { background: false });
|
||||
});
|
||||
}
|
||||
|
||||
// v4.2.3 : positionne la petite popup timeline à côté du curseur
|
||||
function moveTimelineTooltip(e) {
|
||||
const tip = tooltipEl();
|
||||
if (!tip || !tip.classList.contains("visible")) return;
|
||||
// La popup ancrée (grande bulle) ne doit pas être déplacée par la souris
|
||||
if (bulleState.pinned) return;
|
||||
// Si la popup affiche une grande bulle d'intervention (classe pinned-like),
|
||||
// on ne la bouge pas non plus : on la laisse ancrée.
|
||||
if (tip.dataset.mode === "anchored") return;
|
||||
const offsetX = 14, offsetY = 16;
|
||||
let x = e.clientX + offsetX;
|
||||
let y = e.clientY + offsetY;
|
||||
const rect = tip.getBoundingClientRect();
|
||||
// Ajuster si on sort de la fenêtre
|
||||
if (x + rect.width > window.innerWidth - 8) x = e.clientX - rect.width - offsetX;
|
||||
if (y + rect.height > window.innerHeight - 8) y = e.clientY - rect.height - offsetY;
|
||||
if (x < 4) x = 4;
|
||||
if (y < 4) y = 4;
|
||||
tip.style.left = x + "px";
|
||||
tip.style.top = y + "px";
|
||||
currentTooltipPos = { x, y };
|
||||
}
|
||||
|
||||
// v4.2.3 : trouve l'iv correspondant au segment timeline et ouvre sa fiche
|
||||
function openInterventionFromTimeline(el, opts) {
|
||||
const ivIdx = el.dataset.ivIdx;
|
||||
if (ivIdx === undefined) return;
|
||||
const cardEl = el.closest(".card");
|
||||
if (!cardEl) return;
|
||||
const row = cardEl.querySelector(`.intervention-v2[data-iv-idx="${ivIdx}"]`);
|
||||
if (!row) return;
|
||||
const actionId = row.dataset.actionId;
|
||||
if (!actionId) return;
|
||||
// Récupère l'iv depuis state
|
||||
const iv = findIvByActionId(actionId);
|
||||
if (!iv) return;
|
||||
openInterventionInNewTab(iv, opts || {});
|
||||
}
|
||||
|
||||
function findIvByActionId(actionId) {
|
||||
const data = state.currentData;
|
||||
if (!data || !data.techs) return null;
|
||||
for (const tech of data.techs) {
|
||||
for (const iv of (tech.interventions || [])) {
|
||||
if (String(iv.actionId) === String(actionId)) return iv;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// v4.2.3 : ouvre la GRANDE popup (comme au hover sur une ligne) mais ancrée
|
||||
// juste en dessous du segment timeline cliqué, et épinglée pour qu'elle
|
||||
// reste ouverte et autorise la sélection de texte.
|
||||
function openPersistentTimelinePopup(el) {
|
||||
const ivIdx = el.dataset.ivIdx;
|
||||
if (ivIdx === undefined) return;
|
||||
const cardEl = el.closest(".card");
|
||||
if (!cardEl) return;
|
||||
const row = cardEl.querySelector(`.intervention-v2[data-iv-idx="${ivIdx}"]`);
|
||||
if (!row) return;
|
||||
const actionId = row.dataset.actionId;
|
||||
const iv = findIvByActionId(actionId);
|
||||
if (!iv) return;
|
||||
|
||||
// Fermer toute popup en cours (petite timeline ou grande bulle)
|
||||
const tip = tooltipEl();
|
||||
if (!tip) return;
|
||||
// Reset de l'état pour qu'un hideTooltip forcé ne soit pas bloqué
|
||||
bulleState.pinned = false;
|
||||
hideTooltip({ force: true });
|
||||
// Reset position mémorisée pour éviter re-application au scroll sur
|
||||
// l'ancienne position
|
||||
currentTooltipPos = null;
|
||||
|
||||
// Construit et affiche la grande bulle, ancrée sous la timeline
|
||||
tip.innerHTML = buildTooltipHTML(iv);
|
||||
tip.classList.remove("hidden");
|
||||
tip.classList.add("visible");
|
||||
tip.dataset.mode = "anchored";
|
||||
state.currentTooltipIv = iv;
|
||||
|
||||
// Position : juste en dessous de l'élément timeline, aligné à gauche
|
||||
const r = el.getBoundingClientRect();
|
||||
const tipRect = tip.getBoundingClientRect();
|
||||
let x = r.left;
|
||||
let y = r.bottom + 8;
|
||||
if (x + tipRect.width > window.innerWidth - 8) x = window.innerWidth - tipRect.width - 8;
|
||||
if (x < 4) x = 4;
|
||||
if (y + tipRect.height > window.innerHeight - 8) {
|
||||
// Pas assez de place en bas : on met au-dessus de la timeline
|
||||
y = r.top - tipRect.height - 8;
|
||||
}
|
||||
if (y < 4) y = 4;
|
||||
tip.style.left = x + "px";
|
||||
tip.style.top = y + "px";
|
||||
currentTooltipPos = { x, y };
|
||||
|
||||
// Épingler pour que la popup reste ouverte (elle ne se fermera que sur
|
||||
// Échap, clic sur ✕ ou double-Ctrl). Permet aussi la sélection de texte.
|
||||
bulleState.pinned = true;
|
||||
tip.classList.add("pinned");
|
||||
}
|
||||
|
||||
function showTimelinePopover(e, el) {
|
||||
@@ -2354,10 +2587,15 @@ function showTimelinePopover(e, el) {
|
||||
</dl>`;
|
||||
}
|
||||
const tip = tooltipEl();
|
||||
// v4.2.3 : si une grande bulle est déjà épinglée via clic timeline, on ne
|
||||
// la remplace pas par la petite popup hover.
|
||||
if (bulleState.pinned && tip.dataset.mode === "anchored") return;
|
||||
tip.innerHTML = html;
|
||||
tip.classList.remove("hidden");
|
||||
tip.classList.remove("hidden", "pinned");
|
||||
tip.classList.add("visible");
|
||||
moveTooltip(e);
|
||||
// v4.2.3 : mode "hover" = petite popup qui suit la souris
|
||||
tip.dataset.mode = "hover";
|
||||
moveTimelineTooltip(e);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -2785,9 +3023,15 @@ function extractContacts(raw) {
|
||||
// Virer les labels parasites (Nom utilisateur, etc.) qui traînent
|
||||
s = s.replace(/\b(Nom utilisateur|Utilisateur)\s*:\s*[^\n]+/gi, "");
|
||||
|
||||
// Séparer sur " ou ", " / ", retours à la ligne
|
||||
// Mais attention : "Nom, Prénom" contient une virgule qu'on ne doit pas découper
|
||||
const parts = s.split(/\s+ou\s+|\n+|\s*\/\s*(?=[A-ZÉÈÀÂÎÔÛÇ])/i)
|
||||
// v4.2.3 : séparer sur plus de délimiteurs pour gérer les cas type
|
||||
// "Nom1 Prénom1 +41XXXXXXXXX et Nom2 Prénom2 0XXXXXXXXX"
|
||||
// Délimiteurs acceptés :
|
||||
// - " ou " / " et " / " and " (mots de liaison)
|
||||
// - " / " suivi d'une majuscule (nouveau contact)
|
||||
// - " ; " (point-virgule)
|
||||
// - saut de ligne
|
||||
// IMPORTANT : on ne touche PAS aux virgules (car "Nom, Prénom" en contient).
|
||||
const parts = s.split(/\s+ou\s+|\s+et\s+|\s+and\s+|\s*;\s*|\n+|\s*\/\s*(?=[A-ZÉÈÀÂÎÔÛÇ])/i)
|
||||
.map(p => p.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -2829,10 +3073,16 @@ function splitOneContact(raw) {
|
||||
// +41 XX XXX XX XX). On exige exactement 11 chiffres collés pour
|
||||
// éviter de matcher des codes postaux ou autres nombres.
|
||||
const rxLong = /(\+41\s?\d(?:[\d\s.\-]*\d)?|\+33\s?\d(?:[\d\s.\-]*\d)?|0\d(?:[\d\s.\-]*\d)?|(?<!\d)41\d{9}(?!\d)|(?<!\d)33\d{9}(?!\d))/g;
|
||||
// SHORT : 5 chiffres isolés. v4.1.20 : on retire l'exigence de séparateur
|
||||
// après pour tolérer "12345Texte". Avant = début, whitespace ou
|
||||
// parenthèse (on évite de prendre 5 chiffres au milieu d'un long).
|
||||
const rxShort = /(?:^|[\s(\/])(\d{5})(?!\d)/g;
|
||||
// SHORT : numéro interne court (5 chiffres).
|
||||
// - v4.1.20 : accepte "12345Texte" (pas de séparateur après)
|
||||
// - v4.2.3 : accepte aussi les formats AVEC ESPACES au sein du numéro,
|
||||
// typique du Canton de Vaud : "7 68 43", "6 12 34", "8 90 12".
|
||||
// Doit commencer par 6, 7 ou 8 (plan de numérotation interne VD).
|
||||
// Pattern : [678] + (4 autres chiffres, avec ou sans espaces/points
|
||||
// intercalés, mais pas plus d'un séparateur à la fois entre 2 chiffres).
|
||||
// - v4.2.3 : la version "collée" classique (5 chiffres sans espace, tout
|
||||
// chiffre de début) reste acceptée comme fallback.
|
||||
const rxShort = /(?:^|[\s(\/])([678](?:[\s.\-]?\d){4})(?!\d)|(?:^|[\s(\/])(\d{5})(?!\d)/g;
|
||||
|
||||
// Trouver toutes les positions de match pour LONG et SHORT
|
||||
const matches = [];
|
||||
@@ -2846,12 +3096,18 @@ function splitOneContact(raw) {
|
||||
}
|
||||
}
|
||||
while ((mm = rxShort.exec(raw)) !== null) {
|
||||
const shortTel = mm[1];
|
||||
const shortStart = mm.index + mm[0].indexOf(shortTel);
|
||||
const shortEnd = shortStart + shortTel.length;
|
||||
const overlaps = matches.some(x => shortStart < x.end && shortEnd > x.start);
|
||||
// v4.2.3 : soit le 1er groupe (format avec espaces "7 68 43"), soit le
|
||||
// 2e groupe (format collé "12345") a matché.
|
||||
const rawTel = mm[1] || mm[2];
|
||||
if (!rawTel) continue;
|
||||
// On normalise en 5 chiffres sans séparateur
|
||||
const shortTel = rawTel.replace(/[\s.\-]/g, "");
|
||||
if (!/^\d{5}$/.test(shortTel)) continue;
|
||||
const rawStart = mm.index + mm[0].indexOf(rawTel);
|
||||
const rawEnd = rawStart + rawTel.length;
|
||||
const overlaps = matches.some(x => rawStart < x.end && rawEnd > x.start);
|
||||
if (!overlaps) {
|
||||
matches.push({ start: shortStart, end: shortEnd, tel: shortTel });
|
||||
matches.push({ start: rawStart, end: rawEnd, tel: shortTel });
|
||||
}
|
||||
}
|
||||
matches.sort((a, b) => a.start - b.start);
|
||||
|
||||
Reference in New Issue
Block a user