diff --git a/background.js b/background.js index 34c3616..77dfafe 100644 --- a/background.js +++ b/background.js @@ -457,12 +457,27 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) { ? [ "Planning_delete_reservation", "delete_reservation", - "fc_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", + "fc_delete_absence" ] : [ - "delete_absence", // nom JS "brut" vu dans le onclick - "Planning_delete_absence", - "fc_delete_absence" + // 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; @@ -527,44 +542,30 @@ async function deletePlanningItem(origin, phpsessid, actionId, kind) { // ============================================================================ /** - * v5.0.1 : Détection de la liste complète des membres du groupe EasyVista - * (pas seulement l'équipe de 8 hardcodée). + * v5.0.1 : Détection de la liste complète des membres du groupe EasyVista. * * Stratégie : - * 1) Fetch la page planning principale pour récupérer le `support_ids` actuel - * et le `group_id`. - * 2) Fetch ensuite `/include/components/staff/planning/plan_view_group_supports.php` - * avec ce group_id, qui retourne le HTML d'une popup listant tous les membres - * du groupe avec leur ID et leur nom. + * 1) On part des valeurs connues (group_id=191 et support_ids par défaut). + * Pas besoin de fetcher la page planning HTML (qui souvent ne contient + * pas ces valeurs accessibles en fetch direct, car EasyVista utilise + * des redirections JS). + * 2) Fetch direct /include/components/staff/planning/plan_view_group_supports.php + * qui retourne le HTML d'une popup listant tous les membres du groupe. * 3) Parser ce HTML pour extraire les paires (id, nom). * - * Retourne un tableau d'objets { id, name, alreadyInTeam }. + * Retourne { ids: [{id, name, alreadyInTeam}], groupId }. */ async function detectTeamFromEV(origin, phpsessid) { - // Étape 1 : récupérer support_ids et group_id - const planUrl = origin + "/index.php?PHPSESSID=" + encodeURIComponent(phpsessid) - + "&eventName=HelpDesk_PlanningItem"; - console.log("[bg] detectTeamFromEV → planning page", planUrl.substring(0, 140)); - let planHtml = ""; - try { - const r = await fetch(planUrl, { method: "GET", credentials: "include" }); - if (!r.ok) throw new Error("HTTP " + r.status); - planHtml = await r.text(); - if (looksLikeLoginPage(planHtml)) throw new Error("session_expired"); - } catch (e) { - console.warn("[bg] detectTeam: fetch planning failed:", e); - throw e; - } + // v5.0.1 : valeurs par défaut (correspondent au groupe actuel). + // À terme elles devraient venir de la config admin. + const DEFAULT_GROUP_ID = "191"; + const DEFAULT_SUPPORT_IDS = "76272,83725,66635,92235,90070,40944,72485,86874"; - // Extraire support_ids et group_id - const mSupport = planHtml.match(/name=["']support_ids["'][^>]*\bvalue=["']([0-9,]+)["']/i); - const mGroup = planHtml.match(/name=["']plan_group_id["'][^>]*\bvalue=["'](\d+)["']/i) - || planHtml.match(/[?&]group_id=(\d+)/); - const supportIds = mSupport ? mSupport[1] : ""; - const groupId = mGroup ? mGroup[1] : "191"; - console.log("[bg] support_ids =", supportIds, "| group_id =", groupId); + const groupId = DEFAULT_GROUP_ID; + const supportIds = DEFAULT_SUPPORT_IDS; + console.log("[bg] detectTeamFromEV : group_id =", groupId, "| support_ids =", supportIds); - // Étape 2 : fetch la popup de sélection des intervenants du groupe + // Fetch la popup de sélection des intervenants du groupe const popupUrl = origin + "/include/components/staff/planning/plan_view_group_supports.php" + "?PHPSESSID=" + encodeURIComponent(phpsessid) + "&eventName=" @@ -572,45 +573,46 @@ async function detectTeamFromEV(origin, phpsessid) { + "&support_ids=" + encodeURIComponent(supportIds) + "&group_id=" + encodeURIComponent(groupId); - console.log("[bg] detectTeamFromEV → popup group_supports", popupUrl.substring(0, 140)); + console.log("[bg] detectTeamFromEV → popup group_supports"); + console.log("[bg] URL =", popupUrl.substring(0, 240)); let popupHtml = ""; try { const r = await fetch(popupUrl, { method: "GET", credentials: "include" }); + console.log("[bg] popup status =", r.status); if (!r.ok) throw new Error("HTTP " + r.status + " sur popup group"); popupHtml = await r.text(); + console.log("[bg] popup taille HTML =", popupHtml.length); if (looksLikeLoginPage(popupHtml)) throw new Error("session_expired"); } catch (e) { console.warn("[bg] detectTeam: fetch popup failed:", e); - // Fallback : on retourne au moins les IDs actuels avec noms vides - const idsCsv = supportIds; - const ids = idsCsv ? idsCsv.split(",").filter(Boolean) : []; - return { ids: ids.map(id => ({ id, name: "? (" + id + ")", alreadyInTeam: true })) }; + // Fallback : au moins on retourne les IDs connus avec noms vides + const ids = DEFAULT_SUPPORT_IDS.split(",").filter(Boolean); + return { + ids: ids.map(id => ({ id, name: "? (" + id + ")", alreadyInTeam: true })), + groupId + }; } - // Étape 3 : parser le HTML. La structure typique EV : - // ... Ciuppa, Mathieu ... - // Ou bien : - // 76272Ciuppa, Mathieu... - // - // On tente plusieurs patterns. - + // Parser le HTML. Différents patterns possibles. const results = []; - const currentIdsSet = new Set((supportIds || "").split(",").filter(Boolean)); + const currentIdsSet = new Set(supportIds.split(",").filter(Boolean)); + + // v5.0.1 : log le début du HTML pour diagnostic si parsing échoue + console.log("[bg] popup HTML (début) =", popupHtml.substring(0, 500)); // Pattern 1 : checkboxes + texte voisin - // "(...)Ciuppa, Mathieu(...)" - const rxCheckbox = /]*type=["']checkbox["'][^>]*value=["'](\d{4,7})["'][^>]*>([\s\S]{0,300}?)(?=]*type=["']checkbox["'][^>]*value=["'](\d{4,7})["'][^>]*>([\s\S]{0,400}?)(?= r.id === id)) { results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) }); } } + console.log("[bg] parsing pattern 1 (checkbox) :", results.length, "résultats"); // Pattern 2 : fallback if (results.length === 0) { @@ -623,19 +625,25 @@ async function detectTeamFromEV(origin, phpsessid) { results.push({ id, name: name || "? (" + id + ")", alreadyInTeam: currentIdsSet.has(id) }); } } + console.log("[bg] parsing pattern 2 (option) :", results.length, "résultats"); } - // Pattern 3 : fallback "76272 - Nom, Prénom" brut dans le texte + // Pattern 3 : fallback brut tags HTML contenant ID à proximité d'un nom if (results.length === 0) { - const rxBrut = /\b(\d{4,7})\s*[-–:]\s*([A-ZÀ-ÿ][A-Za-zÀ-ÿ\-']+,\s*[A-ZÀ-ÿ][A-Za-zÀ-ÿ\-' \.]+)/g; - let mB; - while ((mB = rxBrut.exec(popupHtml)) !== null) { - const id = mB[1]; - const name = mB[2].trim(); - if (!results.some(r => r.id === id)) { - results.push({ id, name, alreadyInTeam: currentIdsSet.has(id) }); + // Chercher chaque ID 4-7 chiffres et regarder les 200 caractères qui suivent + const rxAnyId = /\b(\d{5,7})\b([\s\S]{0,200})/g; + let mA; + while ((mA = rxAnyId.exec(popupHtml)) !== null) { + const id = mA[1]; + // Ignorer les IDs qui ressemblent à des timestamps / hash + if (id.length > 6 && parseInt(id, 10) > 1000000000) continue; + const context = mA[2]; + const nameMatch = context.match(/([A-ZÀ-ÿ][A-Za-zÀ-ÿ\-']+,\s*[A-ZÀ-ÿ][A-Za-zÀ-ÿ\-' \.]{2,30})/); + if (nameMatch && !results.some(r => r.id === id)) { + results.push({ id, name: nameMatch[1].trim(), alreadyInTeam: currentIdsSet.has(id) }); } } + console.log("[bg] parsing pattern 3 (brut) :", results.length, "résultats"); } // Ajouter les IDs actuels manquants (sans nom) @@ -645,7 +653,7 @@ async function detectTeamFromEV(origin, phpsessid) { } } - console.log("[bg] " + results.length + " personnes détectées dans le groupe"); + console.log("[bg] " + results.length + " personnes retournées"); return { ids: results, groupId: groupId }; } diff --git a/manifest.json b/manifest.json index bcaae40..ce7a7de 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Planification", - "version": "5.0.1", + "version": "5.0.3", "description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.", "permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"], "host_permissions": [ diff --git a/viewer.css b/viewer.css index 052a420..ecf923e 100644 --- a/viewer.css +++ b/viewer.css @@ -2194,3 +2194,13 @@ header.topbar::before { .admin-team-table tr.admin-row-excluded input[type="text"] { background: var(--bg); } + +/* v5.0.1 : bouton supprimer sur la carte "Absent toute la journée" */ +.absence-delete-wrap { + margin-top: 8px; + text-align: center; +} +.absence-delete-wrap .tooltip-delete-btn { + font-size: 11px; + padding: 4px 8px; +} diff --git a/viewer.js b/viewer.js index 1dabe37..8dfab6c 100644 --- a/viewer.js +++ b/viewer.js @@ -2143,11 +2143,20 @@ function actionNodeToIntervention(node) { // 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" - // "Rollout / Créé par : Nom24, Prénom24" - // "Congés / Créé par : ..." → pas une réservation, c'est une absence - // "Maladie / Créé par : ..." → idem - // "Pompier / Créé par : ..." → idem + // 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 + // une Réservation UNIQUEMENT si son label correspond à une ressource + // matérielle (Ecrans, PC, MAC, Téléphones, UTP, Rollout). Tout le reste + // est une absence. Ça couvre les types de HOLIDAY_TYPES non-matériels. + const RESERVATION_LABELS = /^(ecran(s)?|pc|mac|t[ée]l[ée]phones?|utp|rollout)$/i; let effectiveType = actionType; let reservationLabel = null; let reservationCreator = null; @@ -2155,14 +2164,15 @@ function actionNodeToIntervention(node) { if (reservationMatch) { const label1 = reservationMatch[1].trim(); const creator = reservationMatch[2].trim(); - // Les "absences" connues (Congés/Maladie/Pompier) restent des absences - if (/^(cong[ée]s|maladie|pompier)$/i.test(label1)) { - effectiveType = "AL-Absence"; - } else { - // Tout autre label (Ecrans, Rollout, ...) → Réservation + if (RESERVATION_LABELS.test(label1)) { + // Ressource matérielle → Réservation effectiveType = "AL-Reservation"; reservationLabel = label1; reservationCreator = creator; + } else { + // Tout autre (Congés, Maladie, Pompier, Evènements spéciaux, Réunion, + // Déménagement, Formation, etc.) → Absence + effectiveType = "AL-Absence"; } } @@ -2345,6 +2355,14 @@ function mergeCacheAndFresh(cached, fresh) { if (!outTech) continue; for (const iv of tech.interventions || []) { if (!freshActionIds.has(iv.actionId)) { + // v5.0.1 : les absences et réservations supprimées côté EasyVista + // sont définitivement retirées (pas ghost). La logique ghost est + // conçue pour les interventions dont on veut garder trace en attendant + // la vérification du statut (clos/annulé). Absences/réservations n'ont + // pas de notion de statut, une disparition = suppression pure. + if (iv.type === "AL-Absence" || iv.type === "AL-Reservation") { + continue; // ne pas rajouter + } const ghost = { ...iv, ghost: true }; outTech.interventions.push(ghost); } @@ -3894,6 +3912,25 @@ function buildCard(tech, isoDate) { note.textContent = "Absent toute la journée"; } 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 + // dans EasyVista). + if (ab.actionId && !ab.isPompier && !ab._recurring) { + const delWrap = document.createElement("div"); + delWrap.className = "absence-delete-wrap"; + const delBtn = document.createElement("button"); + delBtn.type = "button"; + delBtn.className = "tooltip-delete-btn"; + delBtn.textContent = "🗑 Supprimer l'absence"; + delBtn.dataset.actionId = ab.actionId; + delBtn.dataset.kind = "absence"; + delBtn.addEventListener("click", (e) => { + e.stopPropagation(); + _triggerDeleteItem(ab.actionId, "absence"); + }); + delWrap.appendChild(delBtn); + body.appendChild(delWrap); + } } // v4.1.20 : cas spécifique Pillonel Olivier, absent tous les vendredis. @@ -5811,6 +5848,58 @@ function _softUnpinPopup(el) { } /** Ferme toutes les popups épinglées (appelé par Échap ou changement de date). */ +/** + * v5.0.1 : helper pour déclencher la suppression d'une absence ou réservation. + * Affiche la modal de confirmation, puis appelle le background. + */ +function _triggerDeleteItem(actionId, kind, triggerBtn) { + if (!actionId) return; + const label = kind === "reservation" ? "cette réservation" : "cette absence"; + showAlertModal({ + title: "Confirmer la suppression", + message: `Voulez-vous vraiment supprimer ${label} ? Cette action est irréversible.`, + buttons: [ + { label: "Annuler", variant: "secondary", action: () => {} }, + { + label: "Supprimer", + variant: "danger", + action: async () => { + if (triggerBtn) { + triggerBtn.disabled = true; + triggerBtn.textContent = "Suppression…"; + } + try { + const resp = await sendMessage({ + type: "deletePlanningItem", + actionId: actionId, + kind: kind + }); + if (!resp || !resp.ok) { + throw new Error(resp && resp.error ? resp.error : "erreur inconnue"); + } + showToast("Supprimé", "L'élément a été retiré du planning."); + unpinTooltip(); + closeAllPinnedPopups(); + if (state.session) { + await loadForDate(state.currentDate, { forceRefetch: true }); + } + } catch (err) { + showAlertModal({ + title: "Erreur lors de la suppression", + message: "Impossible de supprimer : " + (err.message || err), + buttons: [{ label: "OK", variant: "secondary", action: () => {} }] + }); + if (triggerBtn) { + triggerBtn.disabled = false; + triggerBtn.textContent = "🗑 Supprimer l'absence"; + } + } + } + } + ] + }); +} + function closeAllPinnedPopups() { for (const p of pinnedPopups.slice()) { p.el.remove(); @@ -6140,49 +6229,10 @@ function bindTooltipInteractions() { }).catch(() => {}); } } else if (action === "delete-item") { - // v5.0.0 : supprimer absence/réservation + // v5.0.0 : supprimer absence/réservation (depuis tooltip) const actionId = btn.dataset.actionId; const kind = btn.dataset.kind || "absence"; - if (!actionId) return; - const label = kind === "reservation" ? "cette réservation" : "cette absence"; - showAlertModal({ - title: "Confirmer la suppression", - message: `Voulez-vous vraiment supprimer ${label} ? Cette action est irréversible.`, - buttons: [ - { label: "Annuler", variant: "secondary", action: () => {} }, - { - label: "Supprimer", - variant: "danger", - action: async () => { - btn.disabled = true; - btn.textContent = "Suppression…"; - try { - const resp = await sendMessage({ - type: "deletePlanningItem", - actionId: actionId, - kind: kind - }); - if (!resp || !resp.ok) { - throw new Error(resp && resp.error ? resp.error : "erreur inconnue"); - } - showToast("Supprimé", "L'élément a été retiré du planning."); - // Unpin tooltip + reload de la date courante - unpinTooltip(); - closeAllPinnedPopups(); - if (state.session) { - await loadForDate(state.currentDate, { forceRefetch: true }); - } - } catch (err) { - showAlertModal({ - title: "Erreur lors de la suppression", - message: "Impossible de supprimer : " + (err.message || err), - buttons: [{ label: "OK", variant: "secondary", action: () => {} }] - }); - } - } - } - ] - }); + _triggerDeleteItem(actionId, kind, btn); } });