From be49a890578ec7f8191e5e8a90d94c5f9f38838e Mon Sep 17 00:00:00 2001 From: Quentin Rouiller Date: Sat, 18 Apr 2026 15:00:00 +0200 Subject: [PATCH] =?UTF-8?q?Version=204.1.6=20=E2=80=94=20Am=C3=A9lioration?= =?UTF-8?q?s=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.json | 4 +- viewer.js | 168 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 156 insertions(+), 16 deletions(-) diff --git a/manifest.json b/manifest.json index 1a995fd..3027d2f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "Planning Techniciens — Vue claire", - "version": "4.1.4", - "description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch). v4.1.4 : fetch fiche fraîche à chaque clic (checksum pas en cache) + logging détaillé console pour diagnostiquer le checksum.", + "version": "4.1.6", + "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": [ "activeTab", "scripting", diff --git a/viewer.js b/viewer.js index 8afaa04..120d7d8 100644 --- a/viewer.js +++ b/viewer.js @@ -1013,9 +1013,15 @@ async function fetchAndUpdateIntervention(iv) { 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.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 // actions hors-standard), on prend celle de la fiche. 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 // lazy-load xhr2 au hover. -function parseFicheHtml(html) { +function parseFicheHtml(html, techFullName) { const out = { status: 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) @@ -1155,19 +1165,144 @@ function parseFicheHtml(html) { m = html.match(/"dbFieldName"\s*:\s*"RFC_NUMBER"[^}]*?"value"\s*:\s*"([^"]{5,30})"/); if (m) out.rfc = m[1]; - // Commentaire tech à la fin de DESCRIPTION : "

techN: ..." - m = html.match(/"dbFieldName"\s*:\s*"DESCRIPTION"[^}]*?"value"\s*:\s*"((?:[^"\\]|\\.)+)"/); - if (m) { - const desc = decodeJsonString(m[1]); - const ctm = desc.match(/
\s*
\s*([a-z][a-z0-9]{2,14})\s*:\s*([^<]{3,500})/i); - if (ctm) { - out.commentaireTech = ctm[1] + ": " + ctm[2].trim(); + // ─── Extraction des blocs AM_ACTION-DESCRIPTION ───────────────────────── + // Dans la nouvelle version d'EasyVista, le texte d'action est dans + //
CONTENU
...
+ // sous la section field-label-id="AM_ACTION-DESCRIPTION". + // + // Il peut y avoir PLUSIEURS sections (une par action liée à la demande). + // 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" ... + //
TEXTE DE L'ACTION
← 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 = /]*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; } +/** + * Nettoie un bloc HTML pour obtenir du texte brut lisible. + * -
(avec ou sans attributs) → \n + * - entités HTML décodées (  > etc.) + * - tags HTML restants supprimés + * - espaces multiples compactés + */ +function cleanHtmlBlock(html) { + if (!html) return ""; + let s = html; + //
,
,
,
→ \n + s = s.replace(/]*>/gi, "\n"); + // Entités HTML + s = s.replace(/ /g, " ") + .replace(/>/g, ">") + .replace(/</g, "<") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&/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(/ /g, " ") + .replace(/>/g, ">") + .replace(/</g, "<") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/'/g, "'") + .replace(/&/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) { return s .replace(/\\r/g, "") @@ -2628,10 +2763,15 @@ function buildTooltipHTML(iv) { // Deadline (si connue et différente) if (iv.deadline && !i.date) rows.push(row("Deadline", iv.deadline)); - // Commentaire du tech (si présent dans DESCRIPTION de la fiche) - if (iv.commentaireTech) { + // v4.1.5 : textes d'action de la fiche EasyVista assignés à ce tech + // (potentiellement plusieurs si le tech a plusieurs actions sur ce ticket). + if (iv.ficheActionTexts && iv.ficheActionTexts.length > 0) { rows.push(`
`); - rows.push(`
Commentaire tech
${escapeHtml(iv.commentaireTech)}
`); + for (const txt of iv.ficheActionTexts) { + // Afficher chaque texte en respectant les sauts de ligne + const html = escapeHtml(txt).replace(/\n/g, "
"); + rows.push(`
Action
${html}
`); + } } if (iv.ref) {