diff --git a/manifest.json b/manifest.json index 21663d0..eceaa58 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.16", + "version": "2026.5.17", "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": [ diff --git a/viewer.css b/viewer.css index 0f2adb2..c16db86 100644 --- a/viewer.css +++ b/viewer.css @@ -323,17 +323,53 @@ html, body { flex-wrap: nowrap; } -/* v2026.5.16 : nom court du jour (Mardi, Lundi, ...) à gauche du date-picker */ -.date-picker-day { +/* v2026.5.17 : faux input date custom avec nom du jour */ +.date-custom-wrapper { + position: relative; + display: inline-flex; + align-items: center; +} +.date-custom { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 5px 10px 5px 12px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-muted); + color: var(--text); + font-family: inherit; font-size: 13px; font-weight: 500; - color: var(--text-muted); - padding: 0 6px 0 2px; - min-width: 58px; - text-align: right; + cursor: pointer; white-space: nowrap; user-select: none; + transition: border-color 0.15s, background 0.15s; } +.date-custom:hover { + border-color: var(--border-strong); + background: var(--bg-hover); +} +.date-custom:focus { + outline: 2px solid var(--accent); + outline-offset: -1px; +} +.date-custom-icon { + font-size: 13px; + opacity: 0.7; +} +.date-input-hidden { + position: absolute; + top: 100%; + left: 0; + width: 1px; + height: 1px; + opacity: 0; + pointer-events: none; +} + +/* v2026.5.17 : masquer l'ancien date-picker-day s'il traîne (compat) */ +.date-picker-day { display: none; } .btn-nav { padding: 6px 10px; @@ -979,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 { @@ -2498,3 +2540,194 @@ header.topbar::before { .btn-today { padding: 4px 6px; font-size: 11px; } .btn-nav { min-width: 26px; padding: 4px 6px; } } + +/* ========================================================================== + v2026.5.17 : topbar des popups épinglés (3 boutons : _ ▭ 📍) + ========================================================================== */ +.pinned-popup { + /* Laisser un peu de place en haut pour la topbar */ + padding-top: 30px !important; +} +.pinned-popup-topbar { + position: absolute; + top: 4px; + right: 4px; + display: flex; + gap: 2px; + align-items: center; + z-index: 2; +} +.pinned-popup-btn { + width: 26px; + height: 22px; + padding: 0; + font-size: 13px; + line-height: 1; + background: transparent; + color: var(--text-muted); + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + transition: background 0.1s, color 0.1s, border-color 0.1s; + font-family: inherit; + display: inline-flex; + align-items: center; + justify-content: center; +} +.pinned-popup-btn:hover { + background: var(--bg-muted); + color: var(--text); + border-color: var(--border); +} +.pinned-popup-unpin { + font-size: 14px; +} + +/* ========================================================================== + v2026.5.17 : mode Minimisé (popup flottant compact, juste la ref) + ========================================================================== */ +.pinned-popup.pinned-popup-minimized { + min-width: 160px !important; + max-width: 220px !important; + width: auto !important; + height: auto !important; + padding-top: 28px !important; + padding-bottom: 6px !important; + overflow: hidden; +} +.pinned-popup.pinned-popup-minimized > :not(.pinned-popup-topbar):not(.iv-ref-header):not(.pinned-popup-dragbar) { + display: none !important; +} +.pinned-popup.pinned-popup-minimized .iv-ref-header { + text-align: center; + padding: 4px 8px !important; + grid-column: unset !important; + font-size: 14px; +} + +/* ========================================================================== + v2026.5.17 : mode Réduit (docké en bas de l'écran) + taskbar + ========================================================================== */ +.pinned-popup.pinned-popup-reduced { + display: none !important; +} +.pinned-popups-dock { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 50; + display: none; + flex-wrap: wrap; + gap: 6px; + padding: 6px 10px; + background: var(--bg-elevated); + border-top: 1px solid var(--border); + box-shadow: 0 -2px 10px rgba(0,0,0,0.2); +} +.pinned-popups-dock.visible { + display: flex; +} +.pinned-popup-dock-pill { + display: inline-flex; + align-items: center; + padding: 6px 14px; + background: var(--accent, #3b82f6); + color: white; + border: none; + border-radius: 16px; + font-family: var(--mono, monospace); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, transform 0.15s; +} +.pinned-popup-dock-pill:hover { + background: var(--accent-hover, #2563eb); + transform: translateY(-1px); +} + +/* ========================================================================== + v2026.5.17 : popup user-badge avec ligne session + ========================================================================== */ +.user-name-popup-name { + font-weight: 600; + margin-bottom: 4px; +} +.user-name-popup-session { + font-size: 12px; + font-variant-numeric: tabular-nums; + padding-top: 4px; + border-top: 1px solid var(--border); +} +.user-name-popup-session.session-ok { color: var(--text-muted); } +.user-name-popup-session.session-warn { color: #f59e0b; font-weight: 600; } +.user-name-popup-session.session-critical { color: #ef4444; font-weight: 700; } + +/* ========================================================================== + v2026.5.17 : popup alerte session qui glisse depuis haut-gauche + ========================================================================== */ +.session-slide-alert { + position: fixed; + top: 60px; + left: -420px; /* hors écran au départ */ + width: 380px; + max-width: calc(100vw - 40px); + padding: 14px 18px; + background: var(--bg-elevated); + border: 1px solid var(--border); + border-left: 4px solid #f59e0b; + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0,0,0,0.25); + z-index: 1000; + transition: left 0.28s ease-out, opacity 0.28s; + opacity: 0; +} +.session-slide-alert.visible { + left: 20px; + opacity: 1; +} +.session-slide-alert.urgent { + border-left-color: #ef4444; + animation: session-pulse 1.4s ease-in-out infinite; +} +@keyframes session-pulse { + 0%, 100% { box-shadow: 0 8px 24px rgba(0,0,0,0.25); } + 50% { box-shadow: 0 8px 24px rgba(239,68,68,0.5); } +} +.session-slide-alert-title { + font-size: 14px; + font-weight: 600; + color: var(--text); + margin-bottom: 12px; +} +.session-slide-alert-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} +.session-slide-alert-extend, +.session-slide-alert-later { + padding: 6px 14px; + font-size: 13px; + border-radius: 6px; + border: 1px solid var(--border); + cursor: pointer; + font-family: inherit; +} +.session-slide-alert-extend { + background: #10b981; + color: white; + border-color: #10b981; + font-weight: 600; +} +.session-slide-alert-extend:hover { background: #059669; } +.session-slide-alert-extend:disabled { opacity: 0.6; cursor: wait; } +.session-slide-alert-later { + background: transparent; + color: var(--text-muted); +} +.session-slide-alert-later:hover { + background: var(--bg-muted); + color: var(--text); +} diff --git a/viewer.html b/viewer.html index 4865569..5164111 100644 --- a/viewer.html +++ b/viewer.html @@ -16,9 +16,14 @@

