v2026.5.19 — Drag-and-drop des popups épinglés

This commit is contained in:
Quentin Rouiller
2026-04-23 14:34:08 +02:00
parent 1a7393c297
commit ad952ebc55
3 changed files with 196 additions and 50 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Planification",
"version": "2026.5.18",
"version": "2026.5.19",
"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.",
"permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"],
"host_permissions": [
+72 -28
View File
@@ -2590,46 +2590,39 @@ header.topbar::before {
/* ==========================================================================
v2026.5.17 : mode Minimisé (popup flottant compact, juste la ref)
v2026.5.18 : fix affichage — on masque TOUT sauf la ref et la topbar
v2026.5.19 : refonte — élément .pinned-popup-minref créé à la volée
========================================================================== */
.pinned-popup.pinned-popup-minimized {
min-width: 180px !important;
max-width: 260px !important;
width: auto !important;
height: auto !important;
padding-top: 28px !important;
padding-bottom: 8px !important;
min-height: 60px !important;
padding: 28px 10px 10px 10px !important;
overflow: hidden;
background: var(--bg-elevated) !important;
border: 1px solid var(--border) !important;
}
/* Masquer tous les descendants sauf ceux qu'on veut voir */
.pinned-popup.pinned-popup-minimized * {
/* Masquer tous les enfants directs du popup minimisé */
.pinned-popup.pinned-popup-minimized > *:not(.pinned-popup-topbar):not(.pinned-popup-dragbar):not(.pinned-popup-minref) {
display: none !important;
}
/* Réafficher la topbar et ses boutons */
.pinned-popup.pinned-popup-minimized .pinned-popup-topbar,
.pinned-popup.pinned-popup-minimized .pinned-popup-topbar * {
display: flex !important;
}
/* Réafficher la dragbar */
.pinned-popup.pinned-popup-minimized .pinned-popup-dragbar {
display: block !important;
}
/* Réafficher la ref (peut être dans une row imbriquée) */
.pinned-popup.pinned-popup-minimized .iv-ref-header {
display: block !important;
text-align: center !important;
padding: 6px 12px !important;
grid-column: unset !important;
font-size: 14px !important;
font-weight: 700 !important;
color: var(--text) !important;
/* L'élément ref dédié, centré et gros */
.pinned-popup-minref {
display: block;
text-align: center;
padding: 6px 8px;
font-family: var(--mono, monospace);
font-size: 14px;
font-weight: 700;
color: var(--text);
cursor: pointer;
user-select: none;
border-radius: 4px;
transition: background 0.12s;
}
/* Et si la ref est dans une row grid, reshowrow pour qu'elle s'affiche */
.pinned-popup.pinned-popup-minimized .intervention-v2,
.pinned-popup.pinned-popup-minimized .tt-ref-cell {
display: block !important;
grid-template: none !important;
.pinned-popup-minref:hover {
background: var(--bg-muted);
}
/* ==========================================================================
@@ -2790,3 +2783,54 @@ header.topbar::before {
background: var(--bg-muted);
color: var(--text);
}
/* ==========================================================================
v2026.5.19 : nouveaux éléments
========================================================================== */
/* Bouton Actualiser (↻) dans la topbar du popup épinglé — animation spin */
.pinned-popup-refresh {
font-size: 14px;
line-height: 1;
}
.pinned-popup-refresh.spinning {
animation: pinned-popup-refresh-spin 0.6s linear infinite;
pointer-events: none;
}
@keyframes pinned-popup-refresh-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Pendant le drag d'un popup, ignorer les hover sur les cartes pour ne pas
ouvrir des tooltips parasites */
body.popup-dragging .intervention-v2,
body.popup-dragging .card {
pointer-events: none;
}
/* Mais garder les popups épinglés cliquables */
body.popup-dragging .pinned-popup {
pointer-events: auto;
}
/* Pastille dock à 2 lignes : ref (gras) + date (petit) */
.pinned-popup-dock-pill {
flex-direction: column !important;
align-items: center !important;
padding: 4px 14px !important;
line-height: 1.1;
gap: 1px !important;
}
.pinned-popup-dock-pill-ref {
display: block;
font-size: 13px;
font-weight: 700;
font-family: var(--mono, monospace);
}
.pinned-popup-dock-pill-date {
display: block;
font-size: 10px;
font-weight: 500;
opacity: 0.85;
font-family: var(--mono, monospace);
}
+123 -21
View File
@@ -6094,6 +6094,11 @@ let bulleState = {
};
function showTooltip(e, iv, rowEl) {
// v2026.5.19 : pendant qu'un popup épinglé est en cours de drag, on ignore
// les mouseenter sur les cartes — sinon en survolant une carte on déclenche
// l'ouverture d'un nouveau tooltip par-dessus ce qu'on est en train de bouger.
if (state._popupDragging) return;
// v4.1.15 : si la bulle est épinglée sur une autre iv, on NE REMPLACE PAS
// son contenu (l'user veut garder la fiche épinglée même en survolant
// d'autres cartes).
@@ -6485,6 +6490,9 @@ function pinTooltip() {
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"]');
@@ -6522,6 +6530,26 @@ function pinTooltip() {
});
topbar.appendChild(minBtn);
// v2026.5.19 : Bouton Actualiser (icône ↻)
// 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";
@@ -6614,6 +6642,44 @@ function _closePinnedPopup(el) {
el.remove();
}
/**
* v2026.5.19 : re-fetch les infos d'une intervention et met à jour le contenu
* du popup épinglé correspondant. Utilise fetchAndUpdateIntervention qui fait
* xhr2 + fiche, puis régénère le HTML du tooltip avec buildTooltipHTML.
*/
async function _refreshPinnedPopupIv(popup, iv) {
if (!popup || !iv) return;
try {
// Forcer le refetch : on invalide les flags qui disent "déjà fetché"
iv.xhr2Fetched = false;
iv.xhr2Fetching = false;
iv.ficheFetched = false;
iv.ficheFetching = false;
// Token de refresh actuel (pour que fetchAndUpdateIntervention ne soit
// pas abortée par les checks isRefreshAborted)
const token = (typeof currentRefreshToken !== "undefined") ? currentRefreshToken : 0;
await fetchAndUpdateIntervention(iv, token);
// Régénérer le HTML du tooltip avec les nouvelles infos.
// On doit réinjecter juste le contenu, en gardant la topbar et la dragbar
// (qui ne sont PAS dans le tooltip source, elles sont propres au popup).
const topbar = popup.querySelector(".pinned-popup-topbar");
const dragbar = popup.querySelector(".pinned-popup-dragbar");
const newHtml = buildTooltipHTML(iv);
popup.innerHTML = newHtml;
// Virer aussi la vieille icône 📌 si elle revient dans le rebuild
const oldPin = popup.querySelector('.tooltip-pinbtn[data-action="pin"]');
if (oldPin) oldPin.remove();
// Remettre topbar et dragbar
if (topbar) popup.appendChild(topbar);
if (dragbar) popup.appendChild(dragbar);
} catch (err) {
console.warn("[refresh-popup]", err);
}
}
/**
* Désépinglage "mou" : la popup n'est plus considérée épinglée (elle n'est
* plus dans pinnedPopups, donc le comptage pour Ctrl×2 etc. ignore) mais on
@@ -6686,12 +6752,16 @@ function _softUnpinPopup(el) {
/**
* Passe un popup épinglé en mode Minimisé : on ne montre plus que la ref,
* dans un petit cadre flottant toujours drag-able.
*
* v2026.5.19 : au lieu de masquer tout le contenu via CSS et tenter de
* réafficher la ref (fragile), on crée un élément dédié `.pinned-popup-minref`
* qui contient juste la ref + la date. Cet élément est ajouté/retiré au besoin.
*/
function _minimizePinnedPopup(popup) {
if (!popup) return;
popup.classList.add("pinned-popup-minimized");
// Adapter les boutons topbar : [_] devient [⬆] (agrandir)
// Adapter les boutons topbar : [] devient [⬆] (agrandir)
const minBtn = popup.querySelector(".pinned-popup-minimize");
if (minBtn) {
minBtn.innerHTML = "⬆";
@@ -6705,20 +6775,22 @@ function _minimizePinnedPopup(popup) {
});
}
// Clic sur la ref (dans iv-ref-header) = agrandir aussi
const refEl = popup.querySelector(".iv-ref-header");
if (refEl) {
refEl.style.cursor = "pointer";
refEl.title = "Cliquer pour agrandir";
refEl.addEventListener("click", _onMinimizedRefClick);
// Créer un élément dédié pour afficher la ref en mode minimisé
let minRef = popup.querySelector(".pinned-popup-minref");
if (!minRef) {
minRef = document.createElement("div");
minRef.className = "pinned-popup-minref";
const refText = popup.dataset.ref || "(sans ref)";
minRef.textContent = refText;
minRef.title = "Cliquer pour agrandir";
minRef.addEventListener("click", (e) => {
e.stopPropagation();
_expandPinnedPopup(popup);
});
popup.appendChild(minRef);
}
}
function _onMinimizedRefClick(e) {
const popup = e.currentTarget.closest(".pinned-popup");
if (popup) _expandPinnedPopup(popup);
}
/**
* Repasse un popup minimisé en mode Normal (complet).
*/
@@ -6739,13 +6811,9 @@ function _expandPinnedPopup(popup) {
});
}
// Retirer listener du clic-agrandir sur la ref
const refEl = popup.querySelector(".iv-ref-header");
if (refEl) {
refEl.style.cursor = "";
refEl.title = "";
refEl.removeEventListener("click", _onMinimizedRefClick);
}
// Retirer l'élément ref dédié (s'il existe)
const minRef = popup.querySelector(".pinned-popup-minref");
if (minRef) minRef.remove();
}
/**
@@ -6778,12 +6846,26 @@ function _reducePinnedPopup(popup) {
// Créer la pastille dock
// v2026.5.18 : le fond de la pastille prend la couleur de catégorie
// (via la classe color-XXX déjà utilisée ailleurs dans le CSS)
// v2026.5.19 : pastille à 2 lignes — ref (gras) + date origine (petit)
const pill = document.createElement("button");
pill.type = "button";
pill.className = "pinned-popup-dock-pill color-" + colorKey;
pill.textContent = label;
pill.title = "Cliquer pour agrandir";
const pillRef = document.createElement("span");
pillRef.className = "pinned-popup-dock-pill-ref";
pillRef.textContent = label;
pill.appendChild(pillRef);
// Date d'origine (ex: "21.04")
const originDate = popup.dataset.originDate || "";
if (originDate) {
const pillDate = document.createElement("span");
pillDate.className = "pinned-popup-dock-pill-date";
pillDate.textContent = _formatDateShort(originDate);
pill.appendChild(pillDate);
}
// Mémoriser la position/taille du popup avant de le masquer
const rect = popup.getBoundingClientRect();
popup.dataset.prevLeft = popup.style.left || (rect.left + "px");
@@ -6811,7 +6893,7 @@ function _reducePinnedPopup(popup) {
}
/**
* v2026.5.18 : réduit TOUS les popups épinglés actuellement ouverts (en mode
* v2026.5.19 : réduit TOUS les popups épinglés actuellement ouverts (en mode
* normal ou minimisé) dans la taskbar du bas. Appelé au changement de date.
*/
function _reduceAllPinnedPopups() {
@@ -6821,6 +6903,16 @@ function _reduceAllPinnedPopups() {
});
}
/**
* v2026.5.19 : ISO date (YYYY-MM-DD) format court "DD.MM" pour le dock.
*/
function _formatDateShort(iso) {
if (!iso) return "";
const m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!m) return iso;
return `${m[3]}.${m[2]}`;
}
/**
* v2026.5.18 : ajoute (ou met à jour) le bouton "Fermer tous" dans le dock
* quand au moins 2 popups épinglés existent (réduits OU affichés).
@@ -6997,6 +7089,11 @@ function _attachPopupDragHandler(popup, dragbar) {
popup.classList.remove("dragging");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
// v2026.5.19 : retirer la classe body et le flag global après un petit
// délai pour laisser le temps au mouseleave de la carte de se propager
// sans déclencher de tooltip parasite.
document.body.classList.remove("popup-dragging");
setTimeout(() => { state._popupDragging = false; }, 50);
// Mettre à jour le rect mémorisé pour la détection de chevauchement
const entry = pinnedPopups.find(p => p.el === popup);
@@ -7019,6 +7116,11 @@ function _attachPopupDragHandler(popup, dragbar) {
startLeft = parseFloat(popup.style.left) || 0;
startTop = parseFloat(popup.style.top) || 0;
popup.classList.add("dragging");
// v2026.5.19 : flag global pour que showTooltip ignore les mouseenter
// pendant le drag. Ajout d'une classe sur <body> qui désactive les
// pointer-events sur les cartes.
state._popupDragging = true;
document.body.classList.add("popup-dragging");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});