forked from FroSteel/Planification
Version 3.2.0 — Stabilisation 3.2
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Planning Techniciens — Vue claire",
|
||||
"version": "3.2.0-pre",
|
||||
"version": "3.2.0",
|
||||
"description": "Vue claire du planning EasyVista (itsma.etat-de-vaud.ch et itsma.vd.ch) avec navigation par date, détection automatique des interventions closes et cache 7 jours.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -164,19 +164,41 @@ let state = {
|
||||
// Chaque refresh reçoit un "jeton" (entier incrémenté). Les workers lisent
|
||||
// isRefreshAborted() avant chaque fetch : si le jeton a changé ou si
|
||||
// l'utilisateur a cliqué sur "Arrêter", ils s'arrêtent proprement.
|
||||
//
|
||||
// v3.2 : on ajoute une "abortPromise" par refresh. loadForDate race cette
|
||||
// promesse avec son Promise.all, donc dès qu'on clique Arrêter, loadForDate
|
||||
// sort immédiatement (masque le bouton, fait un toast), même si les fetches
|
||||
// en cours continuent silencieusement. Le changement de token les rend
|
||||
// inoffensifs (ils ne peuvent plus écrire le cache ni updater le DOM).
|
||||
let currentRefreshToken = 0;
|
||||
let abortedToken = -1;
|
||||
let abortResolvers = new Map(); // token → resolve fn of the abort promise
|
||||
|
||||
function startNewRefresh() {
|
||||
currentRefreshToken++;
|
||||
return currentRefreshToken;
|
||||
}
|
||||
function makeAbortPromise(myToken) {
|
||||
return new Promise(resolve => {
|
||||
abortResolvers.set(myToken, resolve);
|
||||
});
|
||||
}
|
||||
function abortCurrentRefresh() {
|
||||
abortedToken = currentRefreshToken;
|
||||
// Réveiller tous les loadForDate en attente (normalement un seul)
|
||||
for (const [token, resolve] of abortResolvers) {
|
||||
if (token <= currentRefreshToken) {
|
||||
resolve("aborted");
|
||||
abortResolvers.delete(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
function isRefreshAborted() {
|
||||
return abortedToken === currentRefreshToken;
|
||||
}
|
||||
function cleanupAbortResolver(myToken) {
|
||||
abortResolvers.delete(myToken);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Boot
|
||||
@@ -253,8 +275,11 @@ function bindTopbar() {
|
||||
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
|
||||
document.getElementById("refresh-btn").addEventListener("click", () => refreshPlanning());
|
||||
document.getElementById("abort-btn").addEventListener("click", () => {
|
||||
abortCurrentRefresh();
|
||||
// Feedback visuel instantané : masquer le bouton tout de suite, sans
|
||||
// attendre que loadForDate finisse sa race.
|
||||
showAbortButton(false);
|
||||
abortCurrentRefresh();
|
||||
showAbortToast();
|
||||
});
|
||||
document.getElementById("clear-cache-btn").addEventListener("click", onClearCache);
|
||||
|
||||
@@ -425,11 +450,12 @@ async function loadForDate(isoDate, opts = {}) {
|
||||
});
|
||||
console.log(`[load] 1er rendu (sans refs) à ${Math.round(performance.now() - t0)} ms`);
|
||||
|
||||
// 5. PARALLÈLE : xhr2 (lieu/contact) + fetches fiches (ref/statut)
|
||||
// Avant v3.1.1 : séquentiel, on devait attendre les 34 xhr2 avant de
|
||||
// lancer les 34 fiches. Résultat : première ref arrivait après ~1s.
|
||||
// Maintenant : les deux démarrent en même temps, chacun met à jour
|
||||
// la ligne correspondante via le rendu incrémental.
|
||||
// 5. SÉQUENTIEL : xhr2 (lieu/contact de la bulle) EN PREMIER,
|
||||
// puis fetches fiches (ref/statut/texte complet) APRÈS.
|
||||
// Raison : le texte complet de la timeline (branche fiches) peut
|
||||
// écraser bulleDescription/bulleContact/bulleLieu. Il ne faut pas
|
||||
// que ça arrive AVANT que la bulle de base soit posée, sinon on se
|
||||
// retrouve avec une popup vide ou incomplète pendant des secondes.
|
||||
const bulleNeeded = [];
|
||||
for (const tech of merged.techs) {
|
||||
for (const iv of tech.interventions) {
|
||||
@@ -439,19 +465,22 @@ async function loadForDate(isoDate, opts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// On refetche les fiches si :
|
||||
// - au moins une intervention n'a jamais été fetchée (pas de ficheTarget), OU
|
||||
// - au moins une intervention n'a pas encore l'actionDescription complète de la fiche
|
||||
// (cas du cache chargé depuis une version antérieure à v3.2)
|
||||
const needFetch = merged.techs.some(tech =>
|
||||
tech.interventions.some(iv =>
|
||||
iv.type === "AL-Intervention" && !iv.ficheTarget
|
||||
iv.type === "AL-Intervention" &&
|
||||
(!iv.ficheTarget || !iv.actionDescriptionFetched)
|
||||
)
|
||||
);
|
||||
|
||||
const promises = [];
|
||||
|
||||
// Étape 5a : xhr2 d'abord (bulle de base : lieu/contact/texte court)
|
||||
if (bulleNeeded.length > 0 && !isRefreshAborted()) {
|
||||
const tBulles = performance.now();
|
||||
console.log(`[load] fetch xhr2 pour ${bulleNeeded.length} interventions…`);
|
||||
promises.push(
|
||||
fetchBullesForInterventions(bulleNeeded).then(() => {
|
||||
await fetchBullesForInterventions(bulleNeeded);
|
||||
console.log(`[load] xhr2 finis en ${Math.round(performance.now() - tBulles)} ms`);
|
||||
if (!isRefreshAborted()) {
|
||||
renderFromData({
|
||||
@@ -461,24 +490,19 @@ async function loadForDate(isoDate, opts = {}) {
|
||||
source: "fresh+bulles"
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Étape 5b : fiches APRÈS les xhr2 (ref/statut + texte complet de timeline)
|
||||
if ((opts.doStatusRefresh || needFetch) && !isRefreshAborted()) {
|
||||
const tFiches = performance.now();
|
||||
const nFiches = merged.techs.flatMap(t=>t.interventions).filter(i=>i.type==="AL-Intervention").length;
|
||||
console.log(`[load] début fetch des ${nFiches} fiches…`);
|
||||
promises.push(
|
||||
refreshStatuses(merged.techs, isoDate).then(() => {
|
||||
await refreshStatuses(merged.techs, isoDate);
|
||||
console.log(`[load] fiches finies en ${Math.round(performance.now() - tFiches)} ms`);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// 6. Sauvegarder dans le cache (une seule fois, après que tout soit enrichi)
|
||||
// Uniquement si on est allé au bout (pas d'annulation).
|
||||
if (!isRefreshAborted()) {
|
||||
await writeCache(isoDate, { techs: merged.techs });
|
||||
}
|
||||
@@ -486,8 +510,15 @@ async function loadForDate(isoDate, opts = {}) {
|
||||
if (!isRefreshAborted()) {
|
||||
showRefreshDone();
|
||||
console.log(`[load] TOTAL: ${Math.round(performance.now() - t0)} ms`);
|
||||
|
||||
// Retry silencieux en arrière-plan pour les interventions dont le texte
|
||||
// d'action n'a pas pu être récupéré (timeline partielle au 1er coup).
|
||||
// Lancé SANS await : l'user peut continuer à utiliser l'extension.
|
||||
// La fonction respecte le token : si l'user change de jour, elle s'arrête.
|
||||
runBackgroundTimelineRetry(merged.techs, isoDate, myToken).catch(() => {});
|
||||
} else {
|
||||
console.log(`[load] annulé par l'utilisateur à ${Math.round(performance.now() - t0)} ms`);
|
||||
showAbortToast();
|
||||
}
|
||||
} finally {
|
||||
// Masquer le bouton "Arrêter" uniquement si c'est NOTRE chargement qui
|
||||
@@ -496,6 +527,7 @@ async function loadForDate(isoDate, opts = {}) {
|
||||
if (currentRefreshToken === myToken) {
|
||||
showAbortButton(false);
|
||||
}
|
||||
cleanupAbortResolver(myToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -878,9 +910,11 @@ async function refreshStatuses(techs, isoDate) {
|
||||
if (!iv.formLink) continue;
|
||||
// On skip si :
|
||||
// - Déjà clos / résolu ET ficheTarget déjà connu (statut + requestId OK)
|
||||
// - Sinon on garde (pour avoir statut frais OU ficheTarget pour clic)
|
||||
// ET actionDescription déjà remplacée depuis la fiche
|
||||
// - Sinon on garde (pour avoir statut frais OU ficheTarget pour clic
|
||||
// OU le texte complet de l'action)
|
||||
const statusClosed = isClosedStatus(iv.status) || isResolvedStatus(iv.status);
|
||||
if (statusClosed && iv.ficheTarget) continue;
|
||||
if (statusClosed && iv.ficheTarget && iv.actionDescriptionFetched) continue;
|
||||
toFetch.push(iv);
|
||||
}
|
||||
}
|
||||
@@ -981,32 +1015,72 @@ async function fetchAndUpdateIntervention(iv) {
|
||||
}
|
||||
iv.commentaireTech = fiche.commentaireTech;
|
||||
|
||||
// ─── Remplacement UNIQUEMENT du texte d'action affiché dans la popup ──────
|
||||
// ─── Remplacement du texte d'action + contact/lieu depuis la fiche ─────────
|
||||
// Le texte de la bulle (planning_xhr_2.php) est parfois tronqué/incomplet.
|
||||
// La fiche contient le texte complet dans AM_ACTION.DESCRIPTION.
|
||||
// SÉCURITÉ : on ne remplace QUE si l'Intervenant de la fiche correspond au
|
||||
// tech de la ligne du planning (car une même fiche peut avoir plusieurs
|
||||
// actions assignées à différents techs, et on fetche la MÊME fiche pour tous).
|
||||
// NB : on NE touche PAS à bulleContact/bulleLieu (ils viennent de la bulle
|
||||
// de base et sont utilisés tels quels ailleurs).
|
||||
if (fiche.actionDescription && fiche.intervenant && iv.techId) {
|
||||
const expectedTechName = TEAM[iv.techId];
|
||||
if (expectedTechName && namesMatch(fiche.intervenant, expectedTechName)) {
|
||||
const expectedTechName = iv.techId ? TEAM[iv.techId] : null;
|
||||
const matchOk = fiche.intervenant && expectedTechName &&
|
||||
namesMatch(fiche.intervenant, expectedTechName);
|
||||
if (fiche.actionDescription && matchOk) {
|
||||
// Remplace le texte d'action (affiché dans la popup)
|
||||
iv.bulleDescription = fiche.actionDescription;
|
||||
iv.actionDescriptionFetched = true; // flag : déjà remplacé depuis la fiche
|
||||
// Reparse contact/lieu depuis le nouveau texte : la carte affiche
|
||||
// bulleContact/bulleLieu, donc il faut les mettre à jour aussi.
|
||||
const infob = parseActionText(fiche.actionDescription);
|
||||
if (infob) {
|
||||
iv.infobulle = infob;
|
||||
if (infob.contact) iv.bulleContact = infob.contact;
|
||||
if (infob.lieu) iv.bulleLieu = infob.lieu;
|
||||
}
|
||||
// Si ça ne matche pas : on garde bulleDescription tel quel (sécurité)
|
||||
}
|
||||
// Si ça ne matche pas : on garde bulleDescription/Contact/Lieu tels quels (sécurité)
|
||||
|
||||
// Extraire le checksum CORRECT pour ouvrir la fiche :
|
||||
// - Le target de la FICHE = iv.requestId (vient du XML)
|
||||
// - Il faut trouver le checksum qui est accolé à ce target dans le HTML
|
||||
// (pattern : target=REQUEST_ID&checksum=XXX...)
|
||||
// La regex principale cherche "target=REQUEST_ID&checksum=XXX" mais peut
|
||||
// échouer si ce pattern n'apparaît pas dans le HTML (selon les sections
|
||||
// hydratées par Angular). On a plusieurs fallbacks robustes.
|
||||
if (iv.requestId) {
|
||||
const rx = new RegExp(`target=${iv.requestId}&(?:amp;)?checksum=([a-f0-9]{40})`);
|
||||
const ckm = ficheResp.html.match(rx);
|
||||
if (ckm) {
|
||||
let checksumFound = false;
|
||||
// Tentative 1 : target=ID&checksum=... (pattern le plus courant dans les liens)
|
||||
const rx1 = new RegExp(`target=${iv.requestId}&(?:amp;)?checksum=([a-f0-9]{40})`);
|
||||
const m1 = ficheResp.html.match(rx1);
|
||||
if (m1) {
|
||||
iv.ficheTarget = iv.requestId;
|
||||
iv.ficheChecksum = ckm[1];
|
||||
iv.ficheChecksum = m1[1];
|
||||
checksumFound = true;
|
||||
} else {
|
||||
// Tentative 2 : dans le JSON formData : "id":"REQUEST_ID"..."checksum":"..."
|
||||
// ou l'inverse : "checksum":"..."..."id":"REQUEST_ID"
|
||||
const rx2a = new RegExp(`"id"\\s*:\\s*"${iv.requestId}"[\\s\\S]{0,200}?"checksum"\\s*:\\s*"([a-f0-9]{40})"`);
|
||||
const m2a = ficheResp.html.match(rx2a);
|
||||
if (m2a) {
|
||||
iv.ficheTarget = iv.requestId;
|
||||
iv.ficheChecksum = m2a[1];
|
||||
checksumFound = true;
|
||||
} else {
|
||||
const rx2b = new RegExp(`"checksum"\\s*:\\s*"([a-f0-9]{40})"[\\s\\S]{0,200}?"id"\\s*:\\s*"${iv.requestId}"`);
|
||||
const m2b = ficheResp.html.match(rx2b);
|
||||
if (m2b) {
|
||||
iv.ficheTarget = iv.requestId;
|
||||
iv.ficheChecksum = m2b[1];
|
||||
checksumFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tentative 3 (ultime) : le checksum global du form principal.
|
||||
if (!checksumFound) {
|
||||
const rx3 = /"form"\s*:\s*\{[^}]*?"checksum"\s*:\s*"([a-f0-9]{40})"[\s\S]{0,2000}?"id"\s*:\s*"(\d+)"/;
|
||||
const m3 = ficheResp.html.match(rx3);
|
||||
if (m3 && m3[2] === String(iv.requestId)) {
|
||||
iv.ficheTarget = iv.requestId;
|
||||
iv.ficheChecksum = m3[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
iv.ficheFetched = true;
|
||||
@@ -1024,8 +1098,15 @@ async function fetchAndUpdateIntervention(iv) {
|
||||
// Étape timeline API : on veut le texte COMPLET de l'action.
|
||||
// planning_xhr_2.php tronque souvent à ~300 chars, mais l'API timeline
|
||||
// retourne le texte intégral. On la fetch à chaque fois que possible.
|
||||
//
|
||||
// PROBLÈME OBSERVÉ : EasyVista retourne parfois une timeline "partielle"
|
||||
// au 1er appel (ex: 8 Ko au lieu de 44 Ko), sans le texte de l'action
|
||||
// courante. Le serveur a besoin de "construire" le contexte après le fetch
|
||||
// de la fiche. Dans ce cas on MARQUE l'intervention pour un retry silencieux
|
||||
// en arrière-plan (fait plus tard par runBackgroundTimelineRetry).
|
||||
const needsTimelineValidation = !iv.actionText;
|
||||
if (needsTimelineValidation && timelineTarget && timelineChecksum) {
|
||||
if (isRefreshAborted()) return;
|
||||
const tlResp = await sendMessage({
|
||||
type: "fetchTimeline",
|
||||
target: timelineTarget,
|
||||
@@ -1034,18 +1115,13 @@ async function fetchAndUpdateIntervention(iv) {
|
||||
if (tlResp && tlResp.ok) {
|
||||
const actionDetails = parseTimelineJson(tlResp.body, iv.actionId);
|
||||
if (actionDetails && actionDetails.text) {
|
||||
iv.actionText = actionDetails.text;
|
||||
iv.actionDone = actionDetails.doneById;
|
||||
// Le texte de timeline est plus complet que bulleDescription :
|
||||
// on remplace bulleDescription par actionText pour le tooltip.
|
||||
iv.bulleDescription = actionDetails.text;
|
||||
const infob = parseActionText(actionDetails.text);
|
||||
if (infob) {
|
||||
iv.infobulle = infob;
|
||||
if (infob.contact) iv.bulleContact = infob.contact;
|
||||
if (infob.lieu) iv.bulleLieu = infob.lieu;
|
||||
}
|
||||
applyActionTextToIv(iv, actionDetails);
|
||||
} else {
|
||||
// Timeline partielle : marquer pour retry silencieux en arrière-plan
|
||||
iv.actionTextPending = true;
|
||||
}
|
||||
} else {
|
||||
iv.actionTextPending = true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1055,6 +1131,94 @@ async function fetchAndUpdateIntervention(iv) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique les détails d'action (texte timeline) à une intervention :
|
||||
* - met à jour bulleDescription (texte affiché dans la popup)
|
||||
* - reparse contact/lieu pour mettre à jour la carte
|
||||
* - rafraîchit la ligne dans le DOM
|
||||
* Utilisé à la fois par le flow principal et par le retry silencieux.
|
||||
*/
|
||||
function applyActionTextToIv(iv, actionDetails) {
|
||||
iv.actionText = actionDetails.text;
|
||||
iv.actionDone = actionDetails.doneById;
|
||||
iv.bulleDescription = actionDetails.text;
|
||||
iv.actionDescriptionFetched = true;
|
||||
iv.actionTextPending = false;
|
||||
const infob = parseActionText(actionDetails.text);
|
||||
if (infob) {
|
||||
iv.infobulle = infob;
|
||||
if (infob.contact) iv.bulleContact = infob.contact;
|
||||
if (infob.lieu) iv.bulleLieu = infob.lieu;
|
||||
}
|
||||
// Rafraîchir la ligne dans le DOM (lieu/contact mis à jour en live)
|
||||
updateInterventionRow(iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry silencieux en arrière-plan : liste les interventions dont le texte
|
||||
* d'action n'a pas pu être récupéré (timeline partielle au 1er coup), et
|
||||
* refait un fetch timeline pour chacune, avec un petit délai entre les appels
|
||||
* pour ne pas surcharger le serveur.
|
||||
*
|
||||
* Cette fonction est lancée sans await — elle tourne en tâche de fond pendant
|
||||
* que l'utilisateur navigue. Elle respecte le jeton de refresh : si l'user
|
||||
* change de jour, le jeton change et le retry s'arrête silencieusement.
|
||||
*
|
||||
* Aucun spinner ni indication visuelle : l'user ne voit rien, sauf que les
|
||||
* popups se mettent à jour quand le texte arrive.
|
||||
*/
|
||||
async function runBackgroundTimelineRetry(techs, isoDate, myToken) {
|
||||
// Collecter les interventions qui ont besoin d'un retry
|
||||
const pending = [];
|
||||
for (const tech of techs) {
|
||||
for (const iv of tech.interventions) {
|
||||
if (iv.actionTextPending && iv.ficheTarget && iv.ficheChecksum) {
|
||||
pending.push(iv);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pending.length === 0) return;
|
||||
|
||||
// Attendre un peu avant de démarrer (laisser le serveur "respirer")
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
|
||||
// Si l'user a changé de jour entre-temps, abandonner
|
||||
if (currentRefreshToken !== myToken) return;
|
||||
|
||||
for (const iv of pending) {
|
||||
// Si l'user a navigué ailleurs OU cliqué arrêter : on sort sans bruit
|
||||
if (currentRefreshToken !== myToken) return;
|
||||
if (isRefreshAborted()) return;
|
||||
|
||||
try {
|
||||
const tlResp = await sendMessage({
|
||||
type: "fetchTimeline",
|
||||
target: iv.ficheTarget,
|
||||
checksum: iv.ficheChecksum
|
||||
});
|
||||
if (tlResp && tlResp.ok) {
|
||||
const actionDetails = parseTimelineJson(tlResp.body, iv.actionId);
|
||||
if (actionDetails && actionDetails.text) {
|
||||
applyActionTextToIv(iv, actionDetails);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silence : c'est du retry en arrière-plan, on ne dérange pas l'user
|
||||
}
|
||||
|
||||
// Petit délai entre chaque retry pour ménager le serveur
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
}
|
||||
|
||||
// Sauvegarder le cache avec les nouvelles infos (si on est toujours
|
||||
// sur la même date et même token)
|
||||
if (currentRefreshToken === myToken && !isRefreshAborted()) {
|
||||
try {
|
||||
await writeCache(isoDate, { techs });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function isClosedStatus(s) {
|
||||
return !!s && CLOSED_STATUS.some(x => s.includes(x));
|
||||
}
|
||||
@@ -1447,6 +1611,16 @@ function showAbortButton(on) {
|
||||
else btn.classList.add("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast de feedback quand l'user clique Arrêter. Les fetches en cours peuvent
|
||||
* encore prendre 1-2 secondes avant de se terminer (on ne peut pas vraiment
|
||||
* annuler un fetch() en cours), mais du point de vue de l'interface tout
|
||||
* est arrêté : plus de mise à jour, plus de cache, plus rien.
|
||||
*/
|
||||
function showAbortToast() {
|
||||
showToast("Rafraîchissement", "arrêté");
|
||||
}
|
||||
|
||||
function renderFromData(data) {
|
||||
state.currentData = data;
|
||||
document.getElementById("loading").classList.add("hidden");
|
||||
@@ -2008,56 +2182,8 @@ function buildInterventionRow(iv, cardEl) {
|
||||
const contactRaw = i.contact || iv.bulleContact || null;
|
||||
const lieuRaw = i.lieu || iv.bulleLieu || null;
|
||||
|
||||
// Extraire tous les contacts (s'il y en a plusieurs séparés par "ou", etc.)
|
||||
const contacts = extractContacts(contactRaw);
|
||||
|
||||
// Split le lieu : ville / adresse
|
||||
const { ville, adresse } = splitLieu(lieuRaw);
|
||||
|
||||
// ── Lieu : ville (MAJUSCULES GRAS) puis adresse (italique noir) ──────────
|
||||
if (ville || adresse) {
|
||||
const lieuBlock = document.createElement("div");
|
||||
lieuBlock.className = "iv-lieu-block";
|
||||
if (ville) {
|
||||
const villeEl = document.createElement("div");
|
||||
villeEl.className = "iv-lieu-ville";
|
||||
villeEl.textContent = ville.toUpperCase();
|
||||
lieuBlock.appendChild(villeEl);
|
||||
}
|
||||
if (adresse) {
|
||||
const addrEl = document.createElement("div");
|
||||
addrEl.className = "iv-lieu-adresse";
|
||||
addrEl.textContent = adresse;
|
||||
lieuBlock.appendChild(addrEl);
|
||||
}
|
||||
rightCol.appendChild(lieuBlock);
|
||||
}
|
||||
|
||||
// ── Contact(s) + téléphone — un par ligne si plusieurs ──────────────────
|
||||
for (const c of contacts) {
|
||||
if (!c.name && !c.phone) continue;
|
||||
const contactEl = document.createElement("div");
|
||||
contactEl.className = "iv-contact-line";
|
||||
if (c.name) {
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "iv-contact";
|
||||
nameSpan.textContent = c.name;
|
||||
contactEl.appendChild(nameSpan);
|
||||
}
|
||||
if (c.phone) {
|
||||
if (c.name) {
|
||||
const sep = document.createElement("span");
|
||||
sep.className = "iv-sep";
|
||||
sep.textContent = " | ";
|
||||
contactEl.appendChild(sep);
|
||||
}
|
||||
const phoneSpan = document.createElement("span");
|
||||
phoneSpan.className = "iv-phone";
|
||||
phoneSpan.textContent = c.phone;
|
||||
contactEl.appendChild(phoneSpan);
|
||||
}
|
||||
rightCol.appendChild(contactEl);
|
||||
}
|
||||
// Rendu initial de lieu + contacts dans rightCol
|
||||
renderLieuContactBlocks(rightCol, lieuRaw, contactRaw);
|
||||
|
||||
// ── Bas : Catégorie (à gauche) + Signature planificateur (à droite) ──────
|
||||
const bottomEl = document.createElement("div");
|
||||
@@ -2576,6 +2702,73 @@ const ALL_COLOR_CLASSES = [
|
||||
"color-reservation", "color-autre"
|
||||
];
|
||||
|
||||
/**
|
||||
* (Re)génère les blocs Lieu et Contact(s) dans le conteneur .iv-right.
|
||||
* Supprime d'abord les anciens blocs (.iv-lieu-block + .iv-contact-line),
|
||||
* puis insère les nouveaux AVANT le bloc .iv-bottom-line (si présent) pour
|
||||
* conserver l'ordre d'affichage. Utilisé à la création ET lors de la
|
||||
* mise à jour après fetch de la fiche.
|
||||
*/
|
||||
function renderLieuContactBlocks(rightCol, lieuRaw, contactRaw) {
|
||||
// Supprime les anciens blocs lieu/contact
|
||||
rightCol.querySelectorAll(".iv-lieu-block, .iv-contact-line").forEach(el => el.remove());
|
||||
|
||||
const contacts = extractContacts(contactRaw);
|
||||
const { ville, adresse } = splitLieu(lieuRaw);
|
||||
|
||||
// Point d'insertion : avant .iv-bottom-line (catégorie + signature), sinon à la fin
|
||||
const anchor = rightCol.querySelector(".iv-bottom-line");
|
||||
const insert = (el) => {
|
||||
if (anchor) rightCol.insertBefore(el, anchor);
|
||||
else rightCol.appendChild(el);
|
||||
};
|
||||
|
||||
// ── Lieu : ville (MAJUSCULES GRAS) puis adresse (italique noir) ──────────
|
||||
if (ville || adresse) {
|
||||
const lieuBlock = document.createElement("div");
|
||||
lieuBlock.className = "iv-lieu-block";
|
||||
if (ville) {
|
||||
const villeEl = document.createElement("div");
|
||||
villeEl.className = "iv-lieu-ville";
|
||||
villeEl.textContent = ville.toUpperCase();
|
||||
lieuBlock.appendChild(villeEl);
|
||||
}
|
||||
if (adresse) {
|
||||
const addrEl = document.createElement("div");
|
||||
addrEl.className = "iv-lieu-adresse";
|
||||
addrEl.textContent = adresse;
|
||||
lieuBlock.appendChild(addrEl);
|
||||
}
|
||||
insert(lieuBlock);
|
||||
}
|
||||
|
||||
// ── Contact(s) + téléphone — un par ligne si plusieurs ──────────────────
|
||||
for (const c of contacts) {
|
||||
if (!c.name && !c.phone) continue;
|
||||
const contactEl = document.createElement("div");
|
||||
contactEl.className = "iv-contact-line";
|
||||
if (c.name) {
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "iv-contact";
|
||||
nameSpan.textContent = c.name;
|
||||
contactEl.appendChild(nameSpan);
|
||||
}
|
||||
if (c.phone) {
|
||||
if (c.name) {
|
||||
const sep = document.createElement("span");
|
||||
sep.className = "iv-sep";
|
||||
sep.textContent = " | ";
|
||||
contactEl.appendChild(sep);
|
||||
}
|
||||
const phoneSpan = document.createElement("span");
|
||||
phoneSpan.className = "iv-phone";
|
||||
phoneSpan.textContent = c.phone;
|
||||
contactEl.appendChild(phoneSpan);
|
||||
}
|
||||
insert(contactEl);
|
||||
}
|
||||
}
|
||||
|
||||
function updateInterventionRow(iv) {
|
||||
// Réservations : pas concerné (pas de fetch fiche pour elles)
|
||||
if (iv.type === "AL-Reservation") return;
|
||||
@@ -2641,6 +2834,16 @@ function updateInterventionRow(iv) {
|
||||
const catEl = row.querySelector(".iv-category");
|
||||
if (catEl) catEl.textContent = deriveShortTitle(iv);
|
||||
|
||||
// Lieu + Contact(s) : régénérés depuis les valeurs actuelles de iv
|
||||
// (elles peuvent avoir été mises à jour par le fetch de la fiche).
|
||||
const rightCol = row.querySelector(".iv-right");
|
||||
if (rightCol) {
|
||||
const info = iv.infobulle || {};
|
||||
const contactRaw = info.contact || iv.bulleContact || null;
|
||||
const lieuRaw = info.lieu || iv.bulleLieu || null;
|
||||
renderLieuContactBlocks(rightCol, lieuRaw, contactRaw);
|
||||
}
|
||||
|
||||
// Segment timeline correspondant : même couleur + même classe statut
|
||||
const card = row.closest(".card");
|
||||
if (card && row.dataset.ivIdx !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user