Public Access
Compare commits
4
Commits
v2026.5.49
...
v2026.5.50
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a37b90aa13 | ||
|
|
a1c7f45b03 | ||
|
|
cd106484d5 | ||
|
|
e846e90bb8 |
+27
-5
@@ -1,7 +1,7 @@
|
|||||||
# CHANGELOG — Extension Planification EasyVista Canton de Vaud
|
# CHANGELOG — Extension Planification EasyVista Canton de Vaud
|
||||||
|
|
||||||
> Ce changelog documente l'évolution de l'extension Chrome/Firefox "Planification"
|
> Ce changelog documente l'évolution de l'extension Chrome/Firefox "Planification"
|
||||||
> développée par FroSteel pour les coordinateurs DGNSI (Canton de Vaud).
|
> développée par Quentin pour les coordinateurs DGNSI (Canton de Vaud).
|
||||||
>
|
>
|
||||||
> Les versions documentées ci-dessous sont celles dont les détails sont connus.
|
> Les versions documentées ci-dessous sont celles dont les détails sont connus.
|
||||||
> Pour les versions plus anciennes, l'analyse du code source permet de
|
> Pour les versions plus anciennes, l'analyse du code source permet de
|
||||||
@@ -9,6 +9,28 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v2026.5.50 — Texte des actions rétabli (format EasyVista du 29.09.2026), catégories Roll Out et Déménagement
|
||||||
|
|
||||||
|
> Correctif urgent : depuis le 29 septembre 2026, le texte complet des actions ne s'affichait plus. EasyVista renvoie désormais les actions avec une cellule de moins, et l'extension les lisait par position. Elle les retrouve maintenant par leur contenu. Cette version ajoute aussi deux catégories de réservation.
|
||||||
|
|
||||||
|
### Texte des actions
|
||||||
|
|
||||||
|
- **Texte complet des actions rétabli** dans les bulles et les fiches, avec l'ancien comme avec le nouveau format d'EasyVista.
|
||||||
|
- **Plus robuste aux prochains changements** : la description est repérée par le nom de sa colonne, et l'identifiant de l'action par son contenu, plus par leur position.
|
||||||
|
- **Changement de format signalé** : si EasyVista modifie encore la structure au point qu'aucune action ne soit lisible, un avertissement apparaît dans la console (une seule fois, sans aucune donnée personnelle). Les fiches ou historiques qui ne se chargent pas sont aussi signalés dans la console.
|
||||||
|
|
||||||
|
### Réservations
|
||||||
|
|
||||||
|
- **Deux nouvelles catégories : Roll Out et Déménagement.** Elles sont propres à la Planification : dans EasyVista, la réservation reste « Evènements spéciaux ». Elles sont partagées entre tous les postes par le dossier partagé, comme le n°, la ville et la rue.
|
||||||
|
- **Catégorie proposée à la saisie du n°** : le n° d'une intervention Roll Out propose Roll Out.
|
||||||
|
- **Catégorie inconnue conservée** : une catégorie posée par une version plus récente n'est plus effacée quand on modifie la réservation.
|
||||||
|
|
||||||
|
### Déploiement
|
||||||
|
|
||||||
|
- **Mettre à jour tous les postes le même jour.** Un poste resté en 2026.5.49 qui modifie une réservation Roll Out ou Déménagement effacerait cette catégorie sur tous les postes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2026.5.49 — Sécurité hors session, onglet Email clarifié, lecture au défilement, affichage et synchronisation fiabilisés
|
## v2026.5.49 — Sécurité hors session, onglet Email clarifié, lecture au défilement, affichage et synchronisation fiabilisés
|
||||||
|
|
||||||
> Cette version renforce la **sécurité** quand aucune session EasyVista n'est active, clarifie l'**onglet Email** (déplacement et annulation bien séparés, aperçu de la copie), améliore la **lecture du planning au défilement** (le nom du technicien, son compteur et la frise horaire restent visibles), et corrige en profondeur l'**affichage de la reconnexion** ainsi que plusieurs comportements de la synchronisation et des réservations.
|
> Cette version renforce la **sécurité** quand aucune session EasyVista n'est active, clarifie l'**onglet Email** (déplacement et annulation bien séparés, aperçu de la copie), améliore la **lecture du planning au défilement** (le nom du technicien, son compteur et la frise horaire restent visibles), et corrige en profondeur l'**affichage de la reconnexion** ainsi que plusieurs comportements de la synchronisation et des réservations.
|
||||||
@@ -122,7 +144,7 @@
|
|||||||
- **Règle des heures fixe** en haut (reste collée au défilement), **lignes verticales continues** sur toute la hauteur du tableau : trait net à chaque heure, pointillé discret aux demi-heures, césure rayée à midi ; les traits passent derrière les bulles et sautent les techniciens absents.
|
- **Règle des heures fixe** en haut (reste collée au défilement), **lignes verticales continues** sur toute la hauteur du tableau : trait net à chaque heure, pointillé discret aux demi-heures, césure rayée à midi ; les traits passent derrière les bulles et sautent les techniciens absents.
|
||||||
- **Ligne rouge « maintenant »** continue sur toute la hauteur (jour courant), reposée instantanément à chaque rendu ; elle saute aussi les absents.
|
- **Ligne rouge « maintenant »** continue sur toute la hauteur (jour courant), reposée instantanément à chaque rendu ; elle saute aussi les absents.
|
||||||
- **Chevauchements** : les éléments qui se recouvrent s'empilent en sous-lignes de même hauteur — la zone du technicien s'agrandit seulement dans ce cas ; tolérance de 10 minutes pour ne pas empiler de simples enchaînements.
|
- **Chevauchements** : les éléments qui se recouvrent s'empilent en sous-lignes de même hauteur — la zone du technicien s'agrandit seulement dans ce cas ; tolérance de 10 minutes pour ne pas empiler de simples enchaînements.
|
||||||
- **Cartes** : fond neutre, le bord gauche porte la couleur de catégorie et sert de **poignée** (cliquer = fenêtre de modification, glisser = déplacer ; présente sur toutes les cartes, déplacement bloqué pour les faites/clôturées, réservations et absences). Numéro d'intervention en tête, ville + adresse groupées ; cartes étroites : texte compact (ville + fin d'adresse, « Rue de l'Exemple 5 » → « Université 5 »), jamais vides tant qu'il reste de la place.
|
- **Cartes** : fond neutre, le bord gauche porte la couleur de catégorie et sert de **poignée** (cliquer = fenêtre de modification, glisser = déplacer ; présente sur toutes les cartes, déplacement bloqué pour les faites/clôturées, réservations et absences). Numéro d'intervention en tête, ville + adresse groupées ; cartes étroites : texte compact (ville + fin d'adresse, « Rue de l'Exemple 5 » → « Exemple 5 »), jamais vides tant qu'il reste de la place.
|
||||||
- **Réservations** : affichent sous l'intitulé le lieu renseigné à la création, sinon le nom de la personne qui a réservé.
|
- **Réservations** : affichent sous l'intitulé le lieu renseigné à la création, sinon le nom de la personne qui a réservé.
|
||||||
- **Zones libres** : survol = « Libre HH:MM–HH:MM » (y compris sur les journées totalement vides), clic droit = Réservation/Absence à l'heure du curseur, dépôt du drag précis (pas de 30 min) avec marqueur vert sur toute la bande.
|
- **Zones libres** : survol = « Libre HH:MM–HH:MM » (y compris sur les journées totalement vides), clic droit = Réservation/Absence à l'heure du curseur, dépôt du drag précis (pas de 30 min) avec marqueur vert sur toute la bande.
|
||||||
- Alignement règle/cartes mesuré au pixel réel (indépendant du zoom et de la taille de texte).
|
- Alignement règle/cartes mesuré au pixel réel (indépendant du zoom et de la taille de texte).
|
||||||
@@ -540,7 +562,7 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
|
|||||||
### Attribution auteur
|
### Attribution auteur
|
||||||
- Ajout en-têtes copyright dans tous les fichiers source
|
- Ajout en-têtes copyright dans tous les fichiers source
|
||||||
(viewer.js, viewer.html, viewer.css, background.js)
|
(viewer.js, viewer.html, viewer.css, background.js)
|
||||||
- Ajout `@author FroSteel` sur les fonctions principales
|
- Ajout `@author Quentin` sur les fonctions principales
|
||||||
(loadForDate, buildCard, buildTooltipHTML, pinTooltip, _softUnpinPopup,
|
(loadForDate, buildCard, buildTooltipHTML, pinTooltip, _softUnpinPopup,
|
||||||
positionTooltipAnchored, _applyViewMode, _moveElementsToSidebar,
|
positionTooltipAnchored, _applyViewMode, _moveElementsToSidebar,
|
||||||
_restoreElementsToTopbar, fetchAndShowCurrentUser, _maybeRetryFetchUser,
|
_restoreElementsToTopbar, fetchAndShowCurrentUser, _maybeRetryFetchUser,
|
||||||
@@ -548,7 +570,7 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
|
|||||||
openPersistentTimelinePopup, showTooltip, _findFreePopupPosition,
|
openPersistentTimelinePopup, showTooltip, _findFreePopupPosition,
|
||||||
_clampPopupInSafeArea, findEasyVistaSession, fetchPlanningXml,
|
_clampPopupInSafeArea, findEasyVistaSession, fetchPlanningXml,
|
||||||
fetchCurrentUser, detectNetworkContext)
|
fetchCurrentUser, detectNetworkContext)
|
||||||
- Ajout signature "Développé par FroSteel" en bas du popup
|
- Ajout signature "Développé par Quentin" en bas du popup
|
||||||
user-badge (style cohérent avec footer version : 11px, italique,
|
user-badge (style cohérent avec footer version : 11px, italique,
|
||||||
gris atténué, séparateur fin)
|
gris atténué, séparateur fin)
|
||||||
- Mise à jour `description` du manifest pour mentionner DGNSI
|
- Mise à jour `description` du manifest pour mentionner DGNSI
|
||||||
@@ -740,6 +762,6 @@ Une intervention terminée par le tech mais dont la fiche était passée en stat
|
|||||||
|
|
||||||
## Auteur
|
## Auteur
|
||||||
|
|
||||||
**FroSteel** (FRS)
|
**Quentin** (FRS)
|
||||||
Technicien DGNSI — Canton de Vaud
|
Technicien DGNSI — Canton de Vaud
|
||||||
Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
|
Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 FroSteel
|
Copyright (c) 2026 Quentin
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ Extension Chrome / Firefox pour visualiser de manière claire et rapide le plann
|
|||||||
|
|
||||||
## Aperçu rapide
|
## Aperçu rapide
|
||||||
|
|
||||||
- **Auteur** : FroSteel (FRS), Technicien DGNSI — Canton de Vaud
|
- **Auteur** : Quentin (FRS), Technicien DGNSI — Canton de Vaud
|
||||||
- **Public cible** : coordinateurs DGNSI qui pilotent dans EasyVista (`itsma.etat-de-vaud.ch` / `itsma.vd.ch`) le planning de l'équipe technicienne
|
- **Public cible** : coordinateurs DGNSI qui pilotent dans EasyVista (`itsma.etat-de-vaud.ch` / `itsma.vd.ch`) le planning de l'équipe technicienne
|
||||||
- **Démarrage projet** : jeudi 16 avril 2026
|
- **Démarrage projet** : jeudi 16 avril 2026
|
||||||
- **Version actuelle** : [`v2026.5.49`](https://gitea.netaplaid.ch/FroSteel/Planification/releases/tag/v2026.5.49) (latest)
|
- **Version actuelle** : [`v2026.5.50`](https://gitea.netaplaid.ch/FroSteel/Planification/releases/tag/v2026.5.50) (latest)
|
||||||
- **Contact** : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact) ou [ouvrir une issue](https://gitea.netaplaid.ch/FroSteel/Planification/issues/new)
|
- **Contact** : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact) ou [ouvrir une issue](https://gitea.netaplaid.ch/FroSteel/Planification/issues/new)
|
||||||
- **Manifest** : V3 (Chrome/Edge/Firefox 140+)
|
- **Manifest** : V3 (Chrome/Edge/Firefox 140+)
|
||||||
- **Format** : `.zip` (Chromium) + `.xpi` signé Mozilla (Firefox)
|
- **Format** : `.zip` (Chromium) + `.xpi` signé Mozilla (Firefox)
|
||||||
@@ -121,7 +121,14 @@ Le numéro de **majeure** n'est **pas** un mois et **pas** un chiffre lié au ca
|
|||||||
|
|
||||||
## Versions notables
|
## Versions notables
|
||||||
|
|
||||||
### `v2026.5.49` (latest, 1 septembre 2026) — Sécurité hors session, onglet Email clarifié, lecture au défilement, affichage fiabilisé
|
### `v2026.5.50` (latest, 29 septembre 2026) — Texte des actions rétabli, catégories Roll Out et Déménagement
|
||||||
|
|
||||||
|
- Correctif : depuis le 29 septembre 2026, EasyVista renvoie les actions avec une cellule de moins et le texte complet des actions ne s'affichait plus ; l'extension retrouve désormais ces informations par leur contenu et par le nom des colonnes, pour tenir aux prochains changements de format
|
||||||
|
- Si EasyVista change encore de format, un avertissement clair apparaît dans la console (une seule fois)
|
||||||
|
- Réservations : deux nouvelles catégories, Roll Out et Déménagement, propres à la Planification (dans EasyVista, la réservation reste « Evènements spéciaux ») ; partagées entre postes comme les autres informations de réservation
|
||||||
|
- Mettre à jour tous les postes le même jour : un poste resté en 2026.5.49 effacerait ces catégories en modifiant la réservation
|
||||||
|
|
||||||
|
### `v2026.5.49` (1 septembre 2026) — Sécurité hors session, onglet Email clarifié, lecture au défilement, affichage fiabilisé
|
||||||
|
|
||||||
- Sans session EasyVista active : plus d'écriture dans le dossier partagé (cache local seul) ni de modification (déplacement, suppression, absence, réservation bloqués proprement)
|
- Sans session EasyVista active : plus d'écriture dans le dossier partagé (cache local seul) ni de modification (déplacement, suppression, absence, réservation bloqués proprement)
|
||||||
- Onglet Email séparé en deux sections (déplacement / annulation), copie (CC) commune avec aperçu en direct des adresses retenues, variables en colonnes
|
- Onglet Email séparé en deux sections (déplacement / annulation), copie (CC) commune avec aperçu en direct des adresses retenues, variables en colonnes
|
||||||
@@ -263,10 +270,10 @@ de cette version dans `firefox-updates.json`.
|
|||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
[MIT License](LICENSE) — Copyright (c) 2026 FroSteel
|
[MIT License](LICENSE) — Copyright (c) 2026 Quentin
|
||||||
|
|
||||||
## Auteur
|
## Auteur
|
||||||
|
|
||||||
**FroSteel** (FRS)
|
**Quentin** (FRS)
|
||||||
Technicien DGNSI — Canton de Vaud
|
Technicien DGNSI — Canton de Vaud
|
||||||
Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
|
Contact : voir [page wiki Contact](https://gitea.netaplaid.ch/FroSteel/Planification/wiki/Contact)
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
"addons": {
|
"addons": {
|
||||||
"[email protected]": {
|
"[email protected]": {
|
||||||
"updates": [
|
"updates": [
|
||||||
|
{
|
||||||
|
"version": "2026.5.50",
|
||||||
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.50/planification-v2026.5.50-firefox.xpi",
|
||||||
|
"update_hash": "sha256:314ae3cba7e31d1f6eb89c419a666d49d156cd78a83c116de03f5e48b8a094df"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "2026.5.49",
|
"version": "2026.5.49",
|
||||||
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.49/planification-v2026.5.49-firefox.xpi",
|
"update_link": "https://gitea.netaplaid.ch/FroSteel/Planification/releases/download/v2026.5.49/planification-v2026.5.49-firefox.xpi",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Planification - native messaging host // 2026.1.4 - FRS
|
# Planification - native messaging host // 2026.1.4 - FRS
|
||||||
# Developpe par FroSteel (FRS) - DGNSI, Canton de Vaud.
|
# Developpe par Quentin (FRS) - DGNSI, Canton de Vaud.
|
||||||
# Licence MIT - voir le fichier LICENSE.
|
# Licence MIT - voir le fichier LICENSE.
|
||||||
#
|
#
|
||||||
# Pont entre l'extension et un dossier reseau partage pour le cache du planning.
|
# Pont entre l'extension et un dossier reseau partage pour le cache du planning.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Planification - gestionnaire graphique du helper de sync. // 2026.1.5 - FRS
|
# Planification - gestionnaire graphique du helper de sync. // 2026.1.5 - FRS
|
||||||
# Developpe par FroSteel (FRS) - DGNSI, Canton de Vaud.
|
# Developpe par Quentin (FRS) - DGNSI, Canton de Vaud.
|
||||||
# Licence MIT - voir le fichier LICENSE.
|
# Licence MIT - voir le fichier LICENSE.
|
||||||
#
|
#
|
||||||
# Fenetre avec : Installer / Mettre a jour, Reparer, Desinstaller.
|
# Fenetre avec : Installer / Mettre a jour, Reparer, Desinstaller.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Planification - icone barre des taches (best-effort, optionnel) // 2026.1.4 - FRS
|
# Planification - icone barre des taches (best-effort, optionnel) // 2026.1.4 - FRS
|
||||||
# Developpe par FroSteel (FRS) - DGNSI, Canton de Vaud.
|
# Developpe par Quentin (FRS) - DGNSI, Canton de Vaud.
|
||||||
# Licence MIT - voir le fichier LICENSE.
|
# Licence MIT - voir le fichier LICENSE.
|
||||||
#
|
#
|
||||||
# Indique que le helper de sync est installe. Clic / menu -> fenetre du journal
|
# Indique que le helper de sync est installe. Clic / menu -> fenetre du journal
|
||||||
|
|||||||
+8
-8
@@ -4,13 +4,13 @@
|
|||||||
* Service worker (Manifest V3) : récupération session EV, fetch planning XML,
|
* Service worker (Manifest V3) : récupération session EV, fetch planning XML,
|
||||||
* fetch fiches détaillées, gestion cache.
|
* fetch fiches détaillées, gestion cache.
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 FroSteel
|
* Copyright (c) 2026 Quentin
|
||||||
* Licensed under the MIT License — see LICENSE file in the project root.
|
* Licensed under the MIT License — see LICENSE file in the project root.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
|
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
|
||||||
*
|
*
|
||||||
* Développé par FroSteel (FRS) — DGNSI, Canton de Vaud. Licence MIT.
|
* Développé par Quentin (FRS) — DGNSI, Canton de Vaud. Licence MIT.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// background.js — Service worker (Manifest V3) — v4
|
// background.js — Service worker (Manifest V3) — v4
|
||||||
@@ -262,7 +262,7 @@ chrome.action.onClicked.addListener(async () => {
|
|||||||
* dans l'URL des onglets historiques après un relog. Sinon, fallback sur
|
* dans l'URL des onglets historiques après un relog. Sinon, fallback sur
|
||||||
* l'ancienne logique URL (compat sans la permission).
|
* l'ancienne logique URL (compat sans la permission).
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function _hasCookiesPermission() {
|
async function _hasCookiesPermission() {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@@ -423,7 +423,7 @@ async function findEasyVistaSession() {
|
|||||||
* Ce n'est PAS le HTML de la page Planning — le serveur ne rend pas les données
|
* Ce n'est PAS le HTML de la page Planning — le serveur ne rend pas les données
|
||||||
* dans le HTML, elles arrivent via cet endpoint AJAX.
|
* dans le HTML, elles arrivent via cet endpoint AJAX.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function fetchPlanningXml(origin, phpsessid, unixDate) {
|
async function fetchPlanningXml(origin, phpsessid, unixDate) {
|
||||||
// v2026.5.41 : groupId vient de admin_config (défaut SI-CSS).
|
// v2026.5.41 : groupId vient de admin_config (défaut SI-CSS).
|
||||||
@@ -781,7 +781,7 @@ async function extendSessionKeepAlive(origin, phpsessid) {
|
|||||||
* @param {boolean} force - si true, ignore le cache et refait le test
|
* @param {boolean} force - si true, ignore le cache et refait le test
|
||||||
* @returns {Promise<"internal"|"external">}
|
* @returns {Promise<"internal"|"external">}
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function detectNetworkContext(force = false) {
|
async function detectNetworkContext(force = false) {
|
||||||
const CACHE_KEY = "network_context_v2"; // v5.0.12 : nouvelle clé pour invalider le cache fautif v5.0.11
|
const CACHE_KEY = "network_context_v2"; // v5.0.12 : nouvelle clé pour invalider le cache fautif v5.0.11
|
||||||
@@ -921,7 +921,7 @@ function watchReconnectTabForIamLogin(tabId) {
|
|||||||
* Retourne { name: "Nom Prénom" | null, login: "..." | null } ou null si
|
* Retourne { name: "Nom Prénom" | null, login: "..." | null } ou null si
|
||||||
* tout a échoué.
|
* tout a échoué.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function fetchCurrentUser(origin, phpsessid) {
|
async function fetchCurrentUser(origin, phpsessid) {
|
||||||
const url = `${origin}/index.php?PHPSESSID=${encodeURIComponent(phpsessid)}`;
|
const url = `${origin}/index.php?PHPSESSID=${encodeURIComponent(phpsessid)}`;
|
||||||
@@ -1306,7 +1306,7 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) {
|
|||||||
* Retourne { groups: [{id, name}, ...] }. Liste vide si le fetch échoue ou
|
* Retourne { groups: [{id, name}, ...] }. Liste vide si le fetch échoue ou
|
||||||
* si EV renvoie une page de login / redirection JS.
|
* si EV renvoie une page de login / redirection JS.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function detectGroupsFromEV(origin, phpsessid) {
|
async function detectGroupsFromEV(origin, phpsessid) {
|
||||||
const url = `${origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(phpsessid)}`;
|
const url = `${origin}/index.php?eventName=HelpDesk_PlanningItem&PHPSESSID=${encodeURIComponent(phpsessid)}`;
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Planification",
|
"name": "Planification",
|
||||||
"version": "2026.5.49",
|
"version": "2026.5.50",
|
||||||
"description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par FroSteel — DGNSI, Canton de Vaud.",
|
"description": "Vue claire et rapide du planning des techniciens EasyVista. Développé par Quentin — DGNSI, Canton de Vaud.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl/El4I9gxcZkp5bnheg4ZBCKxckRLt9xQ5x+i6phEPYlydl6NapWlkbOyycUaeTZcpoEh/tKQnpMpoEc5jY8zCoRTwh/vfejVBZE6ANDG/dc3iix/1Z8OhRI4zMaRYQ+DQTjS5Q6l6wkcwzbQB85HZNhke/k1qIW6RCuBjeJYmES5WVDwzfCNVvaF6uQvdOCpLjrA3+LI6vUBEwaMg1hM42mC3dU0z9p7qzAby/dH3XpZD9HTFXLJL9/1zryN9rXNDpvZwYWnJ0hLTu5tfmURkvgKOwquX1KJy9vupmhUALRtS08Fplx/e3kUH3LD+coS1ExLwoOZ+1ORBUE9JCB8wIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl/El4I9gxcZkp5bnheg4ZBCKxckRLt9xQ5x+i6phEPYlydl6NapWlkbOyycUaeTZcpoEh/tKQnpMpoEc5jY8zCoRTwh/vfejVBZE6ANDG/dc3iix/1Z8OhRI4zMaRYQ+DQTjS5Q6l6wkcwzbQB85HZNhke/k1qIW6RCuBjeJYmES5WVDwzfCNVvaF6uQvdOCpLjrA3+LI6vUBEwaMg1hM42mC3dU0z9p7qzAby/dH3XpZD9HTFXLJL9/1zryN9rXNDpvZwYWnJ0hLTu5tfmURkvgKOwquX1KJy9vupmhUALRtS08Fplx/e3kUH3LD+coS1ExLwoOZ+1ORBUE9JCB8wIDAQAB",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"activeTab",
|
"activeTab",
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
|
* Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 FroSteel
|
* Copyright (c) 2026 Quentin
|
||||||
* Licensed under the MIT License — see LICENSE file in the project root.
|
* Licensed under the MIT License — see LICENSE file in the project root.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
|
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,10 +2,10 @@
|
|||||||
<!--
|
<!--
|
||||||
Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
|
Planification — Extension navigateur EasyVista (Canton de Vaud / DGNSI)
|
||||||
|
|
||||||
Copyright (c) 2026 FroSteel
|
Copyright (c) 2026 Quentin
|
||||||
Licensed under the MIT License — see LICENSE file in the project root.
|
Licensed under the MIT License — see LICENSE file in the project root.
|
||||||
|
|
||||||
@author FroSteel
|
@author Quentin
|
||||||
@repository https://gitea.netaplaid.ch/FroSteel/Planification
|
@repository https://gitea.netaplaid.ch/FroSteel/Planification
|
||||||
-->
|
-->
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|||||||
+164
-66
@@ -5,13 +5,13 @@
|
|||||||
* réservations, absences, statut pompier, etc. Regroupé par tech avec
|
* réservations, absences, statut pompier, etc. Regroupé par tech avec
|
||||||
* timelines visuelles et popups détaillés.
|
* timelines visuelles et popups détaillés.
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 FroSteel
|
* Copyright (c) 2026 Quentin
|
||||||
* Licensed under the MIT License — see LICENSE file in the project root.
|
* Licensed under the MIT License — see LICENSE file in the project root.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
|
* @repository https://gitea.netaplaid.ch/FroSteel/Planification
|
||||||
*
|
*
|
||||||
* Développé par FroSteel (FRS) — DGNSI, Canton de Vaud. Licence MIT.
|
* Développé par Quentin (FRS) — DGNSI, Canton de Vaud. Licence MIT.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -543,15 +543,23 @@ let _myActionsRows = null;
|
|||||||
let _myActionsLoading = false;
|
let _myActionsLoading = false;
|
||||||
let _myActionsDiag = ""; // diagnostic du dernier fetch (affiché dans l'onglet)
|
let _myActionsDiag = ""; // diagnostic du dernier fetch (affiché dans l'onglet)
|
||||||
|
|
||||||
// v2026.5.46 — impersonation (RÉSERVÉE à FroSteel) : permet de tester
|
// v2026.5.46 — impersonation (RÉSERVÉE à Quentin) : permet de tester
|
||||||
// la vue coordinateur en "se faisant passer pour" une autre personne. Chargé
|
// la vue coordinateur en "se faisant passer pour" une autre personne. Chargé
|
||||||
// depuis admin_config (cfg.impersonate = { enabled, name, initials, memberId }).
|
// depuis admin_config (cfg.impersonate = { enabled, name, initials, memberId }).
|
||||||
let _impersonate = null;
|
let _impersonate = null;
|
||||||
// Vrai uniquement si l'utilisateur connecté EST FroSteel (garde-fou
|
// Vrai uniquement si l'utilisateur connecté EST Quentin (garde-fou
|
||||||
// pour n'afficher l'onglet Test qu'à lui).
|
// pour n'afficher l'onglet Test qu'à lui).
|
||||||
function _isQuentin() {
|
// Empreintes FNV-1a (32 bits) du nom et du prenom de l'auteur : jamais en clair dans le code.
|
||||||
|
const _EMPREINTES_AUTEUR = [0x716d54e7, 0xa6e31b47];
|
||||||
|
function _empreinteMot(mot) {
|
||||||
|
let h = 0x811c9dc5;
|
||||||
|
for (const c of mot) { h ^= c.codePointAt(0); h = Math.imul(h, 0x01000193) >>> 0; }
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
function _estAuteur() {
|
||||||
const n = (state && state.currentUser && state.currentUser.name) || "";
|
const n = (state && state.currentUser && state.currentUser.name) || "";
|
||||||
return /exemple/i.test(n) && /FroSteel/i.test(n);
|
const mots = new Set(n.toLowerCase().split(/[^a-zà-ÿ]+/).filter(Boolean).map(_empreinteMot));
|
||||||
|
return _EMPREINTES_AUTEUR.every((e) => mots.has(e));
|
||||||
}
|
}
|
||||||
// Identité effective utilisée pour retrouver "l'action liée à la personne"
|
// Identité effective utilisée pour retrouver "l'action liée à la personne"
|
||||||
// (Partie A « Mes actions »). Si impersonation active ET FRS → identité simulée,
|
// (Partie A « Mes actions »). Si impersonation active ET FRS → identité simulée,
|
||||||
@@ -563,7 +571,7 @@ function _effectiveIdentity() {
|
|||||||
memberId: (state && state.currentUser && state.currentUser.memberId) || "",
|
memberId: (state && state.currentUser && state.currentUser.memberId) || "",
|
||||||
impersonated: false
|
impersonated: false
|
||||||
};
|
};
|
||||||
if (_isQuentin() && _impersonate && _impersonate.enabled &&
|
if (_estAuteur() && _impersonate && _impersonate.enabled &&
|
||||||
(_impersonate.name || _impersonate.initials || _impersonate.memberId)) {
|
(_impersonate.name || _impersonate.initials || _impersonate.memberId)) {
|
||||||
return {
|
return {
|
||||||
name: _impersonate.name || "",
|
name: _impersonate.name || "",
|
||||||
@@ -1176,7 +1184,7 @@ const _CURRENT_USER_RETRY_DELAY_MS = 60 * 1000;
|
|||||||
/**
|
/**
|
||||||
* Récupère le nom de l'utilisateur EasyVista connecté et l'affiche dans le badge.
|
* Récupère le nom de l'utilisateur EasyVista connecté et l'affiche dans le badge.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function fetchAndShowCurrentUser() {
|
async function fetchAndShowCurrentUser() {
|
||||||
const attemptId = _currentUserRetryCount + 1;
|
const attemptId = _currentUserRetryCount + 1;
|
||||||
@@ -1278,7 +1286,7 @@ async function fetchAndShowCurrentUser() {
|
|||||||
*
|
*
|
||||||
* @param {string} reason - contexte pour les logs (ex: "after_load_success")
|
* @param {string} reason - contexte pour les logs (ex: "after_load_success")
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _maybeRetryFetchUser(reason) {
|
function _maybeRetryFetchUser(reason) {
|
||||||
if (state.currentUser && state.currentUser.name) {
|
if (state.currentUser && state.currentUser.name) {
|
||||||
@@ -1432,7 +1440,7 @@ function toggleUserNamePopup() {
|
|||||||
// gris atténué, séparé par une fine ligne supérieure.
|
// gris atténué, séparé par une fine ligne supérieure.
|
||||||
const authorLine = document.createElement("div");
|
const authorLine = document.createElement("div");
|
||||||
authorLine.className = "user-name-popup-author";
|
authorLine.className = "user-name-popup-author";
|
||||||
authorLine.textContent = "Développé par FroSteel";
|
authorLine.textContent = "Développé par Quentin";
|
||||||
authorLine.title = "Auteur de l'extension";
|
authorLine.title = "Auteur de l'extension";
|
||||||
popup.appendChild(authorLine);
|
popup.appendChild(authorLine);
|
||||||
|
|
||||||
@@ -2143,7 +2151,7 @@ function initModalScrollLock() {
|
|||||||
/**
|
/**
|
||||||
* Pied de page discret bas-droite avec FRS et numéro de version.
|
* Pied de page discret bas-droite avec FRS et numéro de version.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function initAppFooter() {
|
function initAppFooter() {
|
||||||
if (document.querySelector(".app-footer")) return;
|
if (document.querySelector(".app-footer")) return;
|
||||||
@@ -2214,7 +2222,7 @@ function _toggleView() {
|
|||||||
* logs verbeux étape par étape ont été retirés, ils polluaient la console
|
* logs verbeux étape par étape ont été retirés, ils polluaient la console
|
||||||
* à chaque toggle.
|
* à chaque toggle.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _applyViewMode() {
|
function _applyViewMode() {
|
||||||
const mode = _getCurrentView();
|
const mode = _getCurrentView();
|
||||||
@@ -2291,7 +2299,7 @@ function _applyViewMode() {
|
|||||||
* v2026.5.36 : crée ou retrouve la sidebar, et y déplace les éléments listés.
|
* 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.
|
* Les éléments sont déplacés dans l'ordre du tableau.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _moveElementsToSidebar(ids) {
|
function _moveElementsToSidebar(ids) {
|
||||||
// Créer (ou retrouver) le wrapper qui contiendra sidebar + main côte à côte
|
// Créer (ou retrouver) le wrapper qui contiendra sidebar + main côte à côte
|
||||||
@@ -2369,7 +2377,7 @@ function _moveElementsToSidebar(ids) {
|
|||||||
* v2026.5.36 : restaure chaque élément à son parent d'origine (mémorisé dans
|
* v2026.5.36 : restaure chaque élément à son parent d'origine (mémorisé dans
|
||||||
* data-orig-parent). Utilisé quand on revient en vue classique.
|
* data-orig-parent). Utilisé quand on revient en vue classique.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _restoreElementsToTopbar(ids) {
|
function _restoreElementsToTopbar(ids) {
|
||||||
// v2026.5.37 : d'abord remettre les flèches dans .date-nav (avant de
|
// v2026.5.37 : d'abord remettre les flèches dans .date-nav (avant de
|
||||||
@@ -2476,7 +2484,7 @@ function _restoreToOriginalParent(el, label) {
|
|||||||
/**
|
/**
|
||||||
* Horloge HH:MM au milieu de la topbar, mise à jour chaque minute (depuis v5.0.0).
|
* Horloge HH:MM au milieu de la topbar, mise à jour chaque minute (depuis v5.0.0).
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function initAppClock() {
|
function initAppClock() {
|
||||||
const el = document.getElementById("app-clock");
|
const el = document.getElementById("app-clock");
|
||||||
@@ -3110,10 +3118,10 @@ async function showAdminPanel(initialSection) {
|
|||||||
{ id: "diagnostics",label: "Diagnostics", render: renderAdminSectionDiagnostics },
|
{ id: "diagnostics",label: "Diagnostics", render: renderAdminSectionDiagnostics },
|
||||||
{ id: "about", label: "À propos", render: renderAdminSectionAbout }
|
{ id: "about", label: "À propos", render: renderAdminSectionAbout }
|
||||||
];
|
];
|
||||||
// v2026.5.46 — onglet "Test (FRS)" RÉSERVÉ à FroSteel : permet de
|
// v2026.5.46 — onglet "Test (FRS)" RÉSERVÉ à Quentin : permet de
|
||||||
// se faire passer pour une autre personne (coordinateur) pour tester « Mes
|
// se faire passer pour une autre personne (coordinateur) pour tester « Mes
|
||||||
// actions » sans être réellement coordinateur. Caché pour tout autre utilisateur.
|
// actions » sans être réellement coordinateur. Caché pour tout autre utilisateur.
|
||||||
if (_isQuentin()) {
|
if (_estAuteur()) {
|
||||||
sections.push({ id: "qrotest", label: "🧪 Test (FRS)", render: renderAdminSectionImpersonate });
|
sections.push({ id: "qrotest", label: "🧪 Test (FRS)", render: renderAdminSectionImpersonate });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4888,7 +4896,7 @@ function renderAdminSectionImpersonate(container, cfg, saveFn) {
|
|||||||
|
|
||||||
const intro = document.createElement("p");
|
const intro = document.createElement("p");
|
||||||
intro.style.cssText = "font-size:13px; color:var(--text-muted); margin:0 0 6px;";
|
intro.style.cssText = "font-size:13px; color:var(--text-muted); margin:0 0 6px;";
|
||||||
intro.textContent = "Réservé à FroSteel. Permet de tester la vue « Mes actions » en te faisant passer pour une autre personne (ex. un coordinateur). N'affecte que l'affichage local de l'extension — aucune action côté EasyVista.";
|
intro.textContent = "Réservé à Quentin. Permet de tester la vue « Mes actions » en te faisant passer pour une autre personne (ex. un coordinateur). N'affecte que l'affichage local de l'extension — aucune action côté EasyVista.";
|
||||||
container.appendChild(intro);
|
container.appendChild(intro);
|
||||||
|
|
||||||
const who = document.createElement("p");
|
const who = document.createElement("p");
|
||||||
@@ -5456,7 +5464,7 @@ function renderAdminSectionAbout(container, cfg, saveFn) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="admin-about-row">
|
<div class="admin-about-row">
|
||||||
<div class="admin-about-label">Auteur</div>
|
<div class="admin-about-label">Auteur</div>
|
||||||
<div class="admin-about-value"><strong>FroSteel</strong></div>
|
<div class="admin-about-value"><strong>Quentin</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-about-row">
|
<div class="admin-about-row">
|
||||||
<div class="admin-about-label">Affiliation</div>
|
<div class="admin-about-label">Affiliation</div>
|
||||||
@@ -6616,7 +6624,13 @@ async function submitAbsence(opts) {
|
|||||||
// ville, rue, catégorie) au créneau, propagées par le merge et affichées dans
|
// ville, rue, catégorie) au créneau, propagées par le merge et affichées dans
|
||||||
// la bulle. Les réservations créées directement dans EV restent inchangées.
|
// la bulle. Les réservations créées directement dans EV restent inchangées.
|
||||||
const RESERVATION_TYPE_GUID = "{E7432422-55CB-4DB9-8A26-619D036E2155}"; // Evènements spéciaux
|
const RESERVATION_TYPE_GUID = "{E7432422-55CB-4DB9-8A26-619D036E2155}"; // Evènements spéciaux
|
||||||
const RESERVATION_CATEGORIES = ["Installation", "Remplacement", "Récupération", "Incident"];
|
// v2026.5.50 — « Roll Out » et « Déménagement » : catégories propres à la
|
||||||
|
// Planification (méta du carnet, partagée par le dossier) ; côté EasyVista, la
|
||||||
|
// réservation reste « Evènements spéciaux ».
|
||||||
|
const RESERVATION_CATEGORIES = ["Installation", "Remplacement", "Récupération", "Incident", "Roll Out", "Déménagement"];
|
||||||
|
// Celles que seule la Planification repère (texte de l'action) : la fiche EV,
|
||||||
|
// qui ne connaît que le catalogue, ne doit pas les remplacer.
|
||||||
|
const CATEGORIES_PROPRES_PLANIFICATION = ["Roll Out", "Déménagement"];
|
||||||
// v2026.5.46 — GUID du type "Pompier" (utilisé par le popup pompier de la
|
// v2026.5.46 — GUID du type "Pompier" (utilisé par le popup pompier de la
|
||||||
// modal Réservation). Doit rester aligné avec HOLIDAY_TYPES.
|
// modal Réservation). Doit rester aligné avec HOLIDAY_TYPES.
|
||||||
const POMPIER_TYPE_GUID = "{60D70502-063D-45AD-9415-25C1C556105F}";
|
const POMPIER_TYPE_GUID = "{60D70502-063D-45AD-9415-25C1C556105F}";
|
||||||
@@ -6630,7 +6644,8 @@ function _resTypeColorKey(cat) {
|
|||||||
case "récupération":
|
case "récupération":
|
||||||
case "recuperation": return "recup";
|
case "recuperation": return "recup";
|
||||||
case "incident": return "incident";
|
case "incident": return "incident";
|
||||||
default: return null;
|
case "roll out": return "rollout";
|
||||||
|
default: return null; // dont « Déménagement » : couleur par défaut des réservations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6645,7 +6660,11 @@ function _lookupInterventionInfo(num) {
|
|||||||
if (String(iv.ref || "").trim() === n || String(iv.actionId || "").trim() === n) {
|
if (String(iv.ref || "").trim() === n || String(iv.actionId || "").trim() === n) {
|
||||||
const cat = (iv.categoryLine || "").toLowerCase();
|
const cat = (iv.categoryLine || "").toLowerCase();
|
||||||
let categorie = "";
|
let categorie = "";
|
||||||
if (/install/.test(cat)) categorie = "Installation";
|
// Une réf I… (incident) ne reçoit pas les catégories propres à la Planification.
|
||||||
|
const refIncident = /^I\d/.test(iv.ref || "");
|
||||||
|
if (!refIncident && isRollOut(iv)) categorie = "Roll Out";
|
||||||
|
else if (!refIncident && /d[ée]m[ée]nag/.test(cat)) categorie = "Déménagement";
|
||||||
|
else if (/install/.test(cat)) categorie = "Installation";
|
||||||
else if (/remplac/.test(cat)) categorie = "Remplacement";
|
else if (/remplac/.test(cat)) categorie = "Remplacement";
|
||||||
else if (/r[ée]cup/.test(cat)) categorie = "Récupération";
|
else if (/r[ée]cup/.test(cat)) categorie = "Récupération";
|
||||||
else if (/incident/.test(cat)) categorie = "Incident";
|
else if (/incident/.test(cat)) categorie = "Incident";
|
||||||
@@ -6764,7 +6783,13 @@ async function _fetchReservationInfo(num) {
|
|||||||
const titM = html.match(/"dbFieldName"\s*:\s*"TITLE_FR"[^}]*?"value"\s*:\s*"([^"]{2,300})"/);
|
const titM = html.match(/"dbFieldName"\s*:\s*"TITLE_FR"[^}]*?"value"\s*:\s*"([^"]{2,300})"/);
|
||||||
if (titM) titreFiche = (decodeJsonString ? decodeJsonString(titM[1]) : titM[1]).trim();
|
if (titM) titreFiche = (decodeJsonString ? decodeJsonString(titM[1]) : titM[1]).trim();
|
||||||
const titreCourt = deriveShortTitle({ ref: n, categoryLine: titreFiche });
|
const titreCourt = deriveShortTitle({ ref: n, categoryLine: titreFiche });
|
||||||
const categorie = RESERVATION_CATEGORIES.includes(titreCourt) ? titreCourt : "";
|
let categorie = RESERVATION_CATEGORIES.includes(titreCourt) ? titreCourt : "";
|
||||||
|
// Catégories propres à la Planification, repérées dans le titre de la fiche
|
||||||
|
// (deriveShortTitle ne les connaît pas sans le texte de l'action).
|
||||||
|
if (!categorie && titreCourt !== "Incident") {
|
||||||
|
if (/roll\s*-?\s*out/i.test(titreFiche)) categorie = "Roll Out";
|
||||||
|
else if (/d[ée]m[ée]nag/i.test(titreFiche)) categorie = "Déménagement";
|
||||||
|
}
|
||||||
let ville = "", rue = "";
|
let ville = "", rue = "";
|
||||||
const locM = html.match(/"dbFieldName"\s*:\s*"[^"]*(?:LOCATION|LOCALISATION|ADDRESS|ADRESSE)[^"]*"[^}]*?"value"\s*:\s*"([^"]{3,160})"/i)
|
const locM = html.match(/"dbFieldName"\s*:\s*"[^"]*(?:LOCATION|LOCALISATION|ADDRESS|ADRESSE)[^"]*"[^}]*?"value"\s*:\s*"([^"]{3,160})"/i)
|
||||||
|| html.match(/Localisation[^>]{0,40}>\s*([^<]{3,160})/i);
|
|| html.match(/Localisation[^>]{0,40}>\s*([^<]{3,160})/i);
|
||||||
@@ -6820,11 +6845,18 @@ function _wireResNumberLookup(numField, cibles) {
|
|||||||
if (num === lastNum) return;
|
if (num === lastNum) return;
|
||||||
lastNum = num;
|
lastNum = num;
|
||||||
LOG.info("reservation", `n° saisi : ${num} → recherche infos (planning + EV)`);
|
LOG.info("reservation", `n° saisi : ${num} → recherche infos (planning + EV)`);
|
||||||
apply(_lookupInterventionInfo(num)); // instantané (cache planning)
|
const infoPlanning = _lookupInterventionInfo(num);
|
||||||
|
apply(infoPlanning); // instantané (cache planning)
|
||||||
|
// Roll Out / Déménagement trouvé par le planning pour CE n° : la fiche EV ne
|
||||||
|
// le voit pas et ne doit pas le remplacer par une catégorie du catalogue.
|
||||||
|
const categoriePropre = infoPlanning && CATEGORIES_PROPRES_PLANIFICATION.includes(infoPlanning.categorie)
|
||||||
|
? infoPlanning.categorie : "";
|
||||||
const seq = ++seqRef; // fetch EV réel en arrière-plan
|
const seq = ++seqRef; // fetch EV réel en arrière-plan
|
||||||
if (loadingBar) loadingBar.classList.remove("hidden");
|
if (loadingBar) loadingBar.classList.remove("hidden");
|
||||||
_fetchReservationInfo(num).then(info => {
|
_fetchReservationInfo(num).then(info => {
|
||||||
if (seq === seqRef) { apply(info); if (loadingBar) loadingBar.classList.add("hidden"); }
|
if (seq !== seqRef) return;
|
||||||
|
apply(info && categoriePropre ? { ...info, categorie: categoriePropre } : info);
|
||||||
|
if (loadingBar) loadingBar.classList.add("hidden");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
numField.addEventListener("change", trigger);
|
numField.addEventListener("change", trigger);
|
||||||
@@ -7056,6 +7088,12 @@ function showReservationEditModal(iv) {
|
|||||||
const catSelect = document.createElement("select"); catSelect.className = "modal-form-select";
|
const catSelect = document.createElement("select"); catSelect.className = "modal-form-select";
|
||||||
const emptyOpt = document.createElement("option"); emptyOpt.value = ""; emptyOpt.textContent = "—"; catSelect.appendChild(emptyOpt);
|
const emptyOpt = document.createElement("option"); emptyOpt.value = ""; emptyOpt.textContent = "—"; catSelect.appendChild(emptyOpt);
|
||||||
RESERVATION_CATEGORIES.forEach(c => { const o = document.createElement("option"); o.value = c; o.textContent = c; if (c === iv.resCategorie) o.selected = true; catSelect.appendChild(o); });
|
RESERVATION_CATEGORIES.forEach(c => { const o = document.createElement("option"); o.value = c; o.textContent = c; if (c === iv.resCategorie) o.selected = true; catSelect.appendChild(o); });
|
||||||
|
// Catégorie inconnue de cette version (posée par une version plus récente,
|
||||||
|
// reçue du dossier) : gardée telle quelle, sinon l'enregistrer l'effacerait
|
||||||
|
// sur tous les postes (l'édition la plus récente gagne à la fusion).
|
||||||
|
if (iv.resCategorie && !RESERVATION_CATEGORIES.includes(iv.resCategorie)) {
|
||||||
|
const o = document.createElement("option"); o.value = iv.resCategorie; o.textContent = iv.resCategorie; o.selected = true; catSelect.appendChild(o);
|
||||||
|
}
|
||||||
catGroup.appendChild(catSelect); grid.appendChild(catGroup);
|
catGroup.appendChild(catSelect); grid.appendChild(catGroup);
|
||||||
const villeField = _mkField("Ville / village", iv.resVille, "");
|
const villeField = _mkField("Ville / village", iv.resVille, "");
|
||||||
const rueField = _mkField("Rue / chemin / route", iv.resRue, "");
|
const rueField = _mkField("Rue / chemin / route", iv.resRue, "");
|
||||||
@@ -7660,7 +7698,7 @@ function sendMessage(msg) {
|
|||||||
// Filets supplémentaires à l'ingestion : correspondance par réf, et signature
|
// Filets supplémentaires à l'ingestion : correspondance par réf, et signature
|
||||||
// composite (tech|type|dates|libellé) pour les résa/absences sans requestId.
|
// composite (tech|type|dates|libellé) pour les résa/absences sans requestId.
|
||||||
//
|
//
|
||||||
// Auteur : FroSteel (FRS).
|
// Auteur : Quentin (FRS).
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const TOMBSTONE_TTL_MS = 7 * 24 * 3600 * 1000; // rétention tombstone (convergence postes)
|
const TOMBSTONE_TTL_MS = 7 * 24 * 3600 * 1000; // rétention tombstone (convergence postes)
|
||||||
@@ -7904,7 +7942,7 @@ function ivFreshness(iv) {
|
|||||||
* 3. existence : max des événements des deux côtés, puis règle vie/mort —
|
* 3. existence : max des événements des deux côtés, puis règle vie/mort —
|
||||||
* une VIE strictement postérieure à la mort lève les tombstones
|
* une VIE strictement postérieure à la mort lève les tombstones
|
||||||
* (ré-apparition EV autoritaire), sinon la mort est conservée/propagée.
|
* (ré-apparition EV autoritaire), sinon la mort est conservée/propagée.
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function ivMerge(a, b) {
|
function ivMerge(a, b) {
|
||||||
if (!a) return b;
|
if (!a) return b;
|
||||||
@@ -8197,7 +8235,7 @@ function _ivAbsenceSig(iv, techId) {
|
|||||||
*
|
*
|
||||||
* @returns {{day: object, toAnalyze: object[]}} le store mis à jour + la
|
* @returns {{day: object, toAnalyze: object[]}} le store mis à jour + la
|
||||||
* liste des enregistrements à passer à l'analyse de disparition.
|
* liste des enregistrements à passer à l'analyse de disparition.
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function storeApplyFresh(day, freshTechs, isoDate) {
|
function storeApplyFresh(day, freshTechs, isoDate) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -8854,7 +8892,7 @@ function _legacyDayToV2(isoDate, v1) {
|
|||||||
* Convertit tout l'ancien dossier (racine v1) vers le store v2 : liste les
|
* Convertit tout l'ancien dossier (racine v1) vers le store v2 : liste les
|
||||||
* jours, lit chaque fichier v1, convertit, fusionne au store local (dayMerge)
|
* jours, lit chaque fichier v1, convertit, fusionne au store local (dayMerge)
|
||||||
* et sauvegarde — le push différé propage ensuite vers le sous-dossier v2.
|
* et sauvegarde — le push différé propage ensuite vers le sous-dossier v2.
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function importLegacyFolderCache(rootPath, onProgress) {
|
async function importLegacyFolderCache(rootPath, onProgress) {
|
||||||
if (state.networkContext !== "internal") return { ok: false, error: "réseau externe — dossier partagé injoignable" };
|
if (state.networkContext !== "internal") return { ok: false, error: "réseau externe — dossier partagé injoignable" };
|
||||||
@@ -8958,7 +8996,7 @@ function scheduleFolderPush(isoDate) {
|
|||||||
* v2026.5.46 — le rendu après tirage du dossier passe par storeProject
|
* v2026.5.46 — le rendu après tirage du dossier passe par storeProject
|
||||||
* (mêmes filtres que tout rendu : tombstones, dock, déplacées récentes) → fini
|
* (mêmes filtres que tout rendu : tombstones, dock, déplacées récentes) → fini
|
||||||
* le flash d'une iv parquée/déplacée qui réapparaissait via la sync.
|
* le flash d'une iv parquée/déplacée qui réapparaissait via la sync.
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
// v2026.5.49 — blocage ferme lié à la connexion : les opérations sensibles
|
// v2026.5.49 — blocage ferme lié à la connexion : les opérations sensibles
|
||||||
// (lecture/écriture du dossier PARTAGÉ, déplacement, suppression) exigent une
|
// (lecture/écriture du dossier PARTAGÉ, déplacement, suppression) exigent une
|
||||||
@@ -9080,7 +9118,7 @@ async function reconcileCacheForDate(isoDate) {
|
|||||||
* 6. phase B : fiches complètes + analyse des disparues en SÉQUENTIEL ;
|
* 6. phase B : fiches complètes + analyse des disparues en SÉQUENTIEL ;
|
||||||
* 7. sauvegarde finale + convergence dossier en arrière-plan.
|
* 7. sauvegarde finale + convergence dossier en arrière-plan.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function loadForDate(isoDate, opts = {}) {
|
async function loadForDate(isoDate, opts = {}) {
|
||||||
LOG.info("load", `début pour ${isoDate}`, { opts, hasSession: !!state.session });
|
LOG.info("load", `début pour ${isoDate}`, { opts, hasSession: !!state.session });
|
||||||
@@ -9793,10 +9831,10 @@ const RX_LOGIN_COMMENTAIRE = /(?:^|\n|<br\s*\/?>)\s*([a-z0-9_]{3,12})\s*:\s+(\S[
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extrait toutes les actions d'une fiche en parsant les blocs "rows" du HTML.
|
* Extrait toutes les actions d'une fiche en parsant les blocs "rows" du HTML.
|
||||||
* Une action a 14 values. Indices stables :
|
* Une action a 13 values (14 jusqu'au 28.09.2026). Indices stables :
|
||||||
* [2] = Intervenant (ex: "Nom, Prénom" ou "EZV_WS_REST_USER")
|
* [2] = Intervenant (ex: "Nom, Prénom" ou "EZV_WS_REST_USER")
|
||||||
* [4] = Type d'action (ex: "AL-Intervention", "Ajout d'informations")
|
* [4] = Type d'action (ex: "AL-Intervention", "Ajout d'informations")
|
||||||
* [13] = JSON stringifié contenant ACTION_ID, AM_DONE_BY_ID, …
|
* dernière cellule = JSON stringifié contenant ACTION_ID, AM_DONE_BY_ID, …
|
||||||
*
|
*
|
||||||
* v2026.5.45 — la position des dates et de la description varie selon le
|
* v2026.5.45 — la position des dates et de la description varie selon le
|
||||||
* layout EV (anciennement [8]/[9]/[11], aujourd'hui [6]/[7]/[9] sur les
|
* layout EV (anciennement [8]/[9]/[11], aujourd'hui [6]/[7]/[9] sur les
|
||||||
@@ -9842,26 +9880,30 @@ function parseAllActionsFromFicheHtml(html) {
|
|||||||
// Une "vraie" action a 14 valeurs. On se contente de 12 minimum
|
// Une "vraie" action a 14 valeurs. On se contente de 12 minimum
|
||||||
// pour avoir au moins la description.
|
// pour avoir au moins la description.
|
||||||
//
|
//
|
||||||
// v2026.5.44 on extrait aussi actionId / amDoneById depuis values[13]
|
// v2026.5.44 on extrait aussi actionId / amDoneById depuis la cellule JSON
|
||||||
// qui est un JSON stringifié contenant {NAME, ACTION_ID, AM_DONE_BY_ID, …}.
|
// stringifié qui contient {NAME, ACTION_ID, AM_DONE_BY_ID, …}.
|
||||||
// Permet de filtrer la "bonne action" par ACTION_ID au lieu de matcher
|
// Permet de filtrer la "bonne action" par ACTION_ID au lieu de matcher
|
||||||
// par nom de tech (le matching par nom échouait par exemple sur les
|
// par nom de tech (le matching par nom échouait par exemple sur les
|
||||||
// actions où l'intervenant est noté "EZV_WS_REST_USER" ou un format
|
// actions où l'intervenant est noté "EZV_WS_REST_USER" ou un format
|
||||||
// qui ne correspond pas exactement au tech.name de la config).
|
// qui ne correspond pas exactement au tech.name de la config).
|
||||||
let actionId = null;
|
let actionId = null;
|
||||||
let amDoneById = null;
|
let amDoneById = null;
|
||||||
if (values.length >= 14 && values[13]) {
|
// v2026.5.50 — la cellule JSON est repérée par son contenu (la dernière qui
|
||||||
|
// mentionne ACTION_ID) : EV a retiré la cellule vide qui la précédait
|
||||||
|
// (29.09.2026), elle est passée de values[13] à values[12].
|
||||||
|
const valeurJson = [...values].reverse().find(v => v && v.includes("ACTION_ID"));
|
||||||
|
if (valeurJson) {
|
||||||
// v2026.5.46 — BUG : dans le HTML, les guillemets du JSON imbriqué sont
|
// v2026.5.46 — BUG : dans le HTML, les guillemets du JSON imbriqué sont
|
||||||
// encodés " (pas des " littéraux). Le regex cherchait des " littéraux
|
// encodés " (pas des " littéraux). Le regex cherchait des " littéraux
|
||||||
// → ACTION_ID et surtout AM_DONE_BY_ID étaient TOUJOURS null → le matching
|
// → ACTION_ID et surtout AM_DONE_BY_ID étaient TOUJOURS null → le matching
|
||||||
// d'action par id (le plus fiable, indépendant du nom) ne marchait JAMAIS.
|
// d'action par id (le plus fiable, indépendant du nom) ne marchait JAMAIS.
|
||||||
// On DÉCODE values[13] d'abord pour que le regex matche.
|
// On DÉCODE la cellule d'abord pour que le regex matche.
|
||||||
const raw13 = decodeUnicodeEscapes(values[13]);
|
const jsonDecode = decodeUnicodeEscapes(valeurJson);
|
||||||
// Parsing tolérant via regex (on accepte avec ou sans guillemets autour
|
// Parsing tolérant via regex (on accepte avec ou sans guillemets autour
|
||||||
// des valeurs numériques) — robuste aux formats JSON avec ou sans
|
// des valeurs numériques) — robuste aux formats JSON avec ou sans
|
||||||
// échappements résiduels.
|
// échappements résiduels.
|
||||||
const mAid = raw13.match(/"ACTION_ID"\s*:\s*"?(\d+)"?/);
|
const mAid = jsonDecode.match(/"ACTION_ID"\s*:\s*"?(\d+)"?/);
|
||||||
const mDid = raw13.match(/"AM_DONE_BY_ID"\s*:\s*"?(\d*)"?/);
|
const mDid = jsonDecode.match(/"AM_DONE_BY_ID"\s*:\s*"?(\d*)"?/);
|
||||||
if (mAid) actionId = mAid[1];
|
if (mAid) actionId = mAid[1];
|
||||||
if (mDid && mDid[1]) amDoneById = mDid[1];
|
if (mDid && mDid[1]) amDoneById = mDid[1];
|
||||||
}
|
}
|
||||||
@@ -10148,7 +10190,7 @@ function actionBelongsToTech(action, techName, techId) {
|
|||||||
* 3-4) actions de la fiche : du tech ? même jour ? commentaire « login: » ?
|
* 3-4) actions de la fiche : du tech ? même jour ? commentaire « login: » ?
|
||||||
* 4bis) action de PLANIFICATION via xhr2 (la fiche ne la contient pas).
|
* 4bis) action de PLANIFICATION via xhr2 (la fiche ne la contient pas).
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
async function analyzeMissingIv(iv) {
|
async function analyzeMissingIv(iv) {
|
||||||
const techName = iv.techName || "";
|
const techName = iv.techName || "";
|
||||||
@@ -10725,6 +10767,9 @@ async function fetchAndUpdateIntervention(iv, myToken, opts = {}) {
|
|||||||
if (!ficheResp.ok) {
|
if (!ficheResp.ok) {
|
||||||
iv.ficheFetched = true;
|
iv.ficheFetched = true;
|
||||||
iv.ficheFetchError = ficheResp.error || "fetch_failed";
|
iv.ficheFetchError = ficheResp.error || "fetch_failed";
|
||||||
|
if (ficheResp.error !== "session_expired") {
|
||||||
|
LOG.warn("seq", `iv ${iv.actionId} (ref=${iv.ref}) fiche non chargée : ${iv.ficheFetchError}`);
|
||||||
|
}
|
||||||
if (ficheResp.error === "session_expired") {
|
if (ficheResp.error === "session_expired") {
|
||||||
state.session = null;
|
state.session = null;
|
||||||
// session expirée → + bannière + abort de tout le refresh.
|
// session expirée → + bannière + abort de tout le refresh.
|
||||||
@@ -10830,6 +10875,8 @@ async function fetchAndUpdateIntervention(iv, myToken, opts = {}) {
|
|||||||
} else if (tlResp && tlResp.error === "session_expired") {
|
} else if (tlResp && tlResp.error === "session_expired") {
|
||||||
state.session = null;
|
state.session = null;
|
||||||
showSessionExpiredBanner();
|
showSessionExpiredBanner();
|
||||||
|
} else {
|
||||||
|
LOG.warn("timeline", `iv ${iv.actionId} : timeline non chargée (${(tlResp && tlResp.error) || "échec"})`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
LOG.warn("timeline", "erreur iv", iv.actionId, err);
|
LOG.warn("timeline", "erreur iv", iv.actionId, err);
|
||||||
@@ -11161,12 +11208,16 @@ function parseFicheHtml(html) {
|
|||||||
* { rows: [
|
* { rows: [
|
||||||
* {value:"..."}, // [0..10] statut, groupe, dates, etc.
|
* {value:"..."}, // [0..10] statut, groupe, dates, etc.
|
||||||
* {value:"Date : ... Heure : ... Lieu : ..."}, // [11] DESCRIPTION
|
* {value:"Date : ... Heure : ... Lieu : ..."}, // [11] DESCRIPTION
|
||||||
* {value:""},
|
* {value:"{\"ACTION_ID\":\"57700033\",...}"} // [12] JSON stringifié
|
||||||
* {value:"{\"ACTION_ID\":\"57700033\",...}"} // [13] JSON stringifié
|
|
||||||
* ] }
|
* ] }
|
||||||
* ] }}}
|
* ] }}}
|
||||||
*
|
*
|
||||||
* On cherche l'action dont rows[13].ACTION_ID === actionId ; si trouvée, on
|
* v2026.5.50 — jusqu'au 28.09.2026, EV intercalait une cellule vide en [12] et
|
||||||
|
* le JSON était en [13] (14 cellules) ; depuis, 13 cellules. La cellule JSON est
|
||||||
|
* repérée par son contenu (la dernière qui mentionne "ACTION_ID"), valable pour
|
||||||
|
* les deux formats.
|
||||||
|
*
|
||||||
|
* On cherche l'action dont le JSON porte ACTION_ID === actionId ; si trouvée, on
|
||||||
* retourne rows[11] nettoyé (br→\n, entités décodées) ; sinon null.
|
* retourne rows[11] nettoyé (br→\n, entités décodées) ; sinon null.
|
||||||
*/
|
*/
|
||||||
function parseTimelineJsonForAction(jsonText, actionId) {
|
function parseTimelineJsonForAction(jsonText, actionId) {
|
||||||
@@ -11182,15 +11233,23 @@ function parseTimelineJsonForAction(jsonText, actionId) {
|
|||||||
const values = data?.data?.data?.values;
|
const values = data?.data?.data?.values;
|
||||||
if (!Array.isArray(values)) return null;
|
if (!Array.isArray(values)) return null;
|
||||||
|
|
||||||
|
// Colonne de la description repérée par son nom (« Commentaire ou
|
||||||
|
// Description ») : une cellule ajoutée ou retirée avant elle ne la décale plus.
|
||||||
|
const colonnes = data?.data?.data?.columns;
|
||||||
|
const idxColonneDescription = _indexColonneTimeline(colonnes, /description/i);
|
||||||
const targetId = String(actionId);
|
const targetId = String(actionId);
|
||||||
|
let actionsLisibles = 0;
|
||||||
|
|
||||||
for (const entry of values) {
|
for (const entry of values) {
|
||||||
const rows = entry?.rows;
|
const rows = entry?.rows;
|
||||||
if (!Array.isArray(rows) || rows.length < 14) continue;
|
if (!Array.isArray(rows)) continue;
|
||||||
|
|
||||||
// rows[13] = JSON stringifié qui contient ACTION_ID
|
// JSON stringifié qui contient ACTION_ID : la dernière cellule qui le mentionne.
|
||||||
const extraRaw = rows[13]?.value;
|
const celluleJson = [...rows].reverse().find(
|
||||||
if (!extraRaw || typeof extraRaw !== "string") continue;
|
c => typeof c?.value === "string" && c.value.includes('"ACTION_ID"'));
|
||||||
|
const extraRaw = celluleJson?.value;
|
||||||
|
if (!extraRaw) continue;
|
||||||
|
actionsLisibles++;
|
||||||
|
|
||||||
let extra;
|
let extra;
|
||||||
try {
|
try {
|
||||||
@@ -11205,15 +11264,54 @@ function parseTimelineJsonForAction(jsonText, actionId) {
|
|||||||
|
|
||||||
if (String(extra.ACTION_ID) !== targetId) continue;
|
if (String(extra.ACTION_ID) !== targetId) continue;
|
||||||
|
|
||||||
// Trouvé : extraire la description (rows[11]) et la nettoyer.
|
// Trouvé. Description : la colonne nommée, sinon la position historique 11,
|
||||||
const rawDesc = rows[11]?.value || extra["AM_ACTION.DESCRIPTION"] || "";
|
// jamais la cellule JSON elle-même ; en dernier recours, le champ du JSON.
|
||||||
|
const celluleDesc = rows[idxColonneDescription >= 0 ? idxColonneDescription : 11];
|
||||||
|
const texteCellule = celluleDesc && celluleDesc !== celluleJson &&
|
||||||
|
typeof celluleDesc.value === "string" ? celluleDesc.value : "";
|
||||||
|
const rawDesc = texteCellule || extra["AM_ACTION.DESCRIPTION"] || "";
|
||||||
const cleaned = cleanHtmlBlock(rawDesc);
|
const cleaned = cleanHtmlBlock(rawDesc);
|
||||||
return cleaned || null;
|
return cleaned || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Des actions, mais aucune lisible : EV a changé le format de la timeline.
|
||||||
|
if (values.length > 0 && actionsLisibles === 0) {
|
||||||
|
_signalerFormatEvInconnu("timeline", {
|
||||||
|
nbActions: values.length,
|
||||||
|
nbCellules: values.map(e => (Array.isArray(e?.rows) ? e.rows.length : null)),
|
||||||
|
colonnes: Array.isArray(colonnes) ? colonnes.map(_nomColonneTimeline) : "absentes"
|
||||||
|
});
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _CHAMPS_NOM_COLONNE_TIMELINE = ["dbFieldName", "name", "label", "title"];
|
||||||
|
|
||||||
|
/** Nom lisible d'une colonne de la timeline EV (le premier champ de nom renseigné). */
|
||||||
|
function _nomColonneTimeline(colonne) {
|
||||||
|
const champ = _CHAMPS_NOM_COLONNE_TIMELINE.find(k => typeof colonne?.[k] === "string" && colonne[k]);
|
||||||
|
return champ ? colonne[champ] : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Index de la première colonne de la timeline EV dont un nom répond au motif ; -1 sinon. */
|
||||||
|
function _indexColonneTimeline(colonnes, motif) {
|
||||||
|
if (!Array.isArray(colonnes)) return -1;
|
||||||
|
return colonnes.findIndex(c => _CHAMPS_NOM_COLONNE_TIMELINE.some(
|
||||||
|
k => typeof c?.[k] === "string" && motif.test(c[k])));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signale UNE fois par session et par source une réponse EV dont le format n'est
|
||||||
|
* plus reconnu (mise à jour d'EasyVista) : structure seulement (nombres de
|
||||||
|
* cellules, noms de colonnes), jamais les valeurs.
|
||||||
|
*/
|
||||||
|
const _formatsEvInconnusSignales = new Set();
|
||||||
|
function _signalerFormatEvInconnu(source, structure) {
|
||||||
|
if (_formatsEvInconnusSignales.has(source)) return;
|
||||||
|
_formatsEvInconnusSignales.add(source);
|
||||||
|
LOG.warn(source, "format EasyVista non reconnu : texte des actions indisponible", structure);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nettoie un bloc HTML pour obtenir du texte brut lisible.
|
* Nettoie un bloc HTML pour obtenir du texte brut lisible.
|
||||||
@@ -12209,7 +12307,7 @@ function _recurringAbsenceLabel(isoDate) {
|
|||||||
// v2026.5.46 — menu contextuel (clic droit) sur le nom d'un technicien :
|
// v2026.5.46 — menu contextuel (clic droit) sur le nom d'un technicien :
|
||||||
// ouvre directement Réservation / Absence / Pompier / Sync douchette avec CE
|
// ouvre directement Réservation / Absence / Pompier / Sync douchette avec CE
|
||||||
// tech pré-sélectionné (la liste/sélecteur de tech est masquée dans la modal).
|
// tech pré-sélectionné (la liste/sélecteur de tech est masquée dans la modal).
|
||||||
// Les boutons « Ajouter » du haut restent inchangés. Auteur : FroSteel.
|
// Les boutons « Ajouter » du haut restent inchangés. Auteur : Quentin.
|
||||||
let _techCtxMenuEl = null;
|
let _techCtxMenuEl = null;
|
||||||
function _closeTechContextMenu() {
|
function _closeTechContextMenu() {
|
||||||
if (_techCtxMenuEl) { _techCtxMenuEl.remove(); _techCtxMenuEl = null; }
|
if (_techCtxMenuEl) { _techCtxMenuEl.remove(); _techCtxMenuEl = null; }
|
||||||
@@ -12317,7 +12415,7 @@ function _showQuickAddMenu(ev, tech, startHHMM, endHHMM, disabledReason) {
|
|||||||
/**
|
/**
|
||||||
* Construit la card HTML d'un technicien (header, timeline, interventions, absences).
|
* Construit la card HTML d'un technicien (header, timeline, interventions, absences).
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function buildCard(tech, isoDate) {
|
function buildCard(tech, isoDate) {
|
||||||
const card = document.createElement("section");
|
const card = document.createElement("section");
|
||||||
@@ -13008,7 +13106,7 @@ async function _initDayBoundsFromConfig() {
|
|||||||
|
|
||||||
// v2026.5.46 — abrège une adresse pour les cartes étroites du mode
|
// v2026.5.46 — abrège une adresse pour les cartes étroites du mode
|
||||||
// exact : retire le TYPE de voie et sa particule (« Rue de l'Exemple 5 »
|
// exact : retire le TYPE de voie et sa particule (« Rue de l'Exemple 5 »
|
||||||
// devient « Université 5 », « Av. de l'Exemple 14 » devient « Gare 14 »).
|
// devient « Exemple 5 », « Av. de l'Exemple 14 » devient « Exemple 14 »).
|
||||||
// Si tout disparaît (« Grand-Rue » seule), on garde l'adresse d'origine.
|
// Si tout disparaît (« Grand-Rue » seule), on garde l'adresse d'origine.
|
||||||
function _shortAddress(adresse) {
|
function _shortAddress(adresse) {
|
||||||
const a = String(adresse || "").trim();
|
const a = String(adresse || "").trim();
|
||||||
@@ -14057,7 +14155,7 @@ function getStatusClass(iv) {
|
|||||||
/**
|
/**
|
||||||
* Lie les segments timeline à un popover persistant au clic (depuis v4.2.3).
|
* Lie les segments timeline à un popover persistant au clic (depuis v4.2.3).
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function bindTimelinePopover(el) {
|
function bindTimelinePopover(el) {
|
||||||
// v2026.5.40 : comportement UNIFIÉ vue classique + vue horizontale.
|
// v2026.5.40 : comportement UNIFIÉ vue classique + vue horizontale.
|
||||||
@@ -14191,7 +14289,7 @@ function findIvByActionId(actionId) {
|
|||||||
/**
|
/**
|
||||||
* Ouvre une grande popup détaillée pour un segment timeline.
|
* Ouvre une grande popup détaillée pour un segment timeline.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function openPersistentTimelinePopup(el) {
|
function openPersistentTimelinePopup(el) {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -17547,10 +17645,10 @@ function cleanContactName(raw) {
|
|||||||
// v2026.5.49 — retirer les « ? » (et le caractère de remplacement Unicode
|
// v2026.5.49 — retirer les « ? » (et le caractère de remplacement Unicode
|
||||||
// U+FFFD, souvent rendu « ? ») collés au nom : c'est un encodage cassé côté
|
// U+FFFD, souvent rendu « ? ») collés au nom : c'est un encodage cassé côté
|
||||||
// EasyVista, jamais un vrai caractère d'un nom de personne. Ex : "?Exemple"
|
// EasyVista, jamais un vrai caractère d'un nom de personne. Ex : "?Exemple"
|
||||||
// → "Exemple", "Exemple?, FroSteel" → "FroSteel".
|
// → "Exemple", "Exemple?, Quentin" → "Quentin".
|
||||||
s = s.replace(/[?\uFFFD]/g, " ");
|
s = s.replace(/[?\uFFFD]/g, " ");
|
||||||
// ... et recoller l'espace laisse devant une virgule/point-virgule
|
// ... et recoller l'espace laisse devant une virgule/point-virgule
|
||||||
// ("Exemple?, FroSteel" -> "Exemple , FroSteel" -> "FroSteel").
|
// ("Exemple?, Quentin" -> "Exemple , Quentin" -> "Quentin").
|
||||||
s = s.replace(/\s+([,;:])/g, "$1");
|
s = s.replace(/\s+([,;:])/g, "$1");
|
||||||
// Espaces multiples → un seul
|
// Espaces multiples → un seul
|
||||||
s = s.replace(/\s{2,}/g, " ").trim();
|
s = s.replace(/\s{2,}/g, " ").trim();
|
||||||
@@ -18076,7 +18174,7 @@ function _cancelPendingShowTooltip() {
|
|||||||
/**
|
/**
|
||||||
* Affiche un tooltip au survol d'une intervention/réservation.
|
* Affiche un tooltip au survol d'une intervention/réservation.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function showTooltip(e, iv, rowEl, posOpts) {
|
function showTooltip(e, iv, rowEl, posOpts) {
|
||||||
if (_techCtxMenuEl) return; // v2026.5.46 — menu contextuel ouvert : pas de popup par-dessus
|
if (_techCtxMenuEl) return; // v2026.5.46 — menu contextuel ouvert : pas de popup par-dessus
|
||||||
@@ -18375,7 +18473,7 @@ function reapplyTooltipPosition() {
|
|||||||
* @param {HTMLElement} sourceEl - l'élément déclencheur (row, card, segment)
|
* @param {HTMLElement} sourceEl - l'élément déclencheur (row, card, segment)
|
||||||
* @param {object} opts - options { anchorBelow: true pour préférer dessous }
|
* @param {object} opts - options { anchorBelow: true pour préférer dessous }
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function positionTooltipAnchored(sourceEl, opts) {
|
function positionTooltipAnchored(sourceEl, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
@@ -18555,7 +18653,7 @@ function _rectsOverlap(a, b) {
|
|||||||
* empêche un simple `style.zIndex = "9"` de prendre effet. Il FAUT utiliser
|
* empêche un simple `style.zIndex = "9"` de prendre effet. Il FAUT utiliser
|
||||||
* `setProperty(..., 'important')` pour battre le `!important` du CSS.
|
* `setProperty(..., 'important')` pour battre le `!important` du CSS.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _bringPinnedPopupToFront(popup) {
|
function _bringPinnedPopupToFront(popup) {
|
||||||
if (!popup) return;
|
if (!popup) return;
|
||||||
@@ -18617,7 +18715,7 @@ function _applyRememberedPopupSize(popup, iv, date) {
|
|||||||
* ligne source `rowEl`. Essaie dans l'ordre : droite, gauche, dessous, dessus.
|
* ligne source `rowEl`. Essaie dans l'ordre : droite, gauche, dessous, dessus.
|
||||||
* Retourne {x, y} en coordonnées document, ou null si aucune position libre.
|
* Retourne {x, y} en coordonnées document, ou null si aucune position libre.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _findFreePopupPosition(rowEl, w, h) {
|
function _findFreePopupPosition(rowEl, w, h) {
|
||||||
const pad = 14;
|
const pad = 14;
|
||||||
@@ -18731,7 +18829,7 @@ function _findFreePopupPosition(rowEl, w, h) {
|
|||||||
* v4.3.0 : épingle la bulle courante en la clonant dans un popup détaché
|
* v4.3.0 : épingle la bulle courante en la clonant dans un popup détaché
|
||||||
* ancré au contenu. Le tooltip live redevient disponible.
|
* ancré au contenu. Le tooltip live redevient disponible.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function pinTooltip() {
|
function pinTooltip() {
|
||||||
if (!state.currentTooltipIv) return;
|
if (!state.currentTooltipIv) return;
|
||||||
@@ -19225,7 +19323,7 @@ async function _refreshPinnedPopupIv(popup, iv) {
|
|||||||
*
|
*
|
||||||
* @param {HTMLElement} el - le popup à désépingler
|
* @param {HTMLElement} el - le popup à désépingler
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _softUnpinPopup(el) {
|
function _softUnpinPopup(el) {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
@@ -19848,7 +19946,7 @@ function _getPopupSafeArea() {
|
|||||||
* dans la safe area. Appelé à l'épinglage, pendant le drag, et quand le dock
|
* dans la safe area. Appelé à l'épinglage, pendant le drag, et quand le dock
|
||||||
* apparaît/disparaît.
|
* apparaît/disparaît.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function _clampPopupInSafeArea(popup) {
|
function _clampPopupInSafeArea(popup) {
|
||||||
if (!popup) return;
|
if (!popup) return;
|
||||||
@@ -20731,7 +20829,7 @@ function bindTooltipInteractions() {
|
|||||||
/**
|
/**
|
||||||
* Construit le HTML du tooltip détaillé pour une intervention/réservation.
|
* Construit le HTML du tooltip détaillé pour une intervention/réservation.
|
||||||
*
|
*
|
||||||
* @author FroSteel
|
* @author Quentin
|
||||||
*/
|
*/
|
||||||
function buildTooltipHTML(iv) {
|
function buildTooltipHTML(iv) {
|
||||||
if (!iv) return '<dl><dt>Info</dt><dd>—</dd></dl>';
|
if (!iv) return '<dl><dt>Info</dt><dd>—</dd></dl>';
|
||||||
|
|||||||
Reference in New Issue
Block a user