Public Access
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:
+114
-24
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user