forked from FroSteel/Planification
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be49a89057 |
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Planning Techniciens — Vue claire",
|
"name": "Planning Techniciens — Vue claire",
|
||||||
"version": "4.1.4",
|
"version": "4.1.6",
|
||||||
"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.",
|
"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",
|
||||||
|
|||||||
@@ -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 ( > 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(/ /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) {
|
function decodeJsonString(s) {
|
||||||
return s
|
return s
|
||||||
.replace(/\\r/g, "")
|
.replace(/\\r/g, "")
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user