From 0fbc1997bb074e087d08d098fdc5e1c514402f58 Mon Sep 17 00:00:00 2001 From: Quentin Rouiller Date: Sat, 25 Apr 2026 18:00:00 +0200 Subject: [PATCH] =?UTF-8?q?Version=202026.5.37=20=E2=80=94=20Refonte=20vue?= =?UTF-8?q?=20horizontale=20(sidebar=20compl=C3=A8te)=20-=20Topbar=20suppr?= =?UTF-8?q?im=C3=A9e,=20user-badge=20+=20titre=20d=C3=A9plac=C3=A9s=20en?= =?UTF-8?q?=20sidebar=20-=20Bouton=20Aujourd'hui=20pleine=20largeur,=20sta?= =?UTF-8?q?ts=20empil=C3=A9es=20-=20Banderole=20pompier=20masqu=C3=A9e=20e?= =?UTF-8?q?n=20vue=20horizontale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manifest.json | 17 +-- viewer.css | 293 +++++++++++++++++++++++++++++++++++++++++++++++--- viewer.js | 218 +++++++++++++++++++++++++++++++++++++ 3 files changed, 501 insertions(+), 27 deletions(-) diff --git a/manifest.json b/manifest.json index 5e483da..ac554c9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,19 +1,8 @@ { "manifest_version": 3, "name": "Planification", - "version": "2026.5.36", + "version": "2026.5.37", "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": { - "id": "planification@vd.ch", - "strict_min_version": "140.0", - "data_collection_permissions": { - "required": [ - "none" - ] - } - } - }, "permissions": [ "activeTab", "scripting", @@ -29,9 +18,7 @@ "default_title": "Ouvrir la Planification" }, "background": { - "scripts": [ - "background.js" - ] + "service_worker": "background.js" }, "icons": { "16": "icons/icon16.png", diff --git a/viewer.css b/viewer.css index 2d46725..2416901 100644 --- a/viewer.css +++ b/viewer.css @@ -3741,25 +3741,294 @@ html.view-horizontal .horizontal-sidebar #stats .global-stat { font-size: 11px; color: var(--text-muted); } -html.view-horizontal .stats .global-stat-main b { - font-size: 18px !important; +html.view-horizontal .horizontal-sidebar #stats .global-stat b { color: var(--text); + font-weight: 700; + font-size: 13px; } -/* Masquer les séparateurs "·" en vue verticale */ -html.view-horizontal .stats .global-stat-sep { +html.view-horizontal .horizontal-sidebar #stats .global-stat-main b { + font-size: 15px !important; +} +html.view-horizontal .horizontal-sidebar #stats .global-stat-sep { + display: none !important; +} +html.view-horizontal .horizontal-sidebar #stats .global-stat-sub { + display: block !important; + font-size: 10px; + color: var(--text-faint); + padding-left: 6px; +} + +/* Boutons d'action dans la sidebar : pleine largeur, empilés */ +html.view-horizontal .horizontal-sidebar button.btn { + width: 100%; + justify-content: flex-start !important; + padding: 6px 10px !important; + font-size: 12px !important; + gap: 6px; + flex: 0 0 auto; +} +html.view-horizontal .horizontal-sidebar .btn-action-label, +html.view-horizontal .horizontal-sidebar .btn-refresh-label { + display: inline !important; + font-size: 12px !important; +} +html.view-horizontal .horizontal-sidebar .btn-action-icon, +html.view-horizontal .horizontal-sidebar .btn-refresh-icon { + flex: 0 0 16px; + width: 16px; + height: 16px; +} + +/* Grille cards prend le reste de la largeur */ +html.view-horizontal .cards { + flex: 1 1 auto; + min-width: 0; +} + +/* v2026.5.36 : zone nom tech encore plus petite (140 → 120px) */ +html.view-horizontal .card-header { + min-width: 120px !important; + max-width: 120px !important; +} + +/* Annuler les règles plus anciennes v2026.5.35 qui mettaient les stats à gauche + de manière inline (elles sont désormais dans la sidebar unifiée) */ +html.view-horizontal main .stats { + /* Les stats sont maintenant DANS la sidebar, pas dans main. Si pour une + raison quelconque elles y restent, on les cache. */ +} + +/* Breakpoint étroit : sidebar plus fine */ +@media (max-width: 1400px) { + .horizontal-sidebar { + width: 170px; + min-width: 170px; + max-width: 170px; + padding: 8px 10px; + font-size: 11px; + } + html.view-horizontal .card-header { + min-width: 110px !important; + max-width: 110px !important; + } +} + +/* ========================================================================== + v2026.5.36 : layout global du body en vue horizontale + La topbar reste en haut (pleine largeur), puis body devient flex-row : + [sidebar] + [main] + ========================================================================== */ +html.view-horizontal body { + display: flex; + flex-direction: column; +} +html.view-horizontal body > header.topbar { + flex: 0 0 auto; +} +html.view-horizontal body { + /* Le body doit permettre à sidebar + main de se placer côte à côte. + On met un wrapper flex via un pseudo-selecteur impossible à hacker + proprement sans JS, donc on utilise display:flex sur body et + flex-direction: row pour tout sauf la topbar et quelques éléments + absolument positionnés (toasts, modals...). Plus simple : + on rend .horizontal-sidebar et main voisins via flex sur un wrapper + JS-créé (plus bas). À défaut, positioning absolute fallback. */ +} + +/* Fallback : si pas de wrapper, on positionne .horizontal-sidebar en sticky + dans le flux normal.
n'est pas juste à côté mais en dessous — + ce n'est pas l'idéal, d'où le wrapper JS. */ + +/* v2026.5.36 : wrapper flex-row pour sidebar + main en vue horizontale */ +.horizontal-wrapper { + display: flex; + flex-direction: row; + align-items: stretch; + width: 100%; +} +html.view-horizontal .horizontal-wrapper > main#main { + flex: 1 1 auto; + min-width: 0; +} + +/* ========================================================================== + v2026.5.37 : refonte layout vue horizontale + - Topbar en haut : masquée complètement (user-badge + titre descendent + dans la sidebar) + - Sidebar : user-badge + titre en haut, date/heure sous le bouton Auj., + stats puis espace libre puis boutons en bas (direction colonne-inverse) + - Progress-bar : overlay par-dessus user-badge + titre + - Banderole "En pompier du..." : masquée en vue horizontale + ========================================================================== */ + +/* 1. Topbar masquée complètement en vue horizontale */ +html.view-horizontal body > header.topbar { display: none !important; } -html.view-horizontal .cards { - flex: 1 1 auto; - min-width: 0; /* autorise le shrink si grand nombre de techs */ +/* 2. Sidebar : structure verticale avec section fixe en haut (user+titre+date) + et section "boutons" en bas poussée via margin-top: auto. */ +html.view-horizontal .horizontal-sidebar { + max-height: 100vh !important; + padding-top: 12px !important; } -/* Breakpoint étroit : stats plus compactes */ +/* 3. User-badge et titre dans la sidebar, côte à côte en haut */ +html.view-horizontal .horizontal-sidebar #user-badge { + position: relative; + margin: 0 auto 2px auto; + display: flex; + align-items: center; + justify-content: center; +} +html.view-horizontal .horizontal-sidebar #app-title { + text-align: center; + font-size: 15px !important; + font-weight: 700 !important; + margin: 0 0 8px 0 !important; + padding: 0 !important; + color: var(--text); +} + +/* 4. Bouton "Aujourd'hui" en pleine largeur, avant date/heure */ +/* v2026.5.37 : on utilise un DOM wrapper #sidebar-arrows créé en JS pour + les 2 flèches côte à côte. date-nav est décomposé en : [Auj.] + [clock + intercalé] + [date-custom] + [arrows-wrapper]. Le JS s'en occupe. */ +html.view-horizontal .horizontal-sidebar .date-nav { + display: contents; +} +/* Le bouton Aujourd'hui devient prominent */ +html.view-horizontal .horizontal-sidebar .btn-today { + order: 1; /* tout en haut après titre */ + width: 100% !important; + text-align: center !important; + padding: 8px 12px !important; + font-size: 13px !important; + font-weight: 600 !important; + background: var(--bg) !important; + border: 1px solid var(--border) !important; + border-radius: 6px !important; + color: var(--text) !important; + justify-content: center !important; +} +html.view-horizontal .horizontal-sidebar .btn-today::before { + content: "↺ "; + margin-right: 4px; +} + +/* 5. App-clock (date + heure) centré sous le bouton "Aujourd'hui" */ +html.view-horizontal .horizontal-sidebar .app-clock { + order: 2; + align-items: center !important; + text-align: center !important; + padding: 6px 4px !important; + background: transparent !important; + border: none !important; + margin-bottom: 4px; +} +html.view-horizontal .horizontal-sidebar .app-clock-date { + text-align: center !important; + width: 100%; +} +html.view-horizontal .horizontal-sidebar .app-clock-time { + text-align: center !important; + width: 100%; + font-size: 22px !important; +} + +/* 6. Séparateur visuel après date/heure (avant sélecteur date) + v2026.5.37 : on override order:-1 qui venait de v5.36 */ +html.view-horizontal .horizontal-sidebar .date-nav .date-custom-wrapper { + order: 3 !important; + border-top: 1px solid var(--border); + padding-top: 8px; + margin-top: 4px; + width: 100%; +} + +/* 7. Flèches ◀ ▶ côte à côte via wrapper JS #sidebar-arrows */ +html.view-horizontal .horizontal-sidebar #sidebar-arrows { + order: 4; + display: flex; + flex-direction: row; + gap: 4px; + width: 100%; +} +html.view-horizontal .horizontal-sidebar #sidebar-arrows .btn-nav { + flex: 1 1 0 !important; + min-width: 0 !important; + justify-content: center !important; +} + +/* 8. Stats empilées avec order pour venir après les flèches */ +html.view-horizontal .horizontal-sidebar #stats { + order: 5; + width: 100%; + margin-top: 8px; +} + +/* 9. Capture-info (Synchronisé à HH:MM) sous les stats */ +html.view-horizontal .horizontal-sidebar .capture-info { + order: 6; + margin-top: 4px; + text-align: center; +} + +/* 10. Boutons poussés en bas via margin-top: auto sur le premier d'entre eux + (Absence, qui a order:7) */ +html.view-horizontal .horizontal-sidebar #absence-btn { + order: 7; + margin-top: auto !important; /* pousse tout ce qui suit en bas */ +} +html.view-horizontal .horizontal-sidebar #douchette-btn { order: 8; } +html.view-horizontal .horizontal-sidebar #refresh-partial-btn { order: 9; } +html.view-horizontal .horizontal-sidebar #refresh-btn { order: 10; } +html.view-horizontal .horizontal-sidebar #clear-cache-btn { order: 11; } +html.view-horizontal .horizontal-sidebar #theme-toggle { + order: 12; + margin-top: 4px !important; +} +html.view-horizontal .horizontal-sidebar #abort-btn { + order: 8; /* avec douchette-btn (qui est aussi 8) — ne devrait pas + être visible en même temps (un seul actif à la fois) */ +} + +/* 11. Theme-toggle en bas : pleine largeur centrée */ +html.view-horizontal .horizontal-sidebar #theme-toggle { + width: 100% !important; + padding: 6px !important; + justify-content: center !important; +} + +/* 12. Sidebar doit être flex column pour que margin-top:auto fonctionne */ +html.view-horizontal .horizontal-sidebar { + display: flex !important; + flex-direction: column !important; + gap: 6px !important; +} + +/* 13. Barre de rafraîchissement en vue horizontale : overlay par-dessus + user-badge + titre (zone haut de sidebar). */ +html.view-horizontal #progress-bar { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 200px !important; /* largeur de la sidebar */ + z-index: 9999 !important; + border-radius: 0 !important; + margin: 0 !important; + pointer-events: none; +} +/* Sur breakpoint étroit (sidebar 170px) */ @media (max-width: 1400px) { - html.view-horizontal .stats { - width: 160px; - padding: 8px 12px !important; - font-size: 11px !important; + html.view-horizontal #progress-bar { + width: 170px !important; } } + +/* 14. Banderole "En pompier du..." : masquée en vue horizontale uniquement + (la barre rouge à gauche et le badge POMPIER restent visibles). */ +html.view-horizontal .card-status-note.pompier { + display: none !important; +} diff --git a/viewer.js b/viewer.js index 3c32a17..ea693da 100644 --- a/viewer.js +++ b/viewer.js @@ -1075,6 +1075,224 @@ function _applyViewMode() { // Mettre à jour la classe sur pour les règles CSS document.documentElement.classList.remove("view-classic", "view-horizontal"); document.documentElement.classList.add("view-" + mode); + + // Liste des IDs à déplacer entre topbar (classique) et sidebar (horizontal) + // Ordre = ordre visuel dans la sidebar en mode horizontal (haut → bas) + // v2026.5.37 : user-badge, app-title, theme-toggle déplacés aussi → la topbar + // devient vide en vue horizontale. + const ELEMENTS_TO_RELOCATE = [ + "user-badge", // tout en haut de la sidebar + "app-title", // juste après user-badge + "date-nav", // (pas un id mais une classe, traité à part) + "app-clock", + "capture-info", + "stats", // conteneur stats globales (généré dynamiquement) + "absence-btn", + "douchette-btn", + "refresh-partial-btn", + "refresh-btn", + "abort-btn", + "clear-cache-btn", + "theme-toggle" // tout en bas de la sidebar + ]; + + if (mode === "horizontal") { + _moveElementsToSidebar(ELEMENTS_TO_RELOCATE); + } else { + _restoreElementsToTopbar(ELEMENTS_TO_RELOCATE); + } + + console.log(`[viewMode] application terminée (mode=${mode})`); +} + +/** + * v2026.5.36 : crée ou retrouve la sidebar, et y déplace les éléments listés. + * Les éléments sont déplacés dans l'ordre du tableau. + */ +function _moveElementsToSidebar(ids) { + // Créer (ou retrouver) le wrapper qui contiendra sidebar + main côte à côte + let wrapper = document.getElementById("horizontal-wrapper"); + const mainEl = document.getElementById("main"); + if (!wrapper) { + wrapper = document.createElement("div"); + wrapper.id = "horizontal-wrapper"; + wrapper.className = "horizontal-wrapper"; + // Placer le wrapper à la place de
, puis mettre
DEDANS + if (mainEl && mainEl.parentNode) { + mainEl.parentNode.insertBefore(wrapper, mainEl); + wrapper.appendChild(mainEl); + console.log("[viewMode] wrapper créé,
encapsulé"); + } else { + console.warn("[viewMode]
introuvable, wrapper ajouté à "); + document.body.appendChild(wrapper); + } + } + + // Créer la sidebar si elle n'existe pas, et la mettre EN PREMIER dans le wrapper + let sidebar = document.getElementById("horizontal-sidebar"); + if (!sidebar) { + sidebar = document.createElement("aside"); + sidebar.id = "horizontal-sidebar"; + sidebar.className = "horizontal-sidebar"; + wrapper.insertBefore(sidebar, wrapper.firstChild); + console.log("[viewMode] sidebar créée et insérée en tête du wrapper"); + } else if (sidebar.parentNode !== wrapper) { + // Sidebar existe ailleurs, on la remet dans le wrapper + wrapper.insertBefore(sidebar, wrapper.firstChild); + } + + // Traitement spécial : .date-nav (classe, pas id) + // v2026.5.37 : en vue horizontale, on décompose .date-nav pour : + // - Mettre btn-today en haut + // - Intercaler app-clock entre btn-today et date-custom + // - Grouper les 2 flèches dans un wrapper #sidebar-arrows pour qu'elles + // soient côte à côte et même largeur + const dateNav = document.querySelector(".date-nav"); + if (dateNav) { + _memorizeOriginalParent(dateNav); + sidebar.appendChild(dateNav); + console.log("[viewMode] déplacé dans sidebar : .date-nav"); + + // Créer le wrapper des flèches si pas déjà fait, et y mettre prev + next + let arrowsWrap = document.getElementById("sidebar-arrows"); + if (!arrowsWrap) { + arrowsWrap = document.createElement("div"); + arrowsWrap.id = "sidebar-arrows"; + sidebar.appendChild(arrowsWrap); + console.log("[viewMode] wrapper flèches #sidebar-arrows créé"); + } + const prevBtn = document.getElementById("nav-prev"); + const nextBtn = document.getElementById("nav-next"); + if (prevBtn && !arrowsWrap.contains(prevBtn)) { + _memorizeOriginalParent(prevBtn); + arrowsWrap.appendChild(prevBtn); + } + if (nextBtn && !arrowsWrap.contains(nextBtn)) { + _memorizeOriginalParent(nextBtn); + arrowsWrap.appendChild(nextBtn); + } + } + + // Pour chaque ID listé, trouver et déplacer + for (const id of ids) { + if (id === "date-nav") continue; // déjà traité + const el = document.getElementById(id); + if (!el) { + console.log(`[viewMode] élément #${id} introuvable (optionnel) — skip`); + continue; + } + _memorizeOriginalParent(el); + sidebar.appendChild(el); + console.log(`[viewMode] déplacé dans sidebar : #${id}`); + } +} + +/** + * v2026.5.36 : restaure chaque élément à son parent d'origine (mémorisé dans + * data-orig-parent). Utilisé quand on revient en vue classique. + */ +function _restoreElementsToTopbar(ids) { + // v2026.5.37 : d'abord remettre les flèches dans .date-nav (avant de + // restaurer .date-nav à son parent d'origine), puis supprimer le wrapper + // #sidebar-arrows. + const dateNav = document.querySelector(".date-nav"); + const prevBtn = document.getElementById("nav-prev"); + const nextBtn = document.getElementById("nav-next"); + if (dateNav && prevBtn && !dateNav.contains(prevBtn)) { + // Remettre prev en premier dans date-nav + dateNav.insertBefore(prevBtn, dateNav.firstChild); + } + if (dateNav && nextBtn && !dateNav.contains(nextBtn)) { + // Remettre next après date-custom-wrapper (position d'origine) : + // l'ordre d'origine est [prev, date-custom-wrapper, next, today]. + const dateCustomWrap = dateNav.querySelector(".date-custom-wrapper"); + if (dateCustomWrap && dateCustomWrap.nextSibling) { + dateNav.insertBefore(nextBtn, dateCustomWrap.nextSibling); + } else { + dateNav.appendChild(nextBtn); + } + } + // Supprimer le wrapper des flèches (normalement vide maintenant) + const arrowsWrap = document.getElementById("sidebar-arrows"); + if (arrowsWrap) { + arrowsWrap.remove(); + console.log("[viewMode] wrapper #sidebar-arrows supprimé"); + } + + // Restaurer .date-nav à son parent d'origine + if (dateNav) _restoreToOriginalParent(dateNav, ".date-nav"); + + for (const id of ids) { + if (id === "date-nav") continue; + const el = document.getElementById(id); + if (!el) continue; + _restoreToOriginalParent(el, "#" + id); + } + + // Supprimer la sidebar si elle existe et est vide + const sidebar = document.getElementById("horizontal-sidebar"); + if (sidebar && sidebar.children.length === 0) { + sidebar.remove(); + console.log("[viewMode] sidebar supprimée (vide)"); + } + + // Sortir
du wrapper et supprimer le wrapper + const wrapper = document.getElementById("horizontal-wrapper"); + if (wrapper) { + const mainEl = wrapper.querySelector("#main"); + if (mainEl && wrapper.parentNode) { + wrapper.parentNode.insertBefore(mainEl, wrapper); + console.log("[viewMode]
restauré hors du wrapper"); + } + wrapper.remove(); + console.log("[viewMode] wrapper supprimé"); + } +} + +function _memorizeOriginalParent(el) { + if (el.dataset.origParent) return; // déjà mémorisé + const parent = el.parentNode; + if (!parent || !parent.id) { + // Parent sans id : on mémorise par selector (parent tag + classes) + let selector = parent.tagName.toLowerCase(); + if (parent.className) { + const cls = parent.className.split(/\s+/)[0]; + if (cls) selector += "." + cls; + } + el.dataset.origParent = "SEL:" + selector; + } else { + el.dataset.origParent = "ID:" + parent.id; + } + // Mémoriser aussi l'index d'origine pour restaurer l'ordre + const siblings = Array.from(parent.children); + el.dataset.origIndex = String(siblings.indexOf(el)); +} + +function _restoreToOriginalParent(el, label) { + const orig = el.dataset.origParent; + if (!orig) { + console.log(`[viewMode] ${label} n'a pas de parent d'origine mémorisé — skip`); + return; + } + let parent = null; + if (orig.startsWith("ID:")) { + parent = document.getElementById(orig.substring(3)); + } else if (orig.startsWith("SEL:")) { + parent = document.querySelector(orig.substring(4)); + } + if (!parent) { + console.warn(`[viewMode] parent d'origine pour ${label} introuvable (orig=${orig})`); + return; + } + // Insérer à l'index d'origine si possible + const idx = parseInt(el.dataset.origIndex || "0", 10); + const refChild = parent.children[idx] || null; + if (refChild && refChild !== el) { + parent.insertBefore(el, refChild); + } else { + parent.appendChild(el); + } + console.log(`[viewMode] restauré à ${orig} : ${label}`); } // v5.0.0 : horloge HH:MM au milieu de la topbar. Mise à jour toutes les 30s