diff --git a/background.js b/background.js index af10a54..3798c99 100644 --- a/background.js +++ b/background.js @@ -774,21 +774,15 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) { throw new Error("session_expired"); } - // v5.0.1 : heuristique pour détecter si la suppression a marché. - // EasyVista renvoie typiquement : - // - une chaine vide ou "ok" ou "1" si succès - // - un message d'erreur / html d'erreur si function_name inconnu - // On considère que tout ce qui n'est pas un message d'erreur évident - // est un succès. Si plusieurs fn renvoient 200, on prend le premier. - const trimmed = (body || "").trim().toLowerCase(); - const looksLikeError = trimmed.includes("error") - || trimmed.includes("erreur") - || trimmed.includes("unknown function") - || trimmed.includes("fonction inconnue") - || trimmed.includes("true + const trimmed = (body || "").trim(); + const lower = trimmed.toLowerCase(); + + // Succès explicite : réponse XML du type true + if (/^<\w+>true<\/\w+>\s*$/i.test(trimmed)) { + console.log(`[bg] → SUCCÈS confirmé par XML <...>true avec function_name=${fn}`); + return { status: r.status, functionName: fn, body: trimmed }; } console.log(`[bg] → réponse ressemble à une erreur, on tente le prochain nom`); lastBody = body; diff --git a/manifest.json b/manifest.json index 7c63cc8..2ec05cb 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.19", + "version": "2026.5.20", "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 864c479..a0b5764 100644 --- a/viewer.css +++ b/viewer.css @@ -2000,6 +2000,24 @@ body.modal-open { left: 50%; top: 50%; transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + line-height: 1.1; + color: var(--text); + pointer-events: none; + user-select: none; + white-space: nowrap; +} +.app-clock-date { + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + letter-spacing: 0.3px; + text-transform: capitalize; +} +.app-clock-time { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; diff --git a/viewer.js b/viewer.js index 4d8abb4..172a3d8 100644 --- a/viewer.js +++ b/viewer.js @@ -6457,16 +6457,30 @@ function _findFreePopupPosition(rowEl, w, h) { { x: rowRect.left, y: rowRect.top - h - pad, name: "dessus" } ]; - // Pour chaque candidat, clamper dans le viewport (marge 8px) et convertir - // en coord document, puis tester le chevauchement + // v2026.5.20 : ajouter une grille de positions de fallback couvrant toute + // la safe area (pas de 60px × 60px) — garantit qu'on trouve ~toujours une + // place, sauf si vraiment trop de popups actifs. + const availW = safe.right - safe.left; + const availH = safe.bottom - safe.top; + if (availW > w + 20 && availH > h + 20) { + for (let y = safe.top; y + h <= safe.bottom; y += 60) { + for (let x = safe.left; x + w <= safe.right; x += 60) { + candidates.push({ x, y, name: "grid" }); + } + } + } + + // Tester chaque candidat dans l'ordre for (const c of candidates) { let x = c.x, y = c.y; - // Clamp horizontal dans le viewport - if (x < 4) x = 4; - if (x + w > viewportW - 8) x = viewportW - 8 - w; - // Clamp vertical dans le viewport - if (y < 4) y = 4; - if (y + h > viewportH - 8) y = viewportH - 8 - h; + // Clamp dans la safe area + if (x < safe.left) x = safe.left; + if (x + w > safe.right) x = safe.right - w; + if (x < safe.left) continue; // popup plus large que safe area + if (y < safe.top) y = safe.top; + if (y + h > safe.bottom) y = safe.bottom - h; + if (y < safe.top) continue; + // Si, après clamp, la popup chevaucherait la ligne source elle-même, // on ignore ce candidat (on préfère une direction qui la laisse visible). const rowRectClamped = { @@ -6474,9 +6488,12 @@ function _findFreePopupPosition(rowEl, w, h) { right: rowRect.right, bottom: rowRect.bottom }; const candRect = { left: x, top: y, right: x + w, bottom: y + h }; - if (_rectsOverlap(candRect, rowRectClamped)) continue; + // v2026.5.20 : pour les 4 candidats principaux, on refuse de chevaucher + // la row ; pour les candidats "grid" de fallback, on l'accepte + // (on veut une place à tout prix). + if (c.name !== "grid" && _rectsOverlap(candRect, rowRectClamped)) continue; - // Test chevauchement avec les popups déjà épinglés + // Test chevauchement avec les popups déjà épinglés (coords document) const docRect = { left: _viewportToDocumentX(x), top: _viewportToDocumentY(y), @@ -6485,13 +6502,14 @@ function _findFreePopupPosition(rowEl, w, h) { }; let overlapsOther = false; for (const p of pinnedPopups) { + // Ne pas comparer avec un popup qui est dans le dock (réduit) + if (p.el && p.el.classList && p.el.classList.contains("pinned-popup-reduced")) continue; if (_rectsOverlap(docRect, p.rect)) { overlapsOther = true; break; } } if (!overlapsOther) { - // Position libre trouvée return { viewportX: x, viewportY: y, docX: docRect.left, docY: docRect.top, @@ -6499,6 +6517,30 @@ function _findFreePopupPosition(rowEl, w, h) { }; } } + + // v2026.5.20 : ultime fallback — accepter de chevaucher mais décaler + // un peu par rapport au 1er popup épinglé existant. Évite complètement + // le "Pas de place" injuste. + if (pinnedPopups.length > 0) { + const last = pinnedPopups[pinnedPopups.length - 1]; + let x = (last.rect.left - (window.scrollX || 0)) + 30; + let y = (last.rect.top - (window.scrollY || 0)) + 30; + if (x + w > safe.right) x = safe.right - w; + if (y + h > safe.bottom) y = safe.bottom - h; + if (x < safe.left) x = safe.left; + if (y < safe.top) y = safe.top; + const docRect = { + left: _viewportToDocumentX(x), + top: _viewportToDocumentY(y), + right: _viewportToDocumentX(x + w), + bottom: _viewportToDocumentY(y + h) + }; + return { + viewportX: x, viewportY: y, + docX: docRect.left, docY: docRect.top, + rect: docRect + }; + } return null; } @@ -6512,6 +6554,34 @@ function pinTooltip() { if (!srcEl) return; const iv = state.currentTooltipIv; + // v2026.5.21 : unicité actionId + date. Si un popup pour la même ref + // ET la même date est déjà épinglé, on le supprime et on re-crée un nouveau + // (user a choisi ce comportement : "tu supprime le popup actuellement + // épinglé et tu répingle la nouvelle fenêtre"). + const currentDate = state.currentDate || ""; + const existingKey = (iv.actionId || "") + "|" + currentDate; + for (let i = pinnedPopups.length - 1; i >= 0; i--) { + const p = pinnedPopups[i]; + if (!p || !p.el) continue; + const aid = p.el.dataset.actionId || ""; + const d = p.el.dataset.originDate || ""; + if (aid + "|" + d === existingKey) { + // Retirer l'ancien (popup + pastille dock éventuelle) + if (p.el._linkedPill) { + try { p.el._linkedPill.remove(); } catch (e) {} + } + try { p.el.remove(); } catch (e) {} + pinnedPopups.splice(i, 1); + } + } + // Nettoyer un éventuel dock devenu vide + const dockEl = document.getElementById("pinned-popups-dock"); + if (dockEl && dockEl.querySelectorAll(".pinned-popup-dock-pill").length === 0) { + dockEl.classList.remove("visible"); + const closeAllBtn = document.getElementById("pinned-popups-close-all"); + if (closeAllBtn) closeAllBtn.remove(); + } + // Chercher la ligne source (row iv-v2) let rowEl = null; if (iv.actionId) { @@ -6528,17 +6598,79 @@ function pinTooltip() { popup.dataset.actionId = iv.actionId || ""; popup.innerHTML = srcEl.innerHTML; - // Ajouter un bouton × de fermeture (en plus du 📌) - const closeBtn = document.createElement("button"); - closeBtn.type = "button"; - closeBtn.className = "pinned-popup-close"; - closeBtn.innerHTML = "×"; - closeBtn.title = "Désépingler (reste visible tant que la souris est dessus)"; - closeBtn.addEventListener("click", (e) => { + // v2026.5.18 : mémoriser la ref et la couleur pour le dock (pastille avec + // couleur de catégorie + texte ref) + popup.dataset.ref = iv.ref || ""; + popup.dataset.colorKey = (typeof deriveColorKey === "function" ? deriveColorKey(iv) : "autre") || "autre"; + + // v2026.5.19 : mémoriser aussi la date pour l'afficher sur la pastille dock + popup.dataset.originDate = state.currentDate || ""; + + // v2026.5.17 : masquer l'icône 📌 du contenu cloné (redondante car le + // popup a sa propre topbar avec le bouton "désépingler" 📍 explicite) + const oldPin = popup.querySelector('.tooltip-pinbtn[data-action="pin"]'); + if (oldPin) oldPin.remove(); + + // v2026.5.17 : topbar avec 3 boutons pour un popup épinglé : + // v2026.5.18 : swap des actions — _ réduit dans le dock, ▭ minimise flottant + // _ = Réduire (docké dans la taskbar du bas) + // ▭ = Minimiser (popup reste flottant mais compact, juste la ref) + // 📍 = Désépingler (l'icône d'épingle "plantée" ; clic = retire l'épingle) + const topbar = document.createElement("div"); + topbar.className = "pinned-popup-topbar"; + + // Bouton Réduire (icône _ ) + const reduceBtn = document.createElement("button"); + reduceBtn.type = "button"; + reduceBtn.className = "pinned-popup-btn pinned-popup-reduce"; + reduceBtn.innerHTML = "_"; + reduceBtn.title = "Réduire (docké en bas de l'écran)"; + reduceBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _reducePinnedPopup(popup); + }); + topbar.appendChild(reduceBtn); + + // Bouton Minimiser (icône ▭ ) + const minBtn = document.createElement("button"); + minBtn.type = "button"; + minBtn.className = "pinned-popup-btn pinned-popup-minimize"; + minBtn.innerHTML = "▭"; + minBtn.title = "Minimiser (reste flottant mais compact)"; + minBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _minimizePinnedPopup(popup); + }); + topbar.appendChild(minBtn); + + // v2026.5.19 : Bouton Actualiser (même icône SVG que le tooltip standard) + // Re-fetch la fiche de l'intervention pour mettre à jour les infos (statut, + // commentaires, action text) sans recharger le planning entier. + const refreshBtn = document.createElement("button"); + refreshBtn.type = "button"; + refreshBtn.className = "pinned-popup-btn pinned-popup-refresh"; + refreshBtn.innerHTML = ''; + refreshBtn.title = "Actualiser les informations de cette intervention"; + refreshBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + if (refreshBtn.classList.contains("spinning")) return; + refreshBtn.classList.add("spinning"); + try { + await _refreshPinnedPopupIv(popup, iv); + } finally { + setTimeout(() => refreshBtn.classList.remove("spinning"), 300); + } + }); + topbar.appendChild(refreshBtn); + + // Bouton Désépingler (icône épingle plantée) + const unpinBtn = document.createElement("button"); + unpinBtn.type = "button"; + unpinBtn.className = "pinned-popup-btn pinned-popup-unpin"; + unpinBtn.innerHTML = "📍"; + unpinBtn.title = "Désépingler (se ferme quand la souris sort)"; + unpinBtn.addEventListener("click", (e) => { e.stopPropagation(); - // Désépinglage "mou" : on marque la popup comme non épinglée mais on la - // laisse visible tant que la souris est dessus. Elle disparaît quand la - // souris sort. _softUnpinPopup(popup); }); popup.appendChild(closeBtn);