Planification

- - - + +
+
+ + 📅 +
+ +
diff --git a/viewer.js b/viewer.js index 1574930..0f90d75 100644 --- a/viewer.js +++ b/viewer.js @@ -242,6 +242,7 @@ async function init() { initAppClock(); // v5.0.0 : horloge HH:MM au milieu topbar initAdminMenu(); // v5.0.0 : menu admin caché (5 clics sur titre) initSessionTimer(); // v5.0.9 : compteur de session EV (tick 1s) + initDateCustomPicker(); // v2026.5.17 : faux input date avec jour // Initialiser la date = aujourd'hui state.currentDate = todayISO(); @@ -400,7 +401,21 @@ function toggleUserNamePopup() { return; } if (!state.currentUser || !state.currentUser.name) return; - popup.textContent = state.currentUser.name; + + // v2026.5.17 : afficher aussi le temps restant de la session (MM:SS) avec + // une couleur qui dépend du seuil (vert/jaune/rouge). + popup.innerHTML = ""; + const nameEl = document.createElement("div"); + nameEl.className = "user-name-popup-name"; + nameEl.textContent = state.currentUser.name; + popup.appendChild(nameEl); + + const sessEl = document.createElement("div"); + sessEl.className = "user-name-popup-session"; + sessEl.id = "user-name-popup-session"; + _renderUserPopupSessionLine(sessEl); + popup.appendChild(sessEl); + popup.classList.remove("hidden"); badge.classList.add("open"); // Positionne juste en dessous de la pastille @@ -415,6 +430,38 @@ function hideUserNamePopup() { if (badge) badge.classList.remove("open"); } +// v2026.5.17 : remplit la ligne "Session : MM:SS" avec couleur selon seuil. +// Recalcule à chaque appel — appelée aussi par le tick session pour rafraîchir. +function _renderUserPopupSessionLine(el) { + if (!el) return; + const remainingMs = _getSessionRemainingMs(); + if (remainingMs == null) { + el.textContent = "Session : —"; + el.className = "user-name-popup-session"; + return; + } + const mins = Math.floor(remainingMs / 60000); + const secs = Math.floor((remainingMs % 60000) / 1000); + const txt = `Session : ${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; + el.textContent = txt; + el.className = "user-name-popup-session"; + if (remainingMs <= SESSION_CRITICAL_THRESHOLD_MS) { + el.classList.add("session-critical"); + } else if (remainingMs <= SESSION_WARN_THRESHOLD_MS) { + el.classList.add("session-warn"); + } else { + el.classList.add("session-ok"); + } +} + +// v2026.5.17 : récupère en ms le temps restant avant expiration de la session. +// Retourne null si on ne connaît pas encore (pas de session ouverte). +function _getSessionRemainingMs() { + if (!state.sessionExpireAt) return null; + const remaining = state.sessionExpireAt - Date.now(); + return remaining > 0 ? remaining : 0; +} + // ============================================================================ // Thème clair/sombre // ============================================================================ @@ -839,21 +886,53 @@ function initAppClock() { setInterval(tick, 30 * 1000); } -// v2026.5.16 : met à jour le label court du jour affiché à gauche du -// date-picker (ex: "Mardi", "Lundi"). Appelé à chaque changement de date. +// v2026.5.17 : met à jour le faux input date custom (ex: "Vendredi 24.04.2026") +// Remplace l'ancien updateDatePickerDayLabel. L'input date natif reste présent +// mais caché, et son onChange continue de déclencher le chargement. const DAY_NAMES_FULL = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"]; function updateDatePickerDayLabel(isoDate) { - const el = document.getElementById("date-picker-day"); + const el = document.getElementById("date-custom-label"); if (!el) return; if (!isoDate) { el.textContent = ""; return; } try { const d = isoToDate(isoDate); - el.textContent = DAY_NAMES_FULL[d.getDay()]; + const day = DAY_NAMES_FULL[d.getDay()]; + const dd = String(d.getDate()).padStart(2, "0"); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const yyyy = d.getFullYear(); + el.textContent = `${day} ${dd}.${mm}.${yyyy}`; } catch (e) { el.textContent = ""; } } +// v2026.5.17 : brancher le faux input date — clic dessus ouvre le vrai input +// caché pour choisir une date. +function initDateCustomPicker() { + const custom = document.getElementById("date-custom"); + const picker = document.getElementById("date-picker"); + if (!custom || !picker) return; + const openPicker = () => { + try { + if (typeof picker.showPicker === "function") { + picker.showPicker(); + } else { + picker.focus(); + picker.click(); + } + } catch (e) { + picker.focus(); + } + }; + custom.addEventListener("click", openPicker); + custom.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + openPicker(); + } + }); +} + // v5.0.0 : ligne verticale rouge "heure actuelle" sur la timeline, visible // UNIQUEMENT quand on affiche aujourd'hui. Appelée à chaque tick de l'horloge // + après chaque render (cf renderFromData). @@ -1043,6 +1122,103 @@ function updateSessionIndicator() { } }; } + + // v2026.5.17 : si le popup user-badge est ouvert, rafraîchir la ligne "Session : MM:SS" + const sessLineInPopup = document.getElementById("user-name-popup-session"); + if (sessLineInPopup) _renderUserPopupSessionLine(sessLineInPopup); + + // v2026.5.17 : popup d'alerte "glissante" depuis le haut gauche + // - à 5 min : alerte standard (si pas encore affichée ni "plus tard") + // - à 2 min : alerte urgente (si pas encore affichée) + _handleSessionSlideAlerts(remainingMs); +} + +/** + * v2026.5.17 : gère les 2 alertes popup glissant depuis le haut gauche. + * - Première alerte à 5 min (SESSION_WARN_THRESHOLD_MS). Reste affichée jusqu'à + * action manuelle (Prolonger ou Plus tard). + * - Si "Plus tard", une 2e alerte plus urgente réapparait à 2 min + * (SESSION_CRITICAL_THRESHOLD_MS). + */ +function _handleSessionSlideAlerts(remainingMs) { + if (remainingMs == null) return; + + // Alerte à 5 min + if (remainingMs <= SESSION_WARN_THRESHOLD_MS + && remainingMs > SESSION_CRITICAL_THRESHOLD_MS + && !state._slideAlert5minShown) { + state._slideAlert5minShown = true; + _showSessionSlideAlert({ urgent: false }); + } + + // Alerte à 2 min (si déjà "Plus tard" sur l'alerte 5 min OU alerte 5 min jamais affichée) + if (remainingMs <= SESSION_CRITICAL_THRESHOLD_MS + && !state._slideAlert2minShown) { + state._slideAlert2minShown = true; + // Cacher éventuellement l'ancienne alerte pour ré-afficher la nouvelle + _hideSessionSlideAlert(); + _showSessionSlideAlert({ urgent: true }); + } +} + +function _showSessionSlideAlert({ urgent }) { + // Retirer l'ancienne si elle existe + _hideSessionSlideAlert(); + + const el = document.createElement("div"); + el.id = "session-slide-alert"; + el.className = "session-slide-alert" + (urgent ? " urgent" : ""); + const title = urgent ? "⚠ Session expire dans 2 minutes !" : "⏱ Session expire dans 5 minutes"; + el.innerHTML = ` +
${title}
+
+ + +
+ `; + document.body.appendChild(el); + // Déclenche l'animation de slide-in (petite tempo pour que la transition parte) + requestAnimationFrame(() => el.classList.add("visible")); + + // Action "Prolonger" + el.querySelector(".session-slide-alert-extend").addEventListener("click", async () => { + const extendBtn = el.querySelector(".session-slide-alert-extend"); + extendBtn.disabled = true; + extendBtn.textContent = "…"; + try { + const resp = await sendMessage({ type: "extendSession" }); + if (resp && resp.ok && typeof resp.remainingMs === "number") { + state.sessionExpireAt = Date.now() + resp.remainingMs; + state.sessionPingDone = false; + state._criticalModalShown = false; + // Reset des flags d'alerte pour le prochain cycle + state._slideAlert5minShown = false; + state._slideAlert2minShown = false; + showToast("Session prolongée", "30 minutes de plus"); + updateSessionIndicator(); + _hideSessionSlideAlert(); + } else { + throw new Error((resp && resp.error) || "erreur inconnue"); + } + } catch (err) { + extendBtn.disabled = false; + extendBtn.textContent = "🔄 Prolonger"; + } + }); + + // Action "Plus tard" + el.querySelector(".session-slide-alert-later").addEventListener("click", () => { + _hideSessionSlideAlert(); + // Si c'est l'alerte 5 min qu'on dismissa, l'alerte 2 min reviendra + // automatiquement (state._slideAlert2minShown toujours false). + }); +} + +function _hideSessionSlideAlert() { + const el = document.getElementById("session-slide-alert"); + if (!el) return; + el.classList.remove("visible"); + setTimeout(() => { try { el.remove(); } catch (e) {} }, 250); } /** @@ -2188,12 +2364,11 @@ async function writeCache(isoDate, data) { // ============================================================================ async function loadForDate(isoDate, opts = {}) { - // v4.3.1 : changer de date ferme tous les popups épinglés. Ils réfèrent à - // des interventions du jour courant, ils n'ont aucun sens sur un autre jour. + // v4.3.1 : changer de date fermait tous les popups épinglés. + // v2026.5.17 : les popups épinglés restent maintenant ouverts entre dates, + // avec les données qu'ils avaient au moment de l'épinglage. L'utilisateur + // peut les fermer manuellement s'il veut. const previousDate = state.currentDate; - if (previousDate && previousDate !== isoDate && typeof closeAllPinnedPopups === "function") { - closeAllPinnedPopups(); - } state.currentDate = isoDate; document.getElementById("date-picker").value = isoDate; @@ -5991,7 +6166,8 @@ function hideTooltip(opts = {}) { state.currentTooltipIv = null; currentTooltipPos = null; tooltipPositionMode = null; // re-détecter à la prochaine ouverture - }, 120); + }, 1000); // v2026.5.17 : délai 1s au lieu de 120ms pour laisser le temps + // à l'user d'atteindre le popup depuis la carte } // v4.2 : détecte si l'utilisateur a une sélection de texte active dans la bulle. @@ -6125,9 +6301,51 @@ function positionTooltipAnchored(rowEl) { } if (y < 4) y = 4; + // v2026.5.17 : éviter le chevauchement avec les popups épinglés existants. + // On teste la position candidate, et si elle chevauche un popup épinglé, + // on essaie d'autres candidats (gauche de la carte, au-dessous, au-dessus). + const tipW = tipRect.width || 320; + const tipH = tipRect.height || 200; + const pinnedRects = _getPinnedPopupsViewportRects(); + if (pinnedRects.length) { + const candidates = [ + { x, y, label: "right" }, + { x: rowRect.left - tipW - pad, y: rowRect.top, label: "left" }, + { x: rowRect.left, y: rowRect.bottom + pad, label: "below" }, + { x: rowRect.left, y: rowRect.top - tipH - pad, label: "above" } + ]; + for (const c of candidates) { + // Borne dans le viewport + if (c.x < 4) c.x = 4; + if (c.x + tipW > window.innerWidth - 8) c.x = window.innerWidth - tipW - 8; + if (c.y < 4) c.y = 4; + if (c.y + tipH > window.innerHeight - 8) c.y = window.innerHeight - tipH - 8; + const testRect = { left: c.x, top: c.y, right: c.x + tipW, bottom: c.y + tipH }; + const overlaps = pinnedRects.some(pr => _rectsOverlap(testRect, pr)); + if (!overlaps) { + x = c.x; y = c.y; + break; + } + } + } + setTooltipViewportPosition(x, y); } +/** + * v2026.5.17 : retourne les rectangles (en coords viewport) de tous les popups + * actuellement épinglés et visibles (non réduits). Utilisé pour anti-chevauchement. + */ +function _getPinnedPopupsViewportRects() { + const rects = []; + document.querySelectorAll(".pinned-popup").forEach(p => { + if (p.classList.contains("pinned-popup-reduced")) return; // docké, pas à l'écran + const r = p.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) rects.push(r); + }); + return rects; +} + // ============================================================================ // v4.3.0 : système de popups épinglés détachés // ============================================================================ @@ -6258,20 +6476,55 @@ 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.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é : + // _ = Minimiser (popup reste flottant mais compact, juste la ref) + // ▭ = Réduire (docké dans la taskbar du bas) + // 📍 = 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 Minimiser + 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); + + // Bouton Réduire + 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 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); + topbar.appendChild(unpinBtn); + + popup.appendChild(topbar); // v4.3.3 : barre de drag en haut, pour déplacer la popup à la souris. // Ancrée en haut à 22px de haut ; le padding-top de la popup est augmenté @@ -6377,6 +6630,11 @@ function _softUnpinPopup(el) { if (dragbar) dragbar.remove(); const closeBtn = el.querySelector(".pinned-popup-close"); if (closeBtn) closeBtn.remove(); + // v2026.5.17 : retirer aussi la nouvelle topbar et le conteneur minimisé + const topbar = el.querySelector(".pinned-popup-topbar"); + if (topbar) topbar.remove(); + el.classList.remove("pinned-popup-minimized"); + el.classList.remove("pinned-popup-reduced"); // Helper qui joue l'animation de sortie puis supprime le DOM const animateAndRemove = () => { @@ -6393,6 +6651,164 @@ function _softUnpinPopup(el) { el.addEventListener("mouseleave", animateAndRemove, { once: true }); } +// ============================================================================ +// v2026.5.17 : États d'un popup épinglé +// - Normal (complet, flottant) +// - Minimisé (compact, flottant, juste la ref + topbar) +// - Réduit (docké dans la taskbar en bas de l'écran) +// ============================================================================ + +/** + * Passe un popup épinglé en mode Minimisé : on ne montre plus que la ref, + * dans un petit cadre flottant toujours drag-able. + */ +function _minimizePinnedPopup(popup) { + if (!popup) return; + popup.classList.add("pinned-popup-minimized"); + + // Adapter les boutons topbar : [_] devient [⬆] (agrandir) + const minBtn = popup.querySelector(".pinned-popup-minimize"); + if (minBtn) { + minBtn.innerHTML = "⬆"; + minBtn.title = "Agrandir"; + // On retire les anciens listeners en clonant l'élément + const newBtn = minBtn.cloneNode(true); + minBtn.replaceWith(newBtn); + newBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _expandPinnedPopup(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); + } +} + +function _onMinimizedRefClick(e) { + const popup = e.currentTarget.closest(".pinned-popup"); + if (popup) _expandPinnedPopup(popup); +} + +/** + * Repasse un popup minimisé en mode Normal (complet). + */ +function _expandPinnedPopup(popup) { + if (!popup) return; + popup.classList.remove("pinned-popup-minimized"); + + // Restaurer bouton Minimiser + const minBtn = popup.querySelector(".pinned-popup-minimize"); + if (minBtn) { + minBtn.innerHTML = "_"; + minBtn.title = "Minimiser (reste flottant mais compact)"; + const newBtn = minBtn.cloneNode(true); + minBtn.replaceWith(newBtn); + newBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _minimizePinnedPopup(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); + } +} + +/** + * Passe un popup épinglé en mode Réduit : il disparaît de son emplacement + * flottant et vient s'ajouter dans une taskbar en bas de l'écran sous forme + * de pastille cliquable. + */ +function _reducePinnedPopup(popup) { + if (!popup) return; + + // Récupérer la référence pour le label de la pastille + const refEl = popup.querySelector(".iv-ref-header"); + const label = refEl ? (refEl.textContent || "").trim() || "Popup" : "Popup"; + + // S'assurer que la taskbar du bas existe + let dock = document.getElementById("pinned-popups-dock"); + if (!dock) { + dock = document.createElement("div"); + dock.id = "pinned-popups-dock"; + dock.className = "pinned-popups-dock"; + document.body.appendChild(dock); + } + + // Créer la pastille dock + const pill = document.createElement("button"); + pill.type = "button"; + pill.className = "pinned-popup-dock-pill"; + pill.textContent = label; + pill.title = "Cliquer pour agrandir"; + + // Mémoriser la position/taille du popup avant de le masquer + const rect = popup.getBoundingClientRect(); + popup.dataset.prevLeft = popup.style.left || (rect.left + "px"); + popup.dataset.prevTop = popup.style.top || (rect.top + "px"); + popup.dataset.prevWidth = popup.style.width || ""; + + // Cacher le popup (on le garde en DOM pour conserver son état et restaurer + // instantanément) + popup.classList.add("pinned-popup-reduced"); + + // Associer pill ↔ popup + pill._linkedPopup = popup; + popup._linkedPill = pill; + + pill.addEventListener("click", (e) => { + e.stopPropagation(); + _restorePinnedPopupFromDock(popup); + }); + + dock.appendChild(pill); + dock.classList.add("visible"); +} + +/** + * Ramène un popup réduit en mode Normal : retire la pastille du dock et + * réaffiche le popup flottant à sa position d'avant réduction. + */ +function _restorePinnedPopupFromDock(popup) { + if (!popup) return; + popup.classList.remove("pinned-popup-reduced"); + // Si le popup était minimisé avant d'être réduit, on l'agrandit direct + // (la demande était : "Si la reduit et rappeller s'affiche en grand direct") + popup.classList.remove("pinned-popup-minimized"); + const minBtn = popup.querySelector(".pinned-popup-minimize"); + if (minBtn) { + minBtn.innerHTML = "_"; + minBtn.title = "Minimiser (reste flottant mais compact)"; + const newBtn = minBtn.cloneNode(true); + minBtn.replaceWith(newBtn); + newBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _minimizePinnedPopup(popup); + }); + } + + // Supprimer la pastille associée + if (popup._linkedPill) { + popup._linkedPill.remove(); + popup._linkedPill = null; + } + + // Si le dock est vide, le masquer + const dock = document.getElementById("pinned-popups-dock"); + if (dock && dock.children.length === 0) { + dock.classList.remove("visible"); + } +} + /** Ferme toutes les popups épinglées (appelé par Échap ou changement de date). */ /** * v5.0.1 : helper pour déclencher la suppression d'une absence ou réservation.