diff --git a/background.js b/background.js index e5776f6..9af0422 100644 --- a/background.js +++ b/background.js @@ -400,6 +400,67 @@ function originForContext(context) { : "https://itsma.vd.ch"; } +/** + * v2026.5.16 : surveille un onglet ouvert pour détecter si le Windows SSO + * a échoué et rediriger vers la bonne page. + * + * Quand la session portail Canton est expirée, EasyVista redirige vers + * https://portail.etat-de-vaud.ch/iamlogin/?spEntityID=... + * (page de login manuel moche). On préfère rediriger vers + * https://portail.etat-de-vaud.ch/iam/accueil/ + * qui déclenche le Windows Kerberos SSO automatique. + * + * @param {number} tabId - ID de l'onglet à surveiller + */ +function watchReconnectTabForIamLogin(tabId) { + let redirected = false; + const timeoutMs = 60000; // surveille max 60s + + const listener = (updatedTabId, changeInfo, tab) => { + if (updatedTabId !== tabId) return; + if (redirected) return; + const url = changeInfo.url || (tab && tab.url) || ""; + if (!url) return; + + // Détecter la page de login manuel + // Patterns : portail.etat-de-vaud.ch/iamlogin/ ou www.portail.vd.ch/iamlogin/ + if (/\/iamlogin\//i.test(url) && /portail\./i.test(url)) { + redirected = true; + // Choisir le domaine de redirection : + // - si on voit portail.etat-de-vaud.ch → rester sur interne + // - si on voit www.portail.vd.ch → rester sur externe + let targetUrl; + if (/portail\.etat-de-vaud\.ch/i.test(url)) { + targetUrl = "https://portail.etat-de-vaud.ch/iam/accueil/"; + } else { + targetUrl = "https://www.portail.vd.ch/iam/accueil/"; + } + console.log(`[bg] watchReconnectTab : iamlogin détecté, redirection vers ${targetUrl}`); + chrome.tabs.update(tabId, { url: targetUrl }).catch(e => { + console.warn("[bg] watchReconnectTab : update failed", e); + }); + } + }; + + chrome.tabs.onUpdated.addListener(listener); + + // Stop la surveillance après 60s pour ne pas accumuler des listeners morts + setTimeout(() => { + try { + chrome.tabs.onUpdated.removeListener(listener); + } catch (e) {} + }, timeoutMs); + + // Si l'onglet est fermé, stop aussi + const closeListener = (closedTabId) => { + if (closedTabId === tabId) { + try { chrome.tabs.onUpdated.removeListener(listener); } catch (e) {} + try { chrome.tabs.onRemoved.removeListener(closeListener); } catch (e) {} + } + }; + chrome.tabs.onRemoved.addListener(closeListener); +} + // ============================================================================ // v4.2 : récupération de l'utilisateur connecté // ============================================================================ diff --git a/manifest.json b/manifest.json index aa549ff..8caa52f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.17", + "version": "2026.5.18", "description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.", "browser_specific_settings": { "gecko": { diff --git a/viewer.css b/viewer.css index 38e9c89..9c611bc 100644 --- a/viewer.css +++ b/viewer.css @@ -1015,6 +1015,12 @@ html, body { opacity: 0; transition: opacity 0.1s, background 0.1s, color 0.1s; font-family: inherit; + /* v2026.5.17 : figer largeur/hauteur pour que le changement 📋 → ✓ pendant + la copie ne fasse pas bouger le titre centré dans la grid */ + min-width: 28px; + min-height: 22px; + text-align: center; + box-sizing: border-box; } .intervention-v2:hover .intervention-copy { opacity: 1; } .intervention-copy:hover { diff --git a/viewer.js b/viewer.js index f490274..c2f67aa 100644 --- a/viewer.js +++ b/viewer.js @@ -5062,7 +5062,7 @@ function buildInterventionRow(iv, cardEl) { cardEl._rowIdxCounter = ivIdx + 1; row.dataset.ivIdx = ivIdx; - if (iv.formLink && !iv.ghost) { + if (iv.formLink && !iv.ghost && iv.type !== "AL-Absence") { row.classList.add("clickable"); // v4.1.8 : plus de title au survol (info déjà dans le tooltip en bas) @@ -5100,6 +5100,10 @@ function buildInterventionRow(iv, cardEl) { if (iv.type === "AL-Reservation") { refHeader.textContent = "Réservation"; refHeader.classList.add("is-reservation-title"); + } else if (iv.type === "AL-Absence") { + // v5.0.15 : absence partielle (demi-journée) affichée comme une row + refHeader.textContent = "Absence"; + refHeader.classList.add("is-absence-title"); } else if (iv.ref) { refHeader.textContent = iv.ref; } else { @@ -5108,8 +5112,8 @@ function buildInterventionRow(iv, cardEl) { } row.appendChild(refHeader); - // Check ✓ + bouton copier à droite de la ref (pas pour réservation) - if (statusClass && iv.type !== "AL-Reservation") { + // Check ✓ + bouton copier à droite de la ref (pas pour réservation / absence) + if (statusClass && iv.type !== "AL-Reservation" && iv.type !== "AL-Absence") { const statusEl = document.createElement("div"); statusEl.className = "iv-status-check"; // v4.2.5 : ✓✓ double pour clôturé/résolu (statut officiel EasyVista) @@ -5122,7 +5126,7 @@ function buildInterventionRow(iv, cardEl) { } row.appendChild(statusEl); } - if (iv.ref && iv.type !== "AL-Reservation") { + if (iv.ref && iv.type !== "AL-Reservation" && iv.type !== "AL-Absence") { const copyBtn = document.createElement("button"); copyBtn.className = "intervention-copy"; copyBtn.type = "button"; @@ -5202,6 +5206,40 @@ function buildInterventionRow(iv, cardEl) { return row; } + // v5.0.15 : absence partielle (demi-journée) affichée comme une row au + // même style que les réservations mais en gris foncé, avec le type d'absence + // (Congés, Maladie, Pompier) comme sujet. + if (iv.type === "AL-Absence") { + // Bloc "Par Nom, Prénom" si on a un créateur + if (iv.reservationCreator) { + const parEl = document.createElement("div"); + parEl.className = "iv-reservation-par"; + parEl.textContent = "Par " + iv.reservationCreator; + rightCol.appendChild(parEl); + } + // Type d'absence (Congés, Maladie, Pompier) si dispo dans label + const absenceTypeMatch = (iv.label || "").match(/^([^/]+?)\s*(?:\/|$)/); + const absenceType = absenceTypeMatch ? absenceTypeMatch[1].trim() : null; + if (absenceType) { + const sujetEl = document.createElement("div"); + sujetEl.className = "iv-reservation-sujet"; + sujetEl.textContent = "Type : " + absenceType; + rightCol.appendChild(sujetEl); + } + row.appendChild(rightCol); + + // Tooltip au hover (avec bouton supprimer) + row.addEventListener("mouseenter", (e) => { + showTooltip(e, iv, row); + highlightIntervention(cardEl, ivIdx, true); + }); + row.addEventListener("mouseleave", () => { + hideTooltip(); + highlightIntervention(cardEl, ivIdx, false); + }); + return row; + } + // v4.1.2 : priorité à iv.infobulle (venant du xhr2 = données réelles vérifiées // par le tech sur place) puis fallback sur iv.bulleContact/iv.bulleLieu // (venant de attr1/attr2 = planification initiale, parfois incorrecte). @@ -5597,7 +5635,10 @@ function splitOneContact(raw) { // Prénom 41XXXXXXXXX → extraire 41XXXXXXXXX puis reformater en // +41 XX XXX XX XX). On exige exactement 11 chiffres collés pour // éviter de matcher des codes postaux ou autres nombres. - const rxLong = /(\+41\s?\d(?:[\d\s.\-]*\d)?|\+33\s?\d(?:[\d\s.\-]*\d)?|0\d(?:[\d\s.\-]*\d)?|(? p.trim()).filter(Boolean); + let ville, adresse; - if (idx < 0) { + if (parts.length === 0) { + return { ville: null, adresse: null }; + } else if (parts.length === 1) { + // Pas de slash : tout est l'adresse ville = null; - adresse = s; + adresse = parts[0]; } else { ville = s.substring(0, idx).trim(); adresse = s.substring(idx + 1).trim();