Compare commits

..

5 Commits

Author SHA1 Message Date
Quentin Rouiller 9d701701e6 v5.0.8 — Correctifs 2026-04-21 12:53:22 +02:00
Quentin Rouiller 77c68dbe83 v5.0.7 — Correctifs 2026-04-21 12:50:36 +02:00
Quentin Rouiller d4fc8ff250 v5.0.6 — Correctifs 2026-04-21 12:46:58 +02:00
Quentin Rouiller 3996e3fb4f v5.0.5 — Correctifs admin/UX 2026-04-21 12:42:50 +02:00
Quentin Rouiller 86f52029f5 v5.0.4 — Améliorations admin/UX 2026-04-21 12:40:08 +02:00
4 changed files with 103 additions and 48 deletions
+14 -1
View File
@@ -131,7 +131,17 @@ 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));
const r = await fetch(url, { credentials: "include" }); // v5.0.8 : EasyVista retourne maintenant un <script> de redirection si on
// fait la requête sans Referer. Probablement une protection CSRF ajoutée
// récemment. On ajoute un Referer qui simule une navigation depuis la
// page principale du planning.
const r = await fetch(url, {
credentials: "include",
headers: {
"Referer": `${origin}/index.php?eventName=HelpDesk_PlanningItem`,
"X-Requested-With": "XMLHttpRequest"
}
});
if (!r.ok) { if (!r.ok) {
const err = new Error("HTTP " + r.status); const err = new Error("HTTP " + r.status);
err.kind = classifyHttpStatus(r.status); err.kind = classifyHttpStatus(r.status);
@@ -140,6 +150,9 @@ async function fetchFicheHtml(origin, phpsessid, formLink) {
} }
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);
if (html.length < 500) {
console.warn("[bg] ⚠ fiche très courte, contenu =", JSON.stringify(html));
}
return html; return html;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Planification", "name": "Planification",
"version": "5.0.3", "version": "5.0.8",
"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"], "permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"],
"host_permissions": [ "host_permissions": [
+13
View File
@@ -2204,3 +2204,16 @@ header.topbar::before {
font-size: 11px; font-size: 11px;
padding: 4px 8px; padding: 4px 8px;
} }
/* v5.0.4 : boutons preset matin / après-midi / journée dans modal absence */
.modal-preset-row {
gap: 8px;
flex-wrap: wrap;
}
.modal-preset-btn {
flex: 1;
min-width: 100px;
padding: 8px 10px;
font-size: 13px;
cursor: pointer;
}
+75 -46
View File
@@ -1424,6 +1424,38 @@ function showAbsenceModal() {
endGroup.appendChild(endRow); endGroup.appendChild(endRow);
card.appendChild(endGroup); card.appendChild(endGroup);
// v5.0.4 : presets rapides pour les horaires (matin / après-midi / journée)
const presetGroup = document.createElement("div");
presetGroup.className = "modal-form-group";
const presetLabel = document.createElement("label");
presetLabel.className = "modal-form-label";
presetLabel.textContent = "Presets rapides";
presetGroup.appendChild(presetLabel);
const presetRow = document.createElement("div");
presetRow.className = "modal-form-row modal-preset-row";
const presets = [
{ label: "Matin", start: "08:00", end: "12:00" },
{ label: "Après-midi", start: "13:00", end: "18:00" },
{ label: "Toute la journée", start: "08:00", end: "18:00" }
];
for (const p of presets) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "btn btn-secondary modal-preset-btn";
btn.textContent = p.label;
btn.addEventListener("click", () => {
startTime.value = p.start;
endTime.value = p.end;
// Synchroniser visuellement la mise à jour et déclencher
// endDateTouched si besoin (la date reste inchangée)
startTime.dispatchEvent(new Event("input", { bubbles: true }));
endTime.dispatchEvent(new Event("input", { bubbles: true }));
});
presetRow.appendChild(btn);
}
presetGroup.appendChild(presetRow);
card.appendChild(presetGroup);
// v5.0.0 : la date de fin suit la date de début tant que l'user ne l'a // v5.0.0 : la date de fin suit la date de début tant que l'user ne l'a
// pas explicitement modifiée. 95% des absences sont d'un seul jour, donc // pas explicitement modifiée. 95% des absences sont d'un seul jour, donc
// changer juste le start doit mettre à jour le end aussi. // changer juste le start doit mettre à jour le end aussi.
@@ -1923,11 +1955,19 @@ async function loadForDate(isoDate, opts = {}) {
) )
); );
// v5.0.6 : logs détaillés pour diagnostiquer pourquoi le fetch ne se
// lance pas.
const totalIv = merged.techs.reduce((s, t) => s + (t.interventions || []).length, 0);
const totalInterIv = merged.techs.reduce((s, t) =>
s + (t.interventions || []).filter(i => i.type === "AL-Intervention").length, 0);
const notFetched = merged.techs.reduce((s, t) =>
s + (t.interventions || []).filter(i => i.type === "AL-Intervention" && !i.ficheFetched).length, 0);
console.log(`[load] merged : ${merged.techs.length} techs, ${totalIv} iv totales, ${totalInterIv} interventions réelles, ${notFetched} sans fiche`);
console.log(`[load] needFetch = ${needFetch} | doStatusRefresh = ${!!opts.doStatusRefresh} | forceRefetch = ${!!opts.forceRefetch} | aborted = ${isRefreshAborted(myToken)}`);
// v4.3.2 : avant de lancer le fetch des fiches (lourd, ~460 KB chacune), // v4.3.2 : avant de lancer le fetch des fiches (lourd, ~460 KB chacune),
// on fait d'abord un batch xhr2 (léger, ~2-5 KB chacun) pour récupérer // on fait d'abord un batch xhr2 (léger, ~2-5 KB chacun) pour récupérer
// les vraies infos contact/lieu de toutes les interventions en parallèle. // les vraies infos contact/lieu de toutes les interventions en parallèle.
// Comme ça les cartes s'enrichissent en 1-3 secondes au lieu d'attendre
// que l'utilisateur les survole une par une.
if (!isRefreshAborted(myToken)) { if (!isRefreshAborted(myToken)) {
await prefetchAllXhr2(merged.techs, myToken, opts.doStatusRefresh); await prefetchAllXhr2(merged.techs, myToken, opts.doStatusRefresh);
} }
@@ -1936,13 +1976,10 @@ async function loadForDate(isoDate, opts = {}) {
const tFiches = performance.now(); const tFiches = performance.now();
const nFiches = merged.techs.flatMap(t=>t.interventions).filter(i=>i.type==="AL-Intervention").length; const nFiches = merged.techs.flatMap(t=>t.interventions).filter(i=>i.type==="AL-Intervention").length;
console.log(`[load] début fetch des ${nFiches} fiches (statuts)…`); console.log(`[load] début fetch des ${nFiches} fiches (statuts)…`);
// forceAll : uniquement si refresh manuel (bouton "rafraichir").
// À la navigation normale entre dates, on ne refetch que les iv non
// encore enrichies (ficheFetched=false) — ça reprend là où on s'était
// arrêté si un refresh précédent a été interrompu par un changement de
// date.
await refreshStatuses(merged.techs, isoDate, { forceAll: !!opts.doStatusRefresh, myToken }); await refreshStatuses(merged.techs, isoDate, { forceAll: !!opts.doStatusRefresh, myToken });
console.log(`[load] fiches finies en ${Math.round(performance.now() - tFiches)} ms`); console.log(`[load] fiches finies en ${Math.round(performance.now() - tFiches)} ms`);
} else {
console.log(`[load] PAS DE FETCH : needFetch=${needFetch}, doStatusRefresh=${!!opts.doStatusRefresh}, aborted=${isRefreshAborted(myToken)}`);
} }
// 6. Sauvegarder dans le cache (une seule fois, après que tout soit enrichi) // 6. Sauvegarder dans le cache (une seule fois, après que tout soit enrichi)
@@ -2139,24 +2176,18 @@ function actionNodeToIntervention(node) {
if (refFromLabel) ref = refFromLabel[1]; if (refFromLabel) ref = refFromLabel[1];
} }
// Détection du type "Réservation" : un coordinateur a bloqué un créneau. // Détection du type "Réservation" vs "Absence".
// Dans le XML, action_type = "AL-Absence" pour ce genre de créneau, mais
// action_label contient le vrai pattern :
// action_label = "Xxxxx / Créé par : Nom, Prénom"
// Ex: "Ecrans / Créé par : Nom20, Prénom20" → Réservation (matériel)
// "Rollout / Créé par : Nom24, Prénom24" → Réservation
// "Congés / Créé par : ..." → Absence
// "Maladie / Créé par : ..." → Absence
// "Pompier / Créé par : ..." → Absence
// "Evènements spéciaux / Créé par : ..." → Absence
// "Réunion / Créé par : ..." → Absence
// "Déménagement / Créé par : ..." → Absence
// //
// v5.0.2 : pour éviter les faux positifs, on considère qu'une entrée est // v5.0.3 (simplifiée) : le label suit le pattern "Nom / Créé par : X Y".
// une Réservation UNIQUEMENT si son label correspond à une ressource //
// matérielle (Ecrans, PC, MAC, Téléphones, UTP, Rollout). Tout le reste // - Congés / Maladie / Pompier → AL-Absence (tech réellement absent)
// est une absence. Ça couvre les types de HOLIDAY_TYPES non-matériels. // - TOUT LE RESTE (Ecrans, PC, MAC, Rollout, Téléphones, UTP, Réunion,
const RESERVATION_LABELS = /^(ecran(s)?|pc|mac|t[ée]l[ée]phones?|utp|rollout)$/i; // Déménagement, Evènements spéciaux, Formation, ...)
// → AL-Reservation (créneau bloqué, tech pas absent)
//
// Cette règle simple évite les cas "absence toute la journée" déclenchés
// par erreur pour des réservations de type événement / réunion.
const ABSENCE_LABELS = /^(cong[ée]s|maladie|pompier)$/i;
let effectiveType = actionType; let effectiveType = actionType;
let reservationLabel = null; let reservationLabel = null;
let reservationCreator = null; let reservationCreator = null;
@@ -2164,15 +2195,14 @@ function actionNodeToIntervention(node) {
if (reservationMatch) { if (reservationMatch) {
const label1 = reservationMatch[1].trim(); const label1 = reservationMatch[1].trim();
const creator = reservationMatch[2].trim(); const creator = reservationMatch[2].trim();
if (RESERVATION_LABELS.test(label1)) { if (ABSENCE_LABELS.test(label1)) {
// Ressource matérielle → Réservation // Vraie absence du tech
effectiveType = "AL-Absence";
} else {
// Réservation : créneau bloqué (matériel ou activité), tech pas absent
effectiveType = "AL-Reservation"; effectiveType = "AL-Reservation";
reservationLabel = label1; reservationLabel = label1;
reservationCreator = creator; reservationCreator = creator;
} else {
// Tout autre (Congés, Maladie, Pompier, Evènements spéciaux, Réunion,
// Déménagement, Formation, etc.) → Absence
effectiveType = "AL-Absence";
} }
} }
@@ -3912,24 +3942,23 @@ function buildCard(tech, isoDate) {
note.textContent = "Absent toute la journée"; note.textContent = "Absent toute la journée";
} }
body.appendChild(note); body.appendChild(note);
// v5.0.1 : bouton 🗑 pour supprimer l'absence (seulement si actionId réel,
// pas les absences récurrentes type Pillonel vendredi qui n'existent pas // v5.0.4 : tooltip au hover sur toute la carte absent (pas juste un
// dans EasyVista). // bouton visible). Contient : détail période + bouton supprimer si
// c'est une absence supprimable (actionId réel, pas pompier récurrent).
if (ab.actionId && !ab.isPompier && !ab._recurring) { if (ab.actionId && !ab.isPompier && !ab._recurring) {
const delWrap = document.createElement("div"); // On attache le tooltip sur la CARD ENTIÈRE (card) — comme ça
delWrap.className = "absence-delete-wrap"; // survoler n'importe où sur la zone grisée "absent" le déclenche.
const delBtn = document.createElement("button"); const ivCopy = {
delBtn.type = "button"; ...ab,
delBtn.className = "tooltip-delete-btn"; type: "AL-Absence" // force pour buildTooltipHTML
delBtn.textContent = "🗑 Supprimer l'absence"; };
delBtn.dataset.actionId = ab.actionId; card.addEventListener("mouseenter", (e) => {
delBtn.dataset.kind = "absence"; showTooltip(e, ivCopy, card);
delBtn.addEventListener("click", (e) => { });
e.stopPropagation(); card.addEventListener("mouseleave", () => {
_triggerDeleteItem(ab.actionId, "absence"); hideTooltip();
}); });
delWrap.appendChild(delBtn);
body.appendChild(delWrap);
} }
} }