Compare commits

..

2 Commits

Author SHA1 Message Date
FroSteel be49a89057 Version 4.1.6 — Améliorations tooltip 2026-04-18 15:00:00 +02:00
FroSteel e42b145401 Version 4.1.4 — Corrections mineures tooltip 2026-04-18 12:00:00 +02:00
2 changed files with 178 additions and 38 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Planning Techniciens — Vue claire", "name": "Planning Techniciens — Vue claire",
"version": "4.1.3", "version": "4.1.6",
"description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.1.3 : fix ouverture intervention (tentative 3 regex retirée car elle écrasait le bon checksum avec le mauvais).", "description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.1.6 : parsing fiche corrigé pour trouver l'Intervenant via ng-model=colData.value et ignorer les blocs d'action vides (étapes workflow automatiques).",
"permissions": [ "permissions": [
"activeTab", "activeTab",
"scripting", "scripting",
+176 -36
View File
@@ -1013,9 +1013,15 @@ async function fetchAndUpdateIntervention(iv) {
return; return;
} }
const fiche = parseFicheHtml(ficheResp.html); const techFullName = iv.techId ? TEAM[iv.techId] : null;
const fiche = parseFicheHtml(ficheResp.html, techFullName);
iv.status = fiche.status; iv.status = fiche.status;
iv.commentaireTech = fiche.commentaireTech; // v4.1.5 : on stocke tous les textes d'action assignés à ce tech
// (normalement 1, parfois plusieurs). Ils sont affichés dans le tooltip.
iv.ficheActionTexts = fiche.actionTexts || [];
// Rétrocompat : garder commentaireTech à null (champ plus utilisé, on garde
// le nom pour éviter de casser les caches existants avec un champ undefined).
iv.commentaireTech = null;
// Fallback : si l'XML n'avait pas la ref (rare, mais possible pour des // Fallback : si l'XML n'avait pas la ref (rare, mais possible pour des
// actions hors-standard), on prend celle de la fiche. // actions hors-standard), on prend celle de la fiche.
if (fiche.rfc && !iv.ref) { if (fiche.rfc && !iv.ref) {
@@ -1140,11 +1146,15 @@ function isCancelledStatus(s) {
// supprimées car ces infos viennent maintenant du XML attr1/attr2/attr3 ou du // supprimées car ces infos viennent maintenant du XML attr1/attr2/attr3 ou du
// lazy-load xhr2 au hover. // lazy-load xhr2 au hover.
function parseFicheHtml(html) { function parseFicheHtml(html, techFullName) {
const out = { const out = {
status: null, status: null,
rfc: null, rfc: null,
commentaireTech: null // v4.1.5 : liste des textes d'action dont l'Intervenant == techFullName.
// Chaque élément = texte brut nettoyé (Date/Heure/Lieu/Contact/... ou
// commentaires FRD). Normalement 1 seul, mais parfois plusieurs quand la
// fiche a eu plusieurs actions pour le même tech.
actionTexts: []
}; };
// STATUS_FR (valeur parfois encodée en \u00XX) // STATUS_FR (valeur parfois encodée en \u00XX)
@@ -1155,19 +1165,144 @@ function parseFicheHtml(html) {
m = html.match(/"dbFieldName"\s*:\s*"RFC_NUMBER"[^}]*?"value"\s*:\s*"([^"]{5,30})"/); m = html.match(/"dbFieldName"\s*:\s*"RFC_NUMBER"[^}]*?"value"\s*:\s*"([^"]{5,30})"/);
if (m) out.rfc = m[1]; if (m) out.rfc = m[1];
// Commentaire tech à la fin de DESCRIPTION : "<br><br>techN: ..." // ─── Extraction des blocs AM_ACTION-DESCRIPTION ─────────────────────────
m = html.match(/"dbFieldName"\s*:\s*"DESCRIPTION"[^}]*?"value"\s*:\s*"((?:[^"\\]|\\.)+)"/); // Dans la nouvelle version d'EasyVista, le texte d'action est dans
if (m) { // <div class="htmlEditor-base...">CONTENU<br>...<br></div>
const desc = decodeJsonString(m[1]); // sous la section field-label-id="AM_ACTION-DESCRIPTION".
const ctm = desc.match(/<br>\s*<br>\s*([a-z][a-z0-9]{2,14})\s*:\s*([^<]{3,500})/i); //
if (ctm) { // Il peut y avoir PLUSIEURS sections (une par action liée à la demande).
out.commentaireTech = ctm[1] + ": " + ctm[2].trim(); // Chaque section a son propre Intervenant (field-label="Intervenant").
//
// On récupère UNIQUEMENT les textes dont l'intervenant == techFullName.
// ─── Extraction des blocs AM_ACTION-DESCRIPTION ─────────────────────────
// Dans la version actuelle d'EasyVista, chaque action d'une fiche est
// structurée comme suit (dans l'ordre vertical du HTML) :
//
// field-label="Intervenant" ... value="Nom, Prénom" ← le tech
// field-label-id="AM_ACTION-DESCRIPTION" ...
// <div class="htmlEditor-base">TEXTE DE L'ACTION</div> ← contenu
//
// Il peut y avoir PLUSIEURS actions dans la même fiche. Certaines sont
// automatiques (workflow) : leur "intervenant" n'est pas un nom humain
// mais un label d'étape ("1. Analyse...", "2.1 Récupération..."). Ces
// actions ont aussi un htmlEditor-base mais VIDE. On les ignore.
//
// Stratégie :
// 1. Trouver tous les blocs htmlEditor-base NON vides + leur position
// 2. Trouver toutes les valeurs de champ Intervenant (le value= du
// ng-model="colData.value") + leur position
// 3. Pour chaque bloc non vide, trouver l'intervenant le plus proche
// EN AMONT (l'intervenant précède toujours son texte)
// 4. Filtrer par techFullName
// Trouver les intervenants : on cherche le pattern "field-label=\"Intervenant\""
// puis le premier value="..." précédé de ng-model="colData.value".
// Ce 2e pattern saute les autres attributs Angular présents entre les deux.
const intervenants = []; // [{ pos, name }]
const intervLabelRx = /field-label="Intervenant"\s+field-label-id="AM_EMPLOYEE-LAST_NAME"/g;
let il;
while ((il = intervLabelRx.exec(html)) !== null) {
const searchFrom = il.index;
// Chercher le value= associé dans les 20 000 chars suivants
const window = html.substring(searchFrom, searchFrom + 20000);
const vm = window.match(/ng-model="colData\.value"[\s\S]{0,500}?value="([^"]*)"/);
if (vm) {
const name = decodeHtmlEntities(vm[1]).trim();
intervenants.push({ pos: searchFrom, name });
} else {
intervenants.push({ pos: searchFrom, name: null });
} }
} }
// Trouver les blocs htmlEditor-base et leur contenu nettoyé
const editorRx = /<div[^>]*class="htmlEditor-base[^"]*"[^>]*>([\s\S]*?)<\/div>/g;
const blocs = []; // [{ pos, cleaned }]
let em;
while ((em = editorRx.exec(html)) !== null) {
const cleaned = cleanHtmlBlock(em[1]);
// Ignorer les blocs vides ou trop courts (< 10 chars après nettoyage)
if (cleaned && cleaned.length >= 10) {
blocs.push({ pos: em.index, cleaned });
}
}
// Pour chaque bloc non vide, trouver l'intervenant humain le plus proche en
// amont. Si techFullName fourni, garder uniquement les blocs dont
// l'intervenant correspond.
for (const bloc of blocs) {
let closestInterv = null;
for (const iv of intervenants) {
if (iv.pos < bloc.pos && iv.name) {
closestInterv = iv;
} else if (iv.pos >= bloc.pos) {
break;
}
}
if (!closestInterv) continue;
if (techFullName && !namesMatch(closestInterv.name, techFullName)) continue;
out.actionTexts.push(bloc.cleaned);
}
return out; return out;
} }
/**
* Nettoie un bloc HTML pour obtenir du texte brut lisible.
* - <br> (avec ou sans attributs) → \n
* - entités HTML décodées (&nbsp; &gt; etc.)
* - tags HTML restants supprimés
* - espaces multiples compactés
*/
function cleanHtmlBlock(html) {
if (!html) return "";
let s = html;
// <br>, <br/>, <br id="...">, <br style="..."> → \n
s = s.replace(/<br\b[^>]*>/gi, "\n");
// Entités HTML
s = s.replace(/&nbsp;/g, " ")
.replace(/&gt;/g, ">")
.replace(/&lt;/g, "<")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&amp;/g, "&");
// Tags HTML restants
s = s.replace(/<[^>]+>/g, "");
// Espaces compactés, lignes trimmed, lignes vides retirées
s = s.split("\n").map(l => l.trim().replace(/[ \t]+/g, " ")).filter(Boolean).join("\n");
return s;
}
/**
* Décode les entités HTML courantes dans une chaîne courte (ex: un nom).
*/
function decodeHtmlEntities(s) {
if (!s) return s;
return s.replace(/&nbsp;/g, " ")
.replace(/&gt;/g, ">")
.replace(/&lt;/g, "<")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&amp;/g, "&");
}
/**
* Compare deux noms de personne en étant tolérant (casse, accents, espaces,
* espace autour de la virgule).
*/
function namesMatch(a, b) {
if (!a || !b) return false;
const norm = s => String(s)
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/\s+/g, " ")
.replace(/\s*,\s*/g, ",")
.trim();
return norm(a) === norm(b);
}
function decodeJsonString(s) { function decodeJsonString(s) {
return s return s
.replace(/\\r/g, "") .replace(/\\r/g, "")
@@ -1978,23 +2113,16 @@ async function openInterventionInNewTab(iv, opts = {}) {
return; return;
} }
let target = iv.ficheTarget; let target = null;
let checksum = iv.ficheChecksum; let checksum = null;
// SÉCURITÉ : si ficheTarget n'est pas égal à requestId, c'est qu'il vient // v4.1.4 : on fetch TOUJOURS la fiche à la volée au clic pour extraire un
// d'une ancienne version (buggée) du cache. On invalide et on re-fetch. // checksum FRAIS. Ne pas utiliser iv.ficheChecksum du cache : les checksums
if (target && target !== iv.requestId) { // EasyVista peuvent expirer entre le fetch arrière-plan et le clic utilisateur.
console.warn("[click] ficheTarget incohérent :", target, "!=", iv.requestId, "→ re-fetch"); //
target = null; // Retry automatique en cas d'échec du pattern checksum.
checksum = null; {
iv.ficheTarget = null; console.log("[click] fetch fiche fraîche pour iv", iv.actionId, "requestId=", iv.requestId);
iv.ficheChecksum = null;
}
// Si pas encore fetché (ou invalidé), on fetch la fiche à la volée
// avec retry automatique en cas d'échec du pattern checksum
if (!target || !checksum) {
console.log("[click] fetch fiche à la volée pour iv", iv.actionId, "requestId=", iv.requestId);
let attempts = 0; let attempts = 0;
const maxAttempts = 2; const maxAttempts = 2;
while (attempts < maxAttempts && (!target || !checksum)) { while (attempts < maxAttempts && (!target || !checksum)) {
@@ -2012,10 +2140,13 @@ async function openInterventionInNewTab(iv, opts = {}) {
continue; // retry continue; // retry
} }
// Extraire le checksum lié au requestId précis // Extraire le checksum lié au requestId précis
const rx = new RegExp(`target=${iv.requestId}&(?:amp;)?checksum=([a-f0-9]{40})`); const rx = new RegExp(`target=${iv.requestId}&(?:amp;)?checksum=([a-f0-9]{40})`, 'g');
const m = ficheResp.html.match(rx); const allMatches = [...ficheResp.html.matchAll(rx)];
if (!m) { console.log(`[click] Trouvé ${allMatches.length} occurrence(s) de target=${iv.requestId}&checksum=... dans HTML de la fiche (taille ${ficheResp.html.length})`);
console.warn(`[click] tentative ${attempts}: pattern target=${iv.requestId} introuvable dans HTML (taille ${ficheResp.html.length})`); allMatches.forEach((m, idx) => console.log(` [${idx}] checksum = ${m[1]}`));
if (allMatches.length === 0) {
console.warn(`[click] tentative ${attempts}: pattern target=${iv.requestId} introuvable`);
if (attempts >= maxAttempts) { if (attempts >= maxAttempts) {
alert("Impossible de trouver le checksum pour cette fiche (après retry)."); alert("Impossible de trouver le checksum pour cette fiche (après retry).");
return; return;
@@ -2024,8 +2155,11 @@ async function openInterventionInNewTab(iv, opts = {}) {
await new Promise(r => setTimeout(r, 300)); await new Promise(r => setTimeout(r, 300));
continue; continue;
} }
// On prend le PREMIER checksum trouvé (comme avant, comportement v4)
target = iv.requestId; target = iv.requestId;
checksum = m[1]; checksum = allMatches[0][1];
console.log(`[click] checksum retenu: ${checksum}`);
// On stocke aussi en cache pour accélérer le prochain clic (au cas où)
iv.ficheTarget = target; iv.ficheTarget = target;
iv.ficheChecksum = checksum; iv.ficheChecksum = checksum;
} catch (err) { } catch (err) {
@@ -2037,7 +2171,8 @@ async function openInterventionInNewTab(iv, opts = {}) {
} }
} }
// Construire l'URL qui fonctionne (format v3/v4) // Construire l'URL qui fonctionne (format identique à l'URL manuelle qui
// marche dans le navigateur quand on ouvre une fiche depuis l'UI EasyVista).
const internalurltime = Math.floor(Date.now() / 1000); const internalurltime = Math.floor(Date.now() / 1000);
const url = const url =
`${session.origin}/index.php` + `${session.origin}/index.php` +
@@ -2628,10 +2763,15 @@ function buildTooltipHTML(iv) {
// Deadline (si connue et différente) // Deadline (si connue et différente)
if (iv.deadline && !i.date) rows.push(row("Deadline", iv.deadline)); if (iv.deadline && !i.date) rows.push(row("Deadline", iv.deadline));
// Commentaire du tech (si présent dans DESCRIPTION de la fiche) // v4.1.5 : textes d'action de la fiche EasyVista assignés à ce tech
if (iv.commentaireTech) { // (potentiellement plusieurs si le tech a plusieurs actions sur ce ticket).
if (iv.ficheActionTexts && iv.ficheActionTexts.length > 0) {
rows.push(`<hr>`); rows.push(`<hr>`);
rows.push(`<dt>Commentaire tech</dt><dd class="commentaire">${escapeHtml(iv.commentaireTech)}</dd>`); for (const txt of iv.ficheActionTexts) {
// Afficher chaque texte en respectant les sauts de ligne
const html = escapeHtml(txt).replace(/\n/g, "<br>");
rows.push(`<dt>Action</dt><dd class="commentaire">${html}</dd>`);
}
} }
if (iv.ref) { if (iv.ref) {