v2026.5.50 : texte des actions rétabli (format EasyVista du 29.09), catégories Roll Out et Déménagement

- Timeline et fiche : la cellule JSON qui porte ACTION_ID est repérée par son
  contenu, la description par le nom de sa colonne (EasyVista est passé de 14 à
  13 cellules par action et le texte des actions ne s'affichait plus).
- Format EasyVista non reconnu : avertissement unique dans la console ; fiche ou
  historique non chargé journalisé.
- Réservations : catégories Roll Out et Déménagement, propres à la Planification
  (EasyVista reste « Evènements spéciaux ») ; catégorie inconnue conservée à
  l'édition ; proposée à la saisie du n° d'une intervention Roll Out.
- firefox-updates.json : 2026.5.50, xpi signé par AMO.
This commit is contained in:
FroSteel
2026-09-29 13:18:02 +02:00
parent a1c7f45b03
commit a37b90aa13
5 changed files with 151 additions and 27 deletions
+114 -24
View File
@@ -6624,7 +6624,13 @@ async function submitAbsence(opts) {
// ville, rue, catégorie) au créneau, propagées par le merge et affichées dans
// la bulle. Les réservations créées directement dans EV restent inchangées.
const RESERVATION_TYPE_GUID = "{E7432422-55CB-4DB9-8A26-619D036E2155}"; // Evènements spéciaux
const RESERVATION_CATEGORIES = ["Installation", "Remplacement", "Récupération", "Incident"];
// v2026.5.50 — « Roll Out » et « Déménagement » : catégories propres à la
// Planification (méta du carnet, partagée par le dossier) ; côté EasyVista, la
// réservation reste « Evènements spéciaux ».
const RESERVATION_CATEGORIES = ["Installation", "Remplacement", "Récupération", "Incident", "Roll Out", "Déménagement"];
// Celles que seule la Planification repère (texte de l'action) : la fiche EV,
// qui ne connaît que le catalogue, ne doit pas les remplacer.
const CATEGORIES_PROPRES_PLANIFICATION = ["Roll Out", "Déménagement"];
// v2026.5.46 — GUID du type "Pompier" (utilisé par le popup pompier de la
// modal Réservation). Doit rester aligné avec HOLIDAY_TYPES.
const POMPIER_TYPE_GUID = "{60D70502-063D-45AD-9415-25C1C556105F}";
@@ -6638,7 +6644,8 @@ function _resTypeColorKey(cat) {
case "récupération":
case "recuperation": return "recup";
case "incident": return "incident";
default: return null;
case "roll out": return "rollout";
default: return null; // dont « Déménagement » : couleur par défaut des réservations
}
}
@@ -6653,7 +6660,11 @@ function _lookupInterventionInfo(num) {
if (String(iv.ref || "").trim() === n || String(iv.actionId || "").trim() === n) {
const cat = (iv.categoryLine || "").toLowerCase();
let categorie = "";
if (/install/.test(cat)) categorie = "Installation";
// Une réf I… (incident) ne reçoit pas les catégories propres à la Planification.
const refIncident = /^I\d/.test(iv.ref || "");
if (!refIncident && isRollOut(iv)) categorie = "Roll Out";
else if (!refIncident && /d[ée]m[ée]nag/.test(cat)) categorie = "Déménagement";
else if (/install/.test(cat)) categorie = "Installation";
else if (/remplac/.test(cat)) categorie = "Remplacement";
else if (/r[ée]cup/.test(cat)) categorie = "Récupération";
else if (/incident/.test(cat)) categorie = "Incident";
@@ -6772,7 +6783,13 @@ async function _fetchReservationInfo(num) {
const titM = html.match(/"dbFieldName"\s*:\s*"TITLE_FR"[^}]*?"value"\s*:\s*"([^"]{2,300})"/);
if (titM) titreFiche = (decodeJsonString ? decodeJsonString(titM[1]) : titM[1]).trim();
const titreCourt = deriveShortTitle({ ref: n, categoryLine: titreFiche });
const categorie = RESERVATION_CATEGORIES.includes(titreCourt) ? titreCourt : "";
let categorie = RESERVATION_CATEGORIES.includes(titreCourt) ? titreCourt : "";
// Catégories propres à la Planification, repérées dans le titre de la fiche
// (deriveShortTitle ne les connaît pas sans le texte de l'action).
if (!categorie && titreCourt !== "Incident") {
if (/roll\s*-?\s*out/i.test(titreFiche)) categorie = "Roll Out";
else if (/d[ée]m[ée]nag/i.test(titreFiche)) categorie = "Déménagement";
}
let ville = "", rue = "";
const locM = html.match(/"dbFieldName"\s*:\s*"[^"]*(?:LOCATION|LOCALISATION|ADDRESS|ADRESSE)[^"]*"[^}]*?"value"\s*:\s*"([^"]{3,160})"/i)
|| html.match(/Localisation[^>]{0,40}>\s*([^<]{3,160})/i);
@@ -6828,11 +6845,18 @@ function _wireResNumberLookup(numField, cibles) {
if (num === lastNum) return;
lastNum = num;
LOG.info("reservation", `n° saisi : ${num} → recherche infos (planning + EV)`);
apply(_lookupInterventionInfo(num)); // instantané (cache planning)
const infoPlanning = _lookupInterventionInfo(num);
apply(infoPlanning); // instantané (cache planning)
// Roll Out / Déménagement trouvé par le planning pour CE n° : la fiche EV ne
// le voit pas et ne doit pas le remplacer par une catégorie du catalogue.
const categoriePropre = infoPlanning && CATEGORIES_PROPRES_PLANIFICATION.includes(infoPlanning.categorie)
? infoPlanning.categorie : "";
const seq = ++seqRef; // fetch EV réel en arrière-plan
if (loadingBar) loadingBar.classList.remove("hidden");
_fetchReservationInfo(num).then(info => {
if (seq === seqRef) { apply(info); if (loadingBar) loadingBar.classList.add("hidden"); }
if (seq !== seqRef) return;
apply(info && categoriePropre ? { ...info, categorie: categoriePropre } : info);
if (loadingBar) loadingBar.classList.add("hidden");
});
};
numField.addEventListener("change", trigger);
@@ -7064,6 +7088,12 @@ function showReservationEditModal(iv) {
const catSelect = document.createElement("select"); catSelect.className = "modal-form-select";
const emptyOpt = document.createElement("option"); emptyOpt.value = ""; emptyOpt.textContent = "—"; catSelect.appendChild(emptyOpt);
RESERVATION_CATEGORIES.forEach(c => { const o = document.createElement("option"); o.value = c; o.textContent = c; if (c === iv.resCategorie) o.selected = true; catSelect.appendChild(o); });
// Catégorie inconnue de cette version (posée par une version plus récente,
// reçue du dossier) : gardée telle quelle, sinon l'enregistrer l'effacerait
// sur tous les postes (l'édition la plus récente gagne à la fusion).
if (iv.resCategorie && !RESERVATION_CATEGORIES.includes(iv.resCategorie)) {
const o = document.createElement("option"); o.value = iv.resCategorie; o.textContent = iv.resCategorie; o.selected = true; catSelect.appendChild(o);
}
catGroup.appendChild(catSelect); grid.appendChild(catGroup);
const villeField = _mkField("Ville / village", iv.resVille, "");
const rueField = _mkField("Rue / chemin / route", iv.resRue, "");
@@ -9801,10 +9831,10 @@ const RX_LOGIN_COMMENTAIRE = /(?:^|\n|<br\s*\/?>)\s*([a-z0-9_]{3,12})\s*:\s+(\S[
/**
* Extrait toutes les actions d'une fiche en parsant les blocs "rows" du HTML.
* Une action a 14 values. Indices stables :
* Une action a 13 values (14 jusqu'au 28.09.2026). Indices stables :
* [2] = Intervenant (ex: "Nom, Prénom" ou "EZV_WS_REST_USER")
* [4] = Type d'action (ex: "AL-Intervention", "Ajout d'informations")
* [13] = JSON stringifié contenant ACTION_ID, AM_DONE_BY_ID, …
* dernière cellule = JSON stringifié contenant ACTION_ID, AM_DONE_BY_ID, …
*
* v2026.5.45 — la position des dates et de la description varie selon le
* layout EV (anciennement [8]/[9]/[11], aujourd'hui [6]/[7]/[9] sur les
@@ -9850,26 +9880,30 @@ function parseAllActionsFromFicheHtml(html) {
// Une "vraie" action a 14 valeurs. On se contente de 12 minimum
// pour avoir au moins la description.
//
// v2026.5.44 on extrait aussi actionId / amDoneById depuis values[13]
// qui est un JSON stringifié contenant {NAME, ACTION_ID, AM_DONE_BY_ID, …}.
// v2026.5.44 on extrait aussi actionId / amDoneById depuis la cellule JSON
// stringifié qui contient {NAME, ACTION_ID, AM_DONE_BY_ID, …}.
// Permet de filtrer la "bonne action" par ACTION_ID au lieu de matcher
// par nom de tech (le matching par nom échouait par exemple sur les
// actions où l'intervenant est noté "EZV_WS_REST_USER" ou un format
// qui ne correspond pas exactement au tech.name de la config).
let actionId = null;
let amDoneById = null;
if (values.length >= 14 && values[13]) {
// v2026.5.50 — la cellule JSON est repérée par son contenu (la dernière qui
// mentionne ACTION_ID) : EV a retiré la cellule vide qui la précédait
// (29.09.2026), elle est passée de values[13] à values[12].
const valeurJson = [...values].reverse().find(v => v && v.includes("ACTION_ID"));
if (valeurJson) {
// v2026.5.46 — BUG : dans le HTML, les guillemets du JSON imbriqué sont
// encodés " (pas des " littéraux). Le regex cherchait des " littéraux
// → ACTION_ID et surtout AM_DONE_BY_ID étaient TOUJOURS null → le matching
// d'action par id (le plus fiable, indépendant du nom) ne marchait JAMAIS.
// On DÉCODE values[13] d'abord pour que le regex matche.
const raw13 = decodeUnicodeEscapes(values[13]);
// On DÉCODE la cellule d'abord pour que le regex matche.
const jsonDecode = decodeUnicodeEscapes(valeurJson);
// Parsing tolérant via regex (on accepte avec ou sans guillemets autour
// des valeurs numériques) — robuste aux formats JSON avec ou sans
// échappements résiduels.
const mAid = raw13.match(/"ACTION_ID"\s*:\s*"?(\d+)"?/);
const mDid = raw13.match(/"AM_DONE_BY_ID"\s*:\s*"?(\d*)"?/);
const mAid = jsonDecode.match(/"ACTION_ID"\s*:\s*"?(\d+)"?/);
const mDid = jsonDecode.match(/"AM_DONE_BY_ID"\s*:\s*"?(\d*)"?/);
if (mAid) actionId = mAid[1];
if (mDid && mDid[1]) amDoneById = mDid[1];
}
@@ -10733,6 +10767,9 @@ async function fetchAndUpdateIntervention(iv, myToken, opts = {}) {
if (!ficheResp.ok) {
iv.ficheFetched = true;
iv.ficheFetchError = ficheResp.error || "fetch_failed";
if (ficheResp.error !== "session_expired") {
LOG.warn("seq", `iv ${iv.actionId} (ref=${iv.ref}) fiche non chargée : ${iv.ficheFetchError}`);
}
if (ficheResp.error === "session_expired") {
state.session = null;
// session expirée → + bannière + abort de tout le refresh.
@@ -10838,6 +10875,8 @@ async function fetchAndUpdateIntervention(iv, myToken, opts = {}) {
} else if (tlResp && tlResp.error === "session_expired") {
state.session = null;
showSessionExpiredBanner();
} else {
LOG.warn("timeline", `iv ${iv.actionId} : timeline non chargée (${(tlResp && tlResp.error) || "échec"})`);
}
} catch (err) {
LOG.warn("timeline", "erreur iv", iv.actionId, err);
@@ -11169,12 +11208,16 @@ function parseFicheHtml(html) {
* { rows: [
* {value:"..."}, // [0..10] statut, groupe, dates, etc.
* {value:"Date : ... Heure : ... Lieu : ..."}, // [11] DESCRIPTION
* {value:""},
* {value:"{\"ACTION_ID\":\"57700033\",...}"} // [13] JSON stringifié
* {value:"{\"ACTION_ID\":\"57700033\",...}"} // [12] JSON stringifié
* ] }
* ] }}}
*
* On cherche l'action dont rows[13].ACTION_ID === actionId ; si trouvée, on
* v2026.5.50 — jusqu'au 28.09.2026, EV intercalait une cellule vide en [12] et
* le JSON était en [13] (14 cellules) ; depuis, 13 cellules. La cellule JSON est
* repérée par son contenu (la dernière qui mentionne "ACTION_ID"), valable pour
* les deux formats.
*
* On cherche l'action dont le JSON porte ACTION_ID === actionId ; si trouvée, on
* retourne rows[11] nettoyé (br→\n, entités décodées) ; sinon null.
*/
function parseTimelineJsonForAction(jsonText, actionId) {
@@ -11190,15 +11233,23 @@ function parseTimelineJsonForAction(jsonText, actionId) {
const values = data?.data?.data?.values;
if (!Array.isArray(values)) return null;
// Colonne de la description repérée par son nom (« Commentaire ou
// Description ») : une cellule ajoutée ou retirée avant elle ne la décale plus.
const colonnes = data?.data?.data?.columns;
const idxColonneDescription = _indexColonneTimeline(colonnes, /description/i);
const targetId = String(actionId);
let actionsLisibles = 0;
for (const entry of values) {
const rows = entry?.rows;
if (!Array.isArray(rows) || rows.length < 14) continue;
if (!Array.isArray(rows)) continue;
// rows[13] = JSON stringifié qui contient ACTION_ID
const extraRaw = rows[13]?.value;
if (!extraRaw || typeof extraRaw !== "string") continue;
// JSON stringifié qui contient ACTION_ID : la dernière cellule qui le mentionne.
const celluleJson = [...rows].reverse().find(
c => typeof c?.value === "string" && c.value.includes('"ACTION_ID"'));
const extraRaw = celluleJson?.value;
if (!extraRaw) continue;
actionsLisibles++;
let extra;
try {
@@ -11213,15 +11264,54 @@ function parseTimelineJsonForAction(jsonText, actionId) {
if (String(extra.ACTION_ID) !== targetId) continue;
// Trouvé : extraire la description (rows[11]) et la nettoyer.
const rawDesc = rows[11]?.value || extra["AM_ACTION.DESCRIPTION"] || "";
// Trouvé. Description : la colonne nommée, sinon la position historique 11,
// jamais la cellule JSON elle-même ; en dernier recours, le champ du JSON.
const celluleDesc = rows[idxColonneDescription >= 0 ? idxColonneDescription : 11];
const texteCellule = celluleDesc && celluleDesc !== celluleJson &&
typeof celluleDesc.value === "string" ? celluleDesc.value : "";
const rawDesc = texteCellule || extra["AM_ACTION.DESCRIPTION"] || "";
const cleaned = cleanHtmlBlock(rawDesc);
return cleaned || null;
}
// Des actions, mais aucune lisible : EV a changé le format de la timeline.
if (values.length > 0 && actionsLisibles === 0) {
_signalerFormatEvInconnu("timeline", {
nbActions: values.length,
nbCellules: values.map(e => (Array.isArray(e?.rows) ? e.rows.length : null)),
colonnes: Array.isArray(colonnes) ? colonnes.map(_nomColonneTimeline) : "absentes"
});
}
return null;
}
const _CHAMPS_NOM_COLONNE_TIMELINE = ["dbFieldName", "name", "label", "title"];
/** Nom lisible d'une colonne de la timeline EV (le premier champ de nom renseigné). */
function _nomColonneTimeline(colonne) {
const champ = _CHAMPS_NOM_COLONNE_TIMELINE.find(k => typeof colonne?.[k] === "string" && colonne[k]);
return champ ? colonne[champ] : "?";
}
/** Index de la première colonne de la timeline EV dont un nom répond au motif ; -1 sinon. */
function _indexColonneTimeline(colonnes, motif) {
if (!Array.isArray(colonnes)) return -1;
return colonnes.findIndex(c => _CHAMPS_NOM_COLONNE_TIMELINE.some(
k => typeof c?.[k] === "string" && motif.test(c[k])));
}
/**
* Signale UNE fois par session et par source une réponse EV dont le format n'est
* plus reconnu (mise à jour d'EasyVista) : structure seulement (nombres de
* cellules, noms de colonnes), jamais les valeurs.
*/
const _formatsEvInconnusSignales = new Set();
function _signalerFormatEvInconnu(source, structure) {
if (_formatsEvInconnusSignales.has(source)) return;
_formatsEvInconnusSignales.add(source);
LOG.warn(source, "format EasyVista non reconnu : texte des actions indisponible", structure);
}
/**
* Nettoie un bloc HTML pour obtenir du texte brut lisible.