Compare commits

..

28 Commits

Author SHA1 Message Date
FroSteel e69482add4 Version 2026.5.23 — Reset bulleState.pinned + iv._reloading
[code interpolé v2026.5.22 → v2026.5.35]
2026-04-22 13:00:00 +02:00
FroSteel a382d8f35f Version 2026.5.22 — Régénération tooltip hover après softUnpin 2026-04-22 11:00:00 +02:00
FroSteel 7824990fba Version 2026.5.21 — Ajustements
[code interpolé]
2026-04-22 09:00:00 +02:00
FroSteel e7c5e281d9 Version 2026.5.20 — Safe area popups (topbar + dock)
[code interpolé]
2026-04-21 17:00:00 +02:00
FroSteel c74d52c40c Version 2026.5.19 — Drag popup épinglé
[code interpolé]
2026-04-21 15:00:00 +02:00
FroSteel 8c76085f03 Version 2026.5.18 — Dock pastilles popups épinglés avec couleur catégorie
[code interpolé]
2026-04-21 13:00:00 +02:00
FroSteel f54ccd28d2 Version 2026.5.17 — Popup user-badge avec ligne session (MM:SS)
- Couleur selon seuil
[code interpolé]
2026-04-21 11:00:00 +02:00
FroSteel 72fb565afa Version 2026.5.16 — Passage au versionning par année (YYYY.M.PATCH)
- Format : YYYY.M.PATCH (2026.5.16 succède à 5.0.12)
- Bump du PATCH à chaque livraison
- L'année indique immédiatement la fraîcheur de l'extension
[code interpolé v5.0.12 → v2026.5.22]
2026-04-21 09:00:00 +02:00
FroSteel b3246d3cf2 Version 5.0.12 — Stabilisation finale série 5.0
Dernière version avant passage au système de versionning par année (YYYY.M.PATCH).
2026-04-20 17:00:00 +02:00
FroSteel 8435a2b77e Version 5.0.9 — Stabilisation série 5.0 2026-04-20 13:00:00 +02:00
FroSteel 6ae440cbf1 Version 5.0.0 — Refonte topbar (horloge, menu admin)
- initAppClock : horloge HH:MM au milieu topbar
- initAdminMenu : menu admin caché (5 clics sur titre)
- initSessionTimer : compteur de session EV (tick 1s)
[code interpolé entre v4.3.0 et v5.0.9]
2026-04-20 09:00:00 +02:00
FroSteel f6d549d522 Version 4.3.0 — Tooltip live libéré après épinglage 2026-04-19 18:00:00 +02:00
FroSteel 565075933e Version 4.2.8 — Corrections cumulées 4.2.4-8 2026-04-19 15:00:00 +02:00
FroSteel 7f78493859 Version 4.2.3 — Grande popup timeline persistante (bindTimelinePopover) 2026-04-19 12:00:00 +02:00
FroSteel 0b08ca122b Version 4.2.1 — Démarrage série 4.2 2026-04-19 09:00:00 +02:00
FroSteel 87f561ae10 Version 4.1.14 — moveTooltip devenu no-op (popup statique) 2026-04-18 18:00:00 +02:00
FroSteel be49a89057 Version 4.1.6 — Améliorations tooltip 2026-04-18 15:00:00 +02:00
FroSteel e42b145401 Version 4.1.4 — Corrections mineures tooltip 2026-04-18 12:00:00 +02:00
FroSteel 7201fde2d3 Version 4.1.3 — Introduction tooltips épinglables (pinTooltip) 2026-04-18 09:00:00 +02:00
FroSteel edd6ffc1c3 Version 3.3.0 — Corrections + raffinements
(manifest.json corrigé : était resté à 3.2.0 par oubli)
2026-04-17 18:00:00 +02:00
FroSteel 23244fc4db Version 3.2.0 — Stabilisation 3.2 2026-04-17 16:00:00 +02:00
FroSteel f52095dc4d Version 3.2.0 (pre-release) — Travail en cours sur la 3.2 2026-04-17 14:00:00 +02:00
FroSteel 94877cb816 Version 3.1.0 — Améliorations affichage 2026-04-17 11:00:00 +02:00
FroSteel 8ab62e92d2 Version 3.0.0 — Évolution majeure du viewer 2026-04-17 09:00:00 +02:00
FroSteel 8bc26c326f Version 2.0.1 — Ajustements interface v2 2026-04-16 17:00:00 +02:00
FroSteel d2afbf0dca Version 2.0.0 — Refonte interface et structure 2026-04-16 14:00:00 +02:00
FroSteel 3b1831a83a Version 1.0.0 — Initiale (extension de base sans tooltips avancés)
Première version stable de l'extension Planification : viewer pour planning EasyVista, fetch XML, affichage cards par tech.
2026-04-16 09:30:00 +02:00
FroSteel 43c6e0e487 Initial commit — LICENSE MIT + README + CHANGELOG + .gitignore 2026-04-16 09:00:00 +02:00
9 changed files with 2478 additions and 274 deletions
+39
View File
@@ -0,0 +1,39 @@
# OS
.DS_Store
Thumbs.db
desktop.ini
# Editors
.vscode/
.idea/
*.swp
*.swo
*~
# Backups
*.bak
*.bak-*
*.orig
*.old
# Build artifacts (les ZIP/XPI livrés ne sont pas dans le repo, ils sont buildés à la demande)
*.zip
*.xpi
*.crx
# Node (si jamais utilisé pour build)
node_modules/
package-lock.json
npm-debug.log*
# Logs
*.log
rebuild.log
# Dossiers de travail temporaires
extracted/
temp/
tmp/
# Tests
test-output/
+179
View File
@@ -0,0 +1,179 @@
# CHANGELOG — Extension Planification EasyVista Canton de Vaud
> Ce changelog documente l'évolution de l'extension Chrome/Firefox "Planification"
> développée par Quentin Rouiller pour les techniciens IT du Canton de Vaud.
>
> Les versions documentées ci-dessous sont celles dont les détails sont connus.
> Pour les versions plus anciennes, Claude Code se basera sur l'analyse du code
> source pour déterminer un message de commit pertinent.
---
## v2026.5.37 — Refonte vue horizontale (sidebar complète)
**Branche** : current
- Topbar en haut supprimée en vue horizontale
- User-badge + titre déplacés tout en haut de la sidebar
- Bouton "Aujourd'hui" pleine largeur avec icône ↺
- Date + heure centrés sous le bouton
- Séparateur visuel
- Sélecteur de date pleine largeur
- Flèches ◀ ▶ côte à côte (wrapper #sidebar-arrows)
- Stats empilées
- Synchronisé à HH:MM
- Espace vide intentionnel
- Boutons du bas vers le haut (margin-top: auto sur Absence)
- Barre de rafraîchissement en overlay top-left
- Banderole pompier masquée en vue horizontale (badge + barre rouge à gauche conservés)
## v2026.5.36 — Sidebar verticale en vue horizontale
- Création wrapper flex-row #horizontal-wrapper contenant [sidebar] + [main]
- Sidebar 200px (170px sur <1400px), sticky, bg-muted
- Déplacement physique des éléments via JS (ELEMENTS_TO_RELOCATE)
- Mémorisation parents d'origine (data-orig-parent + data-orig-index)
- Restauration propre en vue classique
- Zone nom tech : 140px → 120px
## v2026.5.35 — Fix popup épinglé position vue horizontale + stats gauche
- Fix popup épinglé qui partait en haut à gauche en vue horizontale
- Cause : rows .intervention-v2 cachées (display: none) → getBoundingClientRect (0,0,0,0)
- Solution : priorité 1 tooltip visible, priorité 2 segment timeline, fallback srcEl
- Stats globales en colonne verticale 200px à gauche en vue horizontale
- Position sticky, fond bg-muted, séparateurs · masqués
- Zone nom tech 200px → 140px (vue horizontale)
## v2026.5.34 — Bouton 📌 restauré + badge user cliquable
- HTML : badge user toujours visible avec "?" par défaut (retiré class hidden)
- _softUnpinPopup refait en 8 étapes loggées
- Popup reste visible après désépinglage (plus de suppression auto au mouseleave)
- Restauration du bouton 📌 dans .tooltip-actions
- Handler click ré-attaché : clic 📌 = ré-épingle, clic ↻ = recharge
- _ensureSoftUnpinnedCleanupHandler : handler global clic hors popup
- _maybeRetryFetchUser : relance opportuniste après succès planning et reconnexion session
- Logs abondants : [currentUser], [softUnpin], [positionTooltip], [persistentTimeline], [showTooltip]
- Fonction positionTooltipAnchored unifiée (4 candidats droite/gauche/dessous/dessus)
- popup._linkedIv stocké pour ré-épinglage
## v2026.5.33 — Interactions vue horizontale différenciées
- Hover segment timeline en vue horizontale → grande popup directement (openPersistentTimelinePopup)
- Clic segment timeline en vue horizontale → ouvre fiche EasyVista
- Popup absence en vue horizontale : hover uniquement sur badge .card-tech-badge (pas sur carte entière)
- Vue classique : comportement inchangé
## v2026.5.32 — Vue horizontale togglable
- Bouton ⊞ "Vue" dans popup user-badge (à côté ⚙ Paramètres)
- Toggle Vue classique ↔ Vue horizontale persisté localStorage "view_mode"
- HTML class "view-classic" ou "view-horizontal" sur <html>
- Chaque tech = 1 ligne horizontale compacte en mode horizontal
- Card header devient barre latérale gauche fixe 200px
- Interventions détaillées masquées (display: none)
- Timeline horizontale pleine largeur
- Stats rapides .tech-row-stats ajoutés au header (nb interv, Xm · Ya)
## v2026.5.31 — Sarcelle pour absence récurrente (REJETÉ par utilisateur)
- Couleur Pillonel vendredi : sarcelle foncée #0f766e / soft #ccfbf1
- Variables --c-recurring, --c-recurring-soft
- Layout 4 colonnes forcées + scroll interne cartes (REJETÉ : "scroll en continu")
## v2026.5.30 — Absence récurrente cyan + mode compact 24"
- Absence récurrente Pillonel vendredi en cyan
- Mode compact @media (max-width: 1920px) avec grid-template-columns: repeat(4, 1fr)
## v2026.5.29 — Contraste++ + footer
- Contrastes encore plus forts (text-muted #d0d5de dark, #2e3642 light)
- Footer QRO/version : 13px badge avec fond bg-muted + bordure
- Fix highlight row : selector .intervention-v2[data-iv-idx]
- Scroll-into-view automatique au hover segment timeline
## v2026.5.28 — Ajustements visuels absences
- Retrait pastille ronde (.tech-name-dot supprimée) — barre gauche + badge suffisent
- "Maladie" → "Maladie/Accident"
- Contraste textes secondaires +30%
- Popups épinglés width fixe 520px (ne rétrécit plus au resize fenêtre)
- _clampPopupInSafeArea ne rétrécit plus si popup > zone dispo
## v2026.5.27 — Classification absences (Maladie/Congé/Pompier)
- Topbar une ligne : "Jeudi 23.04.26 • 21:55" (gros point •, même taille 22px)
- Fermeture auto popups non-épinglés au survol autre popup/carte
- Texte +20% topbar/stats/boutons
- Icône thème ☀/🌙 plus contrastée (bordure 1.5px, fond bg-muted, ombre)
- Classification absences (ABSENCE_LABELS) + absenceCategory : "maladie"|"conge"|"pompier"|null
- Couleurs : Maladie #4338ca indigo foncé, Congé #06b6d4 cyan, Pompier #b03030 rouge
- Badge + barre gauche + dégradé fond pour catégorie
- Libellé "Absent du DD.MM au DD.MM — Maladie/Accident"
- Suffixe `s` adaptatif (Congé/Congés)
## v2026.5.26 — Badge user inconnu cliquable + retry
- En cas d'échec fetch user, afficher rond gris "?" cliquable
- Bouton ⚙ Paramètres accessible même quand user inconnu
- Retry automatique 60s (max 10 essais = 10 min)
- Reset compteur au succès
## v2026.5.25 — Bouton Paramètres dans popup user-badge
- Remplace les 5 clics sur le titre pour ouvrir admin
- Bouton ⚙ Paramètres explicite dans le popup user-badge
## v2026.5.16-v2026.5.24 — Évolutions diverses (à compléter)
- v2026.5.17 : popup user-badge avec ligne session (MM:SS), couleur selon seuil
- v2026.5.18 : dock pastilles popups épinglés avec couleur catégorie
- v2026.5.19 : drag popup épinglé
- v2026.5.20 : safe area popups (topbar + dock)
- v2026.5.22 : régénération tooltip hover après softUnpin
- v2026.5.23 : reset bulleState.pinned + iv._reloading
---
## Versions antérieures (v5.x et v4.x)
> Ces versions sont à analyser par Claude Code à partir des fichiers source.
> Indices clés à chercher dans le viewer.js :
>
> - Présence de `pinTooltip` → version >= v4.x
> - Présence de `_softUnpinPopup` → version >= v4.3.3
> - Présence de `initSessionTimer` → version >= v5.0.9
> - Présence de `initAppClock` → version >= v5.0.0
> - Présence de `_applyViewMode` → version >= v2026.5.32
> - Présence de `bindTimelinePopover` → version >= v4.2.3
> - Présence de `openPersistentTimelinePopup` → version >= v4.2.3
> - Commentaires `// vX.Y.Z` au-dessus des fonctions = version d'introduction
### v5.0.0 — Refonte topbar (horloge, menu admin)
- initAppClock : horloge HH:MM au milieu topbar
- initAdminMenu : menu admin caché (5 clics sur titre)
- initSessionTimer : compteur de session EV (tick 1s)
### v4.x — Fonctions tooltip avancées
- v4.1.12 : moveTooltip devenu no-op (popup statique)
- v4.1.15 : pendant épinglage, ne pas remplacer contenu sur hover autre iv
- v4.2.3 : grande popup timeline persistante (clic), suit-souris (hover)
- v4.2.3 : bindTimelinePopover, showTimelinePopover, moveTimelineTooltip
- v4.2.4 : setTooltipViewportPosition (détection auto fixed/abs)
- v4.2.9 : pied de page discret QRO/version
- v4.2.9 : initModalScrollLock (bloquer scroll arrière modal)
- v4.3.0 : tooltip live libéré après épinglage (réutilisable autres survols)
- v4.3.3 : _softUnpinPopup (désépinglage mou)
### v3.x et antérieures — Versions de base
- À analyser par Claude Code
---
## Notes techniques persistantes (toutes versions)
- 8 techs hardcodés : "76272,83725,66635,92235,90070,40944,72485,86874"
- Pillonel Olivier (ID 40944) absent tous les vendredis (hardcodé)
- Group ID EasyVista : 191
- Domaines cibles : itsma.etat-de-vaud.ch (interne), itsma.vd.ch (externe)
- SSO : Canton ForgeRock OpenAM
- ABSENCE_LABELS = /^(cong[ée]s|maladie|pompier)$/i
- ADMIN_CONFIG_KEY = "admin_config"
- VIEW_MODE_KEY = "view_mode" (depuis v2026.5.32)
- DAY_NAMES_FULL = ["Dimanche", "Lundi", ..., "Samedi"]
- GUIDs forms EV : S={C99ECD05-3D48-4C62-ABF0-66292053AED6} demande, I={07ED9C68-6172-48EA-8A58-90912B0A283E} incident
- Couleurs catégories : livraison #2563eb, recup #16a34a, remplacement #ea580c, incident #8b5cf6, rollout #92400e, reservation #f59e0b, autre #6b7280
## Auteur
**Quentin Rouiller** (QRO)
Canton de Vaud — Service IT
Email pour commits Git : `quentin.rouiller@ikmail.com`
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Quentin Rouiller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+156 -84
View File
@@ -1,110 +1,182 @@
# Planning techniciens — Vue claire (v4.1.2) # Planification — Extension EasyVista Canton de Vaud
Extension Chrome/Brave/Edge pour afficher le planning techniciens EasyVista Extension Chrome / Firefox pour visualiser de manière claire et rapide le planning des techniciens IT du Canton de Vaud dans EasyVista.
(`itsma.etat-de-vaud.ch` / `itsma.vd.ch`) dans une vue plus lisible.
## Nouveautés v4.1.2 ## Aperçu rapide
- **Vraies infos contact/lieu dans les cartes** : les attributs attr1/attr2 du - **Auteur** : Quentin Rouiller (QRO)
XML contiennent les infos saisies à la *planification*, qui ne sont pas - **Cible** : techniciens IT Canton de Vaud, EasyVista (`itsma.etat-de-vaud.ch` / `itsma.vd.ch`)
toujours à jour (le tech a pu corriger le contact/lieu avant intervention). - **Démarrage projet** : jeudi 16 avril 2026
Désormais, pour chaque intervention, on fetch AUSSI le xhr2 en arrière-plan - **Version actuelle** : `v2026.5.37`
(en plus de la fiche), ce qui apporte les **vraies** infos validées. La - **Manifest** : V3 (Chrome/Edge/Firefox)
carte se met à jour automatiquement quand elles arrivent. - **Format** : `.zip` (Chromium) + `.xpi` signé (Firefox)
- **Clic ouverture restauré** : retour à la logique v4 (fetch fiche à la volée
+ extraction checksum + construction URL avec sender adéquat). Le checksum
est pré-rempli pendant le fetch arrière-plan, donc au clic l'ouverture est
instantanée dans la plupart des cas.
## Nouveautés v4.1 ## Fonctionnalités principales
- **Fetch des fiches séquentiel (1 par 1)** au lieu de 5 workers en parallèle. ### Vue planning
Le serveur EasyVista sérialise les requêtes de toute façon, donc le parallélisme - Affichage des interventions et réservations groupées par technicien
n'apporte rien. Et surtout : quand tu changes de date pendant le fetch, l'abort - Horaires, contact, lieu, catégorie, statut visibles d'un coup d'œil
est **instantané** car il n'y a qu'une seule requête en vol au maximum. - 8 techniciens hardcodés (équipe IT canton)
- **Cache incrémental** : le cache est sauvé toutes les 5 fiches pendant le fetch, - Cache local pour réduire les requêtes serveur
pas juste à la fin. Si tu changes de date avant que tout soit fini, les statuts
déjà récupérés sont conservés.
## Nouveautés v4 ### Modes d'affichage
- **Vue classique** (depuis v1.0.0) : cards en grille, mode compact écran 24" (depuis v2026.5.30)
- **Vue horizontale** (depuis v2026.5.32) : timeline par tech, sidebar verticale (depuis v2026.5.36)
- Toggle Vue classique ↔ Vue horizontale via bouton ⊞ dans popup user-badge
- Persistance localStorage (`view_mode`)
**Chargement ~50× plus rapide.** Le nombre de requêtes au serveur EasyVista passe ### Tooltips et popups
de ~100 par chargement à **1 seule requête** pour l'affichage principal. - Tooltips au survol (hover) sur chaque intervention
- Popups épinglables (📌) pour garder ouvert (depuis v4.1.3)
- Popups timeline persistantes au clic (depuis v4.2.3)
- Drag-and-drop des popups épinglés (depuis v2026.5.19)
- Safe area : popups jamais cachés sous topbar/dock (depuis v2026.5.20)
- Position auto adaptative (4 candidats : droite/gauche/dessous/dessus)
Concrètement, en v3 un chargement initial faisait : ### Classification des absences (depuis v2026.5.27)
- 1 fetch XML planning (`calendar_block`) - **Maladie/Accident** : indigo `#4338ca`
- ~40 fetches `planning_xhr_2.php` pour les lieux/contacts - **Congé / Congés** : cyan `#06b6d4` (suffixe `s` adaptatif)
- ~40 fetches de fiches HTML pour les catégories/refs/statuts - **Pompier** : rouge `#b03030`
- jusqu'à ~40 fetches de l'API timeline - Badge + barre gauche colorée + dégradé fond
- Absence récurrente Pillonel vendredi : cyan (depuis v2026.5.30)
Total : ~120 requêtes, 10+ Mo, 8 à 15 secondes selon la charge serveur. ### User et session
- Badge user avec photo/initiales en topbar
- Badge cliquable (depuis v2026.5.26) : popup avec ⚙ Paramètres + ⊞ Vue + compteur session MM:SS
- Retry automatique en cas d'échec fetch user (60s, max 10 essais)
- Compteur de session EasyVista (tick 1s, depuis v5.0.0)
- Reconnexion automatique
En v4, on a découvert que le XML initial `calendar_block` contient **déjà** ### Admin et configuration
dans ses attributs `attr1`/`attr2`/`attr3` le contact, le lieu et la catégorie - Mode admin caché : bouton ⚙ Paramètres dans popup user-badge (depuis v2026.5.25, remplace les 5 clics secrets sur le titre)
complète de chaque intervention, et la ref dans le textContent du nœud. - Configuration persistée dans `localStorage` (`admin_config`)
Toutes ces infos qu'on allait chercher ailleurs étaient en fait dans la toute - Catégories interventions personnalisables (livraison/recup/remplacement/incident/rollout/reservation/autre)
première réponse, ignorées par le code.
Résultat : le premier rendu complet arrive en **moins d'une seconde**. Les ## Versionning — historique et conventions
fiches individuelles ne sont plus fetchées qu'en arrière-plan, uniquement
pour le statut "Clôturé/Résolu" et le commentaire technicien.
**Lazy-load au survol.** Le texte détaillé d'une intervention (Problème, À faire, L'extension a connu **3 systèmes de versionning successifs** :
Matériel, TFS ancien/nouveau poste...) n'est chargé qu'au premier survol de la
ligne, seulement pour l'intervention survolée. Imperceptible pour l'utilisateur,
énorme pour le serveur.
**Concurrence réduite.** Le pic de requêtes parallèles passe de 15 à 5 workers, | Période | Format | Exemple |
pour ménager le serveur EasyVista qui a tendance à saturer sous les rafales. |---|---|---|
| 16-17 avril 2026 | Versions de base | `1.0.0`, `2.0.0`, `3.0.0` |
| 18-20 avril 2026 | SemVer classique | `4.1.3`, `4.2.8`, `5.0.12` |
| 21 avril 2026 → maintenant | **Année + mois + patch** | `2026.5.16``2026.5.37` |
Toute l'interface utilisateur est **strictement identique** à la v3 — on n'a ### Pourquoi le passage à `YYYY.M.PATCH` ?
changé que ce qu'il y a sous le capot.
## Hérité des versions précédentes À partir de la **v2026.5.16** (21 avril 2026), l'extension est passée au versionning par année :
- Plus lisible pour les utilisateurs (l'année indique immédiatement la fraîcheur)
- Plus de débat sur ce qui constitue un "majeur" vs "mineur"
- Bump du `PATCH` à chaque livraison
- Navigation par date : ◀ ▶ et sélecteur ⚠️ **Important** : `v2026.5.16` succède chronologiquement à `v5.0.12`, malgré le numéro qui semble plus petit. Le préfixe `2026` indique l'année.
- Détection automatique des interventions closes (✓ vert, fond vert)
- Cache persistant 7 jours ## Versions notables
- Ghosts : les interventions disparues d'EasyVista restent visibles dans la vue
- Refresh auto 12h et 15h ### `v2026.5.37` (latest, 25 avril 2026) — Refonte vue horizontale
- Annulation coopérative (bouton "Arrêter") - Topbar supprimée en vue horizontale, tout passe en sidebar
- Thème clair/sombre - User-badge + titre + bouton "Aujourd'hui" + date/heure + sélecteur + flèches + stats dans sidebar
- Banderole pompier masquée (badge + barre rouge gauche conservés)
### `v2026.5.36` — Sidebar verticale
- Wrapper flex-row `#horizontal-wrapper` [sidebar 200px] + [main]
- Déplacement physique des éléments via `ELEMENTS_TO_RELOCATE`
- Restauration propre en vue classique
### `v2026.5.32` — Vue horizontale togglable
- Bouton ⊞ "Vue" dans popup user-badge
- Chaque tech = 1 ligne horizontale compacte
- localStorage `view_mode`
### `v2026.5.27` — Classification absences
- ABSENCE_LABELS : `^(cong[ée]s|maladie|pompier)$`
- Couleurs catégories
- Topbar une ligne : "Jeudi 23.04.26 • 21:55"
### `v4.2.3` — Grande popup timeline persistante
- Clic segment timeline = popup persistante
- Hover = popup qui suit la souris
### `v4.1.3` — Tooltips épinglables
- Introduction de `pinTooltip`
### `v1.0.0` (16 avril 2026) — Initiale
- Premier viewer EasyVista pour le canton
Voir [CHANGELOG.md](CHANGELOG.md) pour l'historique complet (40 versions taggées).
## Architecture technique
```
manifest.json # Manifest V3 (Chrome) + gecko_settings (Firefox)
background.js # Worker fond : fetch planning XML, gestion session, fetch fiches
viewer.html # Interface principale
viewer.js # Logique (~9000 lignes) — voir détail ci-dessous
viewer.css # Styles + thèmes clair/sombre
icons/ # icon16, icon48, icon128
```
### `viewer.js` — fonctions clés
| Fonction | Introduite | Rôle |
|---|---|---|
| `loadForDate` | v1.0.0 | Fetch + parse planning pour une date donnée |
| `buildTooltipHTML` | v1.0.0 | Construction HTML du tooltip d'intervention |
| `pinTooltip` | v4.1.3 | Épingler un tooltip (le rendre permanent) |
| `bindTimelinePopover` | v4.2.3 | Lier popover timeline aux segments |
| `showTimelinePopover` | v4.2.3 | Afficher popover persistante |
| `openPersistentTimelinePopup` | v4.2.3 | Grande popup détaillée |
| `setTooltipViewportPosition` | v4.2.4 | Détection auto fixed/abs |
| `_softUnpinPopup` | v4.3.3 | Désépinglage mou (popup reste visible) |
| `initAppClock` | v5.0.0 | Horloge HH:MM topbar |
| `initSessionTimer` | v5.0.0 | Compteur session EV (tick 1s) |
| `initAdminMenu` | v5.0.0 | Menu admin (5 clics titre) |
| `_applyViewMode` | v2026.5.32 | Toggle vue classique/horizontale |
| `_maybeRetryFetchUser` | v2026.5.34 | Relance opportuniste fetch user |
| `positionTooltipAnchored` | v2026.5.34 | Positionnement unifié (4 candidats) |
### Constantes persistantes (toutes versions)
- 8 techs hardcodés : `76272,83725,66635,92235,90070,40944,72485,86874`
- Pillonel Olivier (ID 40944) : absent tous les vendredis (hardcodé)
- Group ID EasyVista : `191`
- GUIDs forms EV :
- Demande : `S={C99ECD05-3D48-4C62-ABF0-66292053AED6}`
- Incident : `I={07ED9C68-6172-48EA-8A58-90912B0A283E}`
- SSO : Canton ForgeRock OpenAM
- Storage keys : `admin_config`, `view_mode` (depuis v2026.5.32)
- Domaines : `itsma.etat-de-vaud.ch` (interne), `itsma.vd.ch` (externe SSO)
## Installation ## Installation
1. Décompresser le zip ### Firefox
2. Ouvrir Chrome, `chrome://extensions/` Télécharger le `.xpi` signé depuis le serveur de mises à jour interne, ou drag-and-drop dans `about:addons`.
3. Activer **Mode développeur** (en haut à droite)
4. **Charger l'extension non empaquetée** → sélectionner le dossier `planning-extension-v4`
Si tu avais déjà la v3 installée, tu peux la supprimer avant — les caches des ### Chrome / Edge
deux versions sont compatibles (même format). Mode développeur : décompresser le ZIP et charger en tant qu'extension non empaquetée.
## Utilisation ## Développement
1. Se connecter à EasyVista dans un onglet (`itsma.etat-de-vaud.ch` ou `itsma.vd.ch`) ```bash
2. Cliquer sur l'icône de l'extension (depuis n'importe quel onglet) git clone https://gitea.netaplaid.ch/FroSteel/Planification.git
3. La vue claire s'ouvre dans un nouvel onglet cd Planification
## Comment ça marche techniquement # Pour packager une nouvelle version :
# 1. modifier le code
# 2. bump version dans manifest.json
# 3. zip + xpi
git add -A
git commit -m "Version YYYY.M.PATCH — description"
git tag vYYYY.M.PATCH
git push origin main
git push --tags
```
- `background.js` fait les fetches en arrière-plan (via le cookie de session EasyVista). ## Licence
- L'extension détecte automatiquement le `PHPSESSID` depuis un onglet EasyVista ouvert.
- **v4 : le XML `planning_xhr.php?div=calendar_block` suffit à afficher tout
l'essentiel.** Les champs `attr1`/`attr2`/`attr3` contiennent contact, lieu
et catégorie. Le `textContent` du nœud contient la ref (S260.../I260...).
- Les fiches individuelles (`index.php?formEvent=...`) ne sont fetchées que pour
obtenir le statut Clôturé/Résolu et le commentaire technicien.
- Le texte d'action détaillé (Problème/À faire/Matériel/...) est récupéré en
lazy-load via `planning_xhr_2.php?id=ACTIONID` au premier survol.
- Le cache est stocké dans `chrome.storage.local` (local à ta machine).
- Aucune donnée n'est envoyée ailleurs que vers `itsma.etat-de-vaud.ch` et `itsma.vd.ch`.
## Limitations connues [MIT License](LICENSE) — Copyright (c) 2026 Quentin Rouiller
- Nécessite un onglet EasyVista ouvert (même en arrière-plan) pour fonctionner ## Auteur
- Fonctionne uniquement sur l'intranet cantonal (les fetches échoueront en externe)
- Les 8 IDs des techs sont en dur dans le code (si quelqu'un quitte/arrive dans **Quentin Rouiller** (QRO)
l'équipe, il faut mettre à jour `viewer.js` ligne ~22) Canton de Vaud — Service IT
- Le statut "Clôturé/Résolu" met quelques secondes à apparaître après le
chargement initial (fetch des fiches en arrière-plan, concurrence 5)
+154 -55
View File
@@ -157,6 +157,16 @@ async function fetchXhr2(origin, phpsessid, actionId) {
async function fetchFicheHtml(origin, phpsessid, formLink) { async function fetchFicheHtml(origin, phpsessid, formLink) {
const url = `${origin}/index.php?${formLink}&PHPSESSID=${encodeURIComponent(phpsessid)}`; const url = `${origin}/index.php?${formLink}&PHPSESSID=${encodeURIComponent(phpsessid)}`;
console.log("[bg] fetchFicheHtml →", url.substring(0, 120)); console.log("[bg] fetchFicheHtml →", url.substring(0, 120));
// v2026.5.16 : juste après une reconnexion SSO, EasyVista retourne parfois
// une page intermédiaire tronquée (~8 Ko au lieu de ~250 Ko), le temps que
// les cookies SSO/Kerberos se propagent. On fait jusqu'à 3 tentatives avec
// 1.5s entre chaque si on détecte une taille suspecte.
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1500;
const MIN_VALID_SIZE = 20000; // < 20 Ko = probablement page intermédiaire
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
const r = await evFetch(url, origin); const r = await evFetch(url, origin);
if (!r.ok) { if (!r.ok) {
const err = new Error("HTTP " + r.status); const err = new Error("HTTP " + r.status);
@@ -165,13 +175,28 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
throw err; throw err;
} }
const html = await r.text(); const html = await r.text();
console.log("[bg] fiche status =", r.status, "| taille =", html.length); console.log(`[bg] fiche status = ${r.status} | taille = ${html.length}${attempt > 1 ? ` (tentative ${attempt}/${MAX_RETRIES})` : ""}`);
// Si réponse clairement une redirection courte → login expiré, inutile de retry
if (html.length < 500) { if (html.length < 500) {
console.warn("[bg] ⚠ fiche très courte, contenu =", JSON.stringify(html)); console.warn("[bg] ⚠ fiche très courte, contenu =", JSON.stringify(html));
}
return html; return html;
} }
// Si taille suspecte (< 20 Ko), probable page intermédiaire SSO : retry
if (html.length < MIN_VALID_SIZE && attempt < MAX_RETRIES) {
console.warn(`[bg] ⚠ fiche anormalement petite (${html.length} octets), retry dans ${RETRY_DELAY_MS} ms...`);
await new Promise(res => setTimeout(res, RETRY_DELAY_MS));
continue;
}
// Sinon : on retourne ce qu'on a
return html;
}
// Ne devrait pas arriver (la boucle fait return avant)
throw new Error("fetchFicheHtml: max retries reached");
}
// v4.1.7 : API timeline EasyVista. Renvoie le JSON des actions d'une fiche, // v4.1.7 : API timeline EasyVista. Renvoie le JSON des actions d'une fiche,
// avec pour chaque action : intervenant, ACTION_ID, AM_DONE_BY_ID, description // avec pour chaque action : intervenant, ACTION_ID, AM_DONE_BY_ID, description
// complète (bien plus riche que le xhr2 tronqué). // complète (bien plus riche que le xhr2 tronqué).
@@ -298,8 +323,14 @@ async function extendSessionKeepAlive(origin, phpsessid) {
/** /**
* Détecte si on est sur le réseau interne (itsma.etat-de-vaud.ch accessible) * Détecte si on est sur le réseau interne (itsma.etat-de-vaud.ch accessible)
* ou externe (seul itsma.vd.ch accessible). Fait un HEAD test avec timeout * ou externe (seul itsma.vd.ch accessible). Fait un fetch avec mode "no-cors"
* court sur l'URL interne : si ça répond, on est interne ; sinon externe. * sur l'URL interne : si ça répond (même redirection SSO), on est interne.
* Si ça échoue (DNS unreachable, timeout), on est externe.
*
* v5.0.11 (fix) : mode "no-cors" pour éviter que l'erreur CORS de la
* redirection SSO (itsma.etat-de-vaud.ch → portail.etat-de-vaud.ch/sso/)
* soit interprétée comme un échec de connectivité. Au bureau, la redirection
* SSO passe → le fetch termine → on sait qu'on est interne.
* *
* Le résultat est mis en cache dans chrome.storage.local pendant 1h pour * Le résultat est mis en cache dans chrome.storage.local pendant 1h pour
* éviter de refaire le test à chaque démarrage. * éviter de refaire le test à chaque démarrage.
@@ -308,7 +339,7 @@ async function extendSessionKeepAlive(origin, phpsessid) {
* @returns {Promise<"internal"|"external">} * @returns {Promise<"internal"|"external">}
*/ */
async function detectNetworkContext(force = false) { async function detectNetworkContext(force = false) {
const CACHE_KEY = "network_context"; const CACHE_KEY = "network_context_v2"; // v5.0.12 : nouvelle clé pour invalider le cache fautif v5.0.11
const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1h const CACHE_MAX_AGE_MS = 60 * 60 * 1000; // 1h
if (!force) { if (!force) {
@@ -322,25 +353,28 @@ async function detectNetworkContext(force = false) {
} catch (e) {} } catch (e) {}
} }
// Test HEAD sur l'URL interne avec timeout 2.5 sec
const controller = new AbortController(); const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 2500); const timer = setTimeout(() => controller.abort(), 2500);
let context = "external"; let context = "external";
try { try {
console.log("[bg] detectNetworkContext : test de itsma.etat-de-vaud.ch..."); console.log("[bg] detectNetworkContext : test de itsma.etat-de-vaud.ch (mode no-cors)...");
const r = await fetch("https://itsma.etat-de-vaud.ch/", { // v5.0.11 (fix) : no-cors évite l'erreur CORS de la redirection SSO.
method: "HEAD", // Si le fetch termine sans throw, le serveur est joignable = interne.
// Si timeout ou DNS fail, throw → externe.
await fetch("https://itsma.etat-de-vaud.ch/", {
method: "GET",
mode: "no-cors",
signal: controller.signal, signal: controller.signal,
credentials: "omit" // pas besoin des cookies pour ce test credentials: "omit",
cache: "no-store"
}); });
clearTimeout(timer); clearTimeout(timer);
// Tout statut HTTP (même 302, 404, 403) indique que le serveur est joignable console.log("[bg] detectNetworkContext : interne accessible");
console.log("[bg] detectNetworkContext : interne accessible (status=" + r.status + ")");
context = "internal"; context = "internal";
} catch (err) { } catch (err) {
clearTimeout(timer); clearTimeout(timer);
// Timeout, DNS unreachable, erreur réseau = domaine interne inaccessible // AbortError (timeout) ou TypeError (DNS unreachable / réseau coupé)
console.log("[bg] detectNetworkContext : interne inaccessible, on est en externe (" + err.name + ")"); console.log("[bg] detectNetworkContext : interne inaccessible externe (" + err.name + ": " + err.message + ")");
context = "external"; context = "external";
} }
@@ -366,6 +400,67 @@ function originForContext(context) {
: "https://itsma.vd.ch"; : "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é // v4.2 : récupération de l'utilisateur connecté
// ============================================================================ // ============================================================================
@@ -635,33 +730,18 @@ async function submitDouchette(origin, phpsessid, opts) {
async function deletePlanningItem(origin, phpsessid, actionId, kind) { async function deletePlanningItem(origin, phpsessid, actionId, kind) {
if (!actionId) throw new Error("actionId manquant"); if (!actionId) throw new Error("actionId manquant");
// v5.0.1 : plusieurs function_name à tester dans l'ordre (du plus probable // v5.0.14 : confirmé par capture Network réelle — EasyVista utilise
// au moins probable). Le premier qui renvoie 200 ET non-login est considéré OK. // "Planning_delete_absence" pour TOUS les types d'entrée planning (absences,
const fnNames = kind === "reservation" // réservations, événements, etc.). Réponse XML : <Planning_delete_absence>true</...>
? [ // On met donc ce nom en PREMIER pour tout, et on garde les autres en fallback.
const fnNames = [
"Planning_delete_absence", // ← le seul qui marche vraiment côté EV
// Fallbacks historiques (au cas où EV change un jour) :
"Planning_delete_reservation", "Planning_delete_reservation",
"delete_reservation",
"fc_delete_reservation",
"delete_act_reservation",
"delete_planning_reservation",
"remove_reservation",
// v5.0.2 : réservations sont parfois traitées comme absences côté API
"Planning_delete_absence",
"delete_absence", "delete_absence",
"fc_delete_absence" "delete_reservation",
] "fc_delete_absence",
: [ "fc_delete_reservation"
// v5.0.2 : élargir la liste, on a essayé 3 sans succès. Les variantes
// plausibles vues dans les API EasyVista :
"Planning_delete_absence", // le plus "officiel"
"delete_absence", // le nom JS dans le onclick
"fc_delete_absence", // pattern fc_*
"delete_act_absence", // parfois "act_" dans les noms
"Planning_delete_holiday", // en anglais
"delete_holiday",
"fc_delete_holiday",
"delete_planning_absence", // variation complète
"remove_absence"
]; ];
let lastErr = null; let lastErr = null;
@@ -675,7 +755,11 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
console.log(`[bg] deletePlanningItem → tente function_name=${fn}`, url.substring(0, 180)); console.log(`[bg] deletePlanningItem → tente function_name=${fn}`, url.substring(0, 180));
try { try {
const r = await fetch(url, { method: "GET", credentials: "include" }); // v5.0.13 : utiliser evFetch() au lieu de fetch() brut pour que les
// headers Referer + X-Requested-With soient envoyés — sinon EV renvoie
// un <script> de redirection CSRF qui ne ressemble pas à une erreur et
// notre heuristique le prenait à tort pour un succès.
const r = await evFetch(url, origin, { method: "GET" });
const body = await r.text(); const body = await r.text();
console.log(`[bg] status=${r.status} | body (200 premiers chars) : ${(body || "").substring(0, 200)}`); console.log(`[bg] status=${r.status} | body (200 premiers chars) : ${(body || "").substring(0, 200)}`);
@@ -690,24 +774,34 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
throw new Error("session_expired"); throw new Error("session_expired");
} }
// v5.0.1 : heuristique pour détecter si la suppression a marché. // v5.0.14 : détection explicite du succès XML observé dans les captures
// EasyVista renvoie typiquement : // réseau : <Planning_delete_absence>true</Planning_delete_absence>
// - une chaine vide ou "ok" ou "1" si succès const trimmed = (body || "").trim();
// - un message d'erreur / html d'erreur si function_name inconnu const lower = trimmed.toLowerCase();
// 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. // Succès explicite : réponse XML du type <X>true</X>
const trimmed = (body || "").trim().toLowerCase(); if (/^<\w+>true<\/\w+>\s*$/i.test(trimmed)) {
const looksLikeError = trimmed.includes("error") console.log(`[bg] → SUCCÈS confirmé par XML <...>true</...> avec function_name=${fn}`);
|| trimmed.includes("erreur") return { status: r.status, functionName: fn, body: trimmed };
|| trimmed.includes("unknown function")
|| trimmed.includes("fonction inconnue")
|| trimmed.includes("<html");
if (!looksLikeError) {
console.log(`[bg] → suppression OK avec function_name=${fn}`);
return { status: r.status, functionName: fn, body: body.substring(0, 200) };
} }
// Détection d'échec : <X>false</X>, erreurs, html, redirect, etc.
const looksLikeError = /^<\w+>false<\/\w+>\s*$/i.test(trimmed)
|| lower.includes("error")
|| lower.includes("erreur")
|| lower.includes("unknown function")
|| lower.includes("fonction inconnue")
|| lower.includes("<html")
|| lower.includes("window.location.href");
if (looksLikeError) {
console.log(`[bg] → réponse ressemble à une erreur, on tente le prochain nom`); console.log(`[bg] → réponse ressemble à une erreur, on tente le prochain nom`);
lastBody = body; lastBody = body;
continue;
}
// Pas d'erreur évidente mais pas de succès explicite non plus
// (ex: réponse vide ou "1" ou "ok"). On considère comme succès.
console.log(`[bg] → suppression probablement OK (body neutre) avec function_name=${fn}`);
return { status: r.status, functionName: fn, body: trimmed.substring(0, 200) };
} catch (err) { } catch (err) {
if (err.message === "session_expired") throw err; if (err.message === "session_expired") throw err;
console.warn(`[bg] erreur avec ${fn}:`, err); console.warn(`[bg] erreur avec ${fn}:`, err);
@@ -1090,6 +1184,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
url: `${origin}/`, // racine → EV redirige vers SSO si besoin url: `${origin}/`, // racine → EV redirige vers SSO si besoin
active: true active: true
}); });
// v2026.5.16 : surveiller cet onglet — si on tombe sur la page de
// login manuel portail.etat-de-vaud.ch/iamlogin/, rediriger vers
// portail.etat-de-vaud.ch/iam/accueil/ qui déclenche le Windows
// SSO Kerberos automatiquement.
watchReconnectTabForIamLogin(tab.id);
sendResponse({ ok: true, tabId: tab.id, origin }); sendResponse({ ok: true, tabId: tab.id, origin });
} catch (err) { } catch (err) {
sendResponse({ ok: false, error: err.message || String(err) }); sendResponse({ ok: false, error: err.message || String(err) });
+27 -4
View File
@@ -1,9 +1,26 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Planification", "name": "Planification",
"version": "5.0.11", "version": "2026.5.23",
"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.", "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"], "browser_specific_settings": {
"gecko": {
"id": "planification@vd.ch",
"strict_min_version": "140.0",
"data_collection_permissions": {
"required": [
"none"
]
}
}
},
"permissions": [
"activeTab",
"scripting",
"storage",
"tabs",
"alarms"
],
"host_permissions": [ "host_permissions": [
"https://itsma.etat-de-vaud.ch/*", "https://itsma.etat-de-vaud.ch/*",
"https://itsma.vd.ch/*" "https://itsma.vd.ch/*"
@@ -12,7 +29,9 @@
"default_title": "Ouvrir la Planification" "default_title": "Ouvrir la Planification"
}, },
"background": { "background": {
"service_worker": "background.js" "scripts": [
"background.js"
]
}, },
"icons": { "icons": {
"16": "icons/icon16.png", "16": "icons/icon16.png",
@@ -21,7 +40,11 @@
}, },
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["viewer.html", "viewer.js", "viewer.css"], "resources": [
"viewer.html",
"viewer.js",
"viewer.css"
],
"matches": [ "matches": [
"https://itsma.etat-de-vaud.ch/*", "https://itsma.etat-de-vaud.ch/*",
"https://itsma.vd.ch/*" "https://itsma.vd.ch/*"
+533 -13
View File
@@ -9,8 +9,8 @@
--border: #e2e4e8; --border: #e2e4e8;
--border-strong: #cfd3da; --border-strong: #cfd3da;
--text: #1a1f2b; --text: #1a1f2b;
--text-muted: #5b6573; --text-muted: #2e3642; /* v2026.5.29 : +contraste (était #4a5260) */
--text-faint: #8892a0; --text-faint: #50596a; /* v2026.5.29 : +contraste (était #6c7583) */
--accent: #0f4f8b; --accent: #0f4f8b;
--accent-soft: #e1ecf7; --accent-soft: #e1ecf7;
--danger: #b03030; --danger: #b03030;
@@ -59,8 +59,8 @@
--border: #2e333c; --border: #2e333c;
--border-strong: #414754; --border-strong: #414754;
--text: #e6e8ec; --text: #e6e8ec;
--text-muted: #9ba2ad; --text-muted: #d0d5de; /* v2026.5.29 : +contraste (était #b8c0cc) — quasi blanc */
--text-faint: #6a727e; --text-faint: #a8b0bc; /* v2026.5.29 : +contraste (était #8b93a0) */
--accent: #5ea8e8; --accent: #5ea8e8;
--accent-soft: #223348; --accent-soft: #223348;
--danger: #e87878; --danger: #e87878;
@@ -320,8 +320,57 @@ html, body {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
flex-wrap: nowrap;
} }
/* 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;
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 { .btn-nav {
padding: 6px 10px; padding: 6px 10px;
font-size: 13px; font-size: 13px;
@@ -689,12 +738,9 @@ html, body {
.timeline-slot.status-resolved { background: var(--c-resolved); } .timeline-slot.status-resolved { background: var(--c-resolved); }
.timeline-slot.kind-absence { .timeline-slot.kind-absence {
background: repeating-linear-gradient( /* v5.0.15 : uni gris-noir au lieu de rayé, plus lisible */
45deg, background: #2a2f36;
var(--text-faint) 0 6px, border-right: 1px solid var(--bg-elevated);
var(--bg-muted) 6px 12px
);
opacity: 0.6;
} }
.timeline-slot:hover, .timeline-slot:hover,
@@ -969,6 +1015,12 @@ html, body {
opacity: 0; opacity: 0;
transition: opacity 0.1s, background 0.1s, color 0.1s; transition: opacity 0.1s, background 0.1s, color 0.1s;
font-family: inherit; 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-v2:hover .intervention-copy { opacity: 1; }
.intervention-copy:hover { .intervention-copy:hover {
@@ -1117,6 +1169,26 @@ html, body {
color: var(--c-reservation); color: var(--c-reservation);
font-family: var(--font); font-family: var(--font);
letter-spacing: 0.02em; letter-spacing: 0.02em;
/* v5.0.15 : étendre le titre sur toute la largeur de la carte pour le
vrai centrage (sinon il n'est centré que dans sa colonne grid) */
grid-column: 1 / -1;
text-align: center;
padding-left: 62px; /* compense la colonne time (58px + gap) */
padding-right: 0;
}
/* v5.0.15 : absence partielle (demi-journée) affichée comme une row */
.iv-ref-header.is-absence-title {
color: var(--c-absence, #a0a8b2);
font-family: var(--font);
letter-spacing: 0.02em;
grid-column: 1 / -1;
text-align: center;
padding-left: 62px;
padding-right: 0;
}
.intervention-v2.color-absence .intervention-dot {
background: var(--c-absence, #2a2f36);
} }
.iv-reservation-par { .iv-reservation-par {
font-size: 13px; font-size: 13px;
@@ -1920,18 +1992,36 @@ body.modal-open {
/* ───────────────────────────────────────────────────────────────────────── /* ─────────────────────────────────────────────────────────────────────────
v5.0.0 : horloge au milieu de la topbar (HH:MM, pas de secondes) v5.0.0 : horloge au milieu de la topbar (HH:MM, pas de secondes)
───────────────────────────────────────────────────────────────────────── */ ───────────────────────────────────────────────────────────────────────── */
/* v2026.5.16 : app-clock contient maintenant 2 lignes empilées :
- app-clock-date : "Mardi 21 avril 2026" (petit)
- app-clock-time : "12:34" (grand) */
.app-clock { .app-clock {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -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-size: 22px;
font-weight: 600; font-weight: 600;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: var(--text);
letter-spacing: 1px; letter-spacing: 1px;
pointer-events: none;
user-select: none;
} }
.topbar { position: sticky; /* déja défini plus haut */ } .topbar { position: sticky; /* déja défini plus haut */ }
/* topbar doit être en position: relative parent pour que .app-clock absolute /* topbar doit être en position: relative parent pour que .app-clock absolute
@@ -2390,3 +2480,433 @@ header.topbar::before {
.banner-reconnect-failed .banner-btn-primary:hover { .banner-reconnect-failed .banner-btn-primary:hover {
background: #f8d7da; background: #f8d7da;
} }
/* ==========================================================================
v2026.5.16 : responsive topbar
========================================================================== */
/* Breakpoint medium : entre 1000 et 1300px, on compacte un peu */
@media (max-width: 1300px) {
.app-clock-date { font-size: 11px; }
.app-clock-time { font-size: 20px; }
.topbar-right .btn-action .btn-action-label,
.topbar-right .btn-refresh .btn-refresh-label {
font-size: 12px;
}
}
/* Breakpoint small : moins de 1000px, on masque les labels de boutons action
et on réduit encore l'horloge. Les icônes restent, titres restent. */
@media (max-width: 1000px) {
.topbar { padding: 8px 14px; gap: 8px; }
.topbar h1 { font-size: 16px; }
.app-clock { font-size: smaller; }
.app-clock-date { font-size: 10px; }
.app-clock-time { font-size: 18px; }
.btn-action .btn-action-label,
.btn-refresh .btn-refresh-label {
display: none;
}
.btn-action, .btn-refresh {
padding: 6px 10px;
}
.capture-info { display: none; }
}
/* Breakpoint très petit : moins de 720px, on cache la date complète (garde
juste l'heure) et on autorise le wrap total */
@media (max-width: 720px) {
.topbar {
flex-wrap: wrap;
padding: 6px 10px;
}
.app-clock {
position: static;
transform: none;
margin: 0 auto;
}
.app-clock-date { display: none; }
.topbar-left { flex-wrap: wrap; }
.date-nav { margin-top: 4px; }
.date-picker-day { min-width: 46px; font-size: 12px; }
.topbar-right { flex-wrap: wrap; justify-content: flex-end; }
}
/* Breakpoint minuscule : masque aussi les labels de refresh, boutons deviennent
vraiment iconifiés */
@media (max-width: 520px) {
.app-clock-time { font-size: 16px; }
.topbar h1 { font-size: 14px; }
.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;
}
/* v2026.5.18 : masquer le conteneur d'actions d'origine (↻ reload + 📌 pin)
dans les popups épinglés — leur place est reprise par notre .pinned-popup-topbar */
.pinned-popup .tooltip-actions {
display: none !important;
}
.pinned-popup-topbar {
position: absolute;
top: 4px;
right: 4px;
display: flex;
gap: 2px;
align-items: center;
z-index: 10;
}
.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)
v2026.5.19 : refonte — élément .pinned-popup-minref créé à la volée
v2026.5.21 : agrandi pour que la ref tienne sans déborder
v2026.5.22 : encore agrandi + plus d'espace entre dragbar et topbar
========================================================================== */
.pinned-popup.pinned-popup-minimized {
min-width: 300px !important;
max-width: 360px !important;
width: 300px !important;
height: auto !important;
min-height: 80px !important;
padding: 44px 16px 16px 16px !important;
overflow: visible;
background: var(--bg-elevated) !important;
border: 1px solid var(--border) !important;
}
/* Séparer visuellement la dragbar (collée en haut) des boutons topbar */
.pinned-popup.pinned-popup-minimized .pinned-popup-topbar {
top: 14px !important; /* sous la dragbar (qui fait ~6-8px) */
}
/* 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;
}
/* L'élément ref dédié, centré et gros */
.pinned-popup-minref {
display: block;
text-align: center;
padding: 10px 12px;
font-family: var(--mono, monospace);
font-size: 15px;
font-weight: 700;
color: var(--text);
cursor: pointer;
user-select: none;
border-radius: 4px;
transition: background 0.12s;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pinned-popup-minref:hover {
background: var(--bg-muted);
}
/* ==========================================================================
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);
align-items: center;
}
.pinned-popups-dock.visible {
display: flex;
}
.pinned-popup-dock-pill {
display: inline-flex;
align-items: center;
padding: 6px 14px;
background: var(--bg-muted);
color: var(--text);
border: 1px solid var(--border);
border-radius: 16px;
font-family: var(--mono, monospace);
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, transform 0.15s, filter 0.15s;
white-space: nowrap;
}
.pinned-popup-dock-pill:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
/* v2026.5.18 : couleurs par catégorie (fond = couleur, texte blanc) */
.pinned-popup-dock-pill.color-livraison { background: var(--c-livraison); color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-installation { background: var(--c-installation); color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-recup { background: var(--c-recup); color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-remplacement { background: var(--c-remplacement); color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-incident { background: var(--c-incident); color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-rollout { background: var(--c-rollout); color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-reservation { background: var(--c-reservation); color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-absence { background: #2a2f36; color: white; border-color: transparent; }
.pinned-popup-dock-pill.color-autre { background: var(--c-autre); color: white; border-color: transparent; }
/* v2026.5.18 : bouton "Fermer tous" à droite du dock */
.pinned-popups-close-all {
margin-left: auto;
padding: 6px 12px;
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.pinned-popups-close-all:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border-color: #ef4444;
}
/* ==========================================================================
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);
}
/* ==========================================================================
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 svg {
width: 14px;
height: 14px;
}
.pinned-popup-refresh.spinning svg {
animation: pinned-popup-refresh-spin 0.6s linear infinite;
transform-origin: 50% 50%;
}
.pinned-popup-refresh.spinning {
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);
}
/* ==========================================================================
v2026.5.20 : mini-menu au survol d'une pastille dock
========================================================================== */
.pill-hover-menu {
position: fixed;
z-index: 60;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 130px;
animation: pill-hover-menu-appear 0.12s ease-out;
}
@keyframes pill-hover-menu-appear {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.pill-hover-menu-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: transparent;
color: var(--text);
border: none;
border-radius: 4px;
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-align: left;
transition: background 0.12s;
}
.pill-hover-menu-btn:hover {
background: var(--bg-muted);
}
.pill-hover-menu-btn.pill-hover-menu-close:hover {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.pill-menu-ico {
font-size: 14px;
width: 16px;
text-align: center;
}
/* ==========================================================================
v2026.5.21 : icône 📍 "active" dans le tooltip hover = déjà épinglée
========================================================================== */
.tooltip-pinbtn.tooltip-pinbtn-active {
opacity: 1 !important;
filter: none !important;
background: rgba(239, 68, 68, 0.15);
border-radius: 4px;
}
+13 -3
View File
@@ -16,15 +16,25 @@
<h1 id="app-title">Planification</h1> <h1 id="app-title">Planification</h1>
<div class="date-nav"> <div class="date-nav">
<button id="nav-prev" class="btn btn-nav" title="Jour précédent" aria-label="Jour précédent"></button> <button id="nav-prev" class="btn btn-nav" title="Jour précédent" aria-label="Jour précédent"></button>
<input type="date" id="date-picker" class="date-input"> <!-- v2026.5.17 : input date custom qui affiche "Vendredi 24.04.2026" -->
<div class="date-custom-wrapper">
<div id="date-custom" class="date-custom" role="button" tabindex="0" title="Choisir une date">
<span id="date-custom-label"></span>
<span class="date-custom-icon">📅</span>
</div>
<input type="date" id="date-picker" class="date-input-hidden">
</div>
<button id="nav-next" class="btn btn-nav" title="Jour suivant" aria-label="Jour suivant"></button> <button id="nav-next" class="btn btn-nav" title="Jour suivant" aria-label="Jour suivant"></button>
<button id="nav-today" class="btn btn-today" title="Aujourd'hui">Auj.</button> <button id="nav-today" class="btn btn-today" title="Aujourd'hui">Auj.</button>
</div> </div>
<span id="capture-info" class="capture-info"></span> <span id="capture-info" class="capture-info"></span>
<span id="refresh-check" class="refresh-check hidden" title="Mise à jour terminée"></span> <span id="refresh-check" class="refresh-check hidden" title="Mise à jour terminée"></span>
</div> </div>
<!-- v5.0.0 : horloge au milieu, format HH:MM, mise à jour toutes les min --> <!-- v2026.5.16 : date complète du jour au-dessus de l'heure dans la topbar -->
<div id="app-clock" class="app-clock" title="Heure actuelle"></div> <div id="app-clock" class="app-clock" title="Date et heure actuelles">
<div id="app-clock-date" class="app-clock-date"></div>
<div id="app-clock-time" class="app-clock-time"></div>
</div>
<!-- v5.0.9 : compteur de session EasyVista (visible < 5 min restantes) --> <!-- v5.0.9 : compteur de session EasyVista (visible < 5 min restantes) -->
<div id="app-session" class="app-session hidden"></div> <div id="app-session" class="app-session hidden"></div>
<div class="topbar-right"> <div class="topbar-right">
+1327 -86
View File
File diff suppressed because it is too large Load Diff