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`);
+ 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`);
+ }
}
if (iv.ref) {