forked from FroSteel/Planification
v5.0.1 — Refonte topbar : horloge HH:MM + compteur session EV + admin caché (5 clics titre)
This commit is contained in:
@@ -211,6 +211,8 @@ async function init() {
|
||||
bindTooltipInteractions();
|
||||
initModalScrollLock(); // v4.2.9 : bloquer le scroll arrière quand modal
|
||||
initAppFooter(); // v4.2.9 : pied de page discret bas-droite
|
||||
initAppClock(); // v5.0.0 : horloge HH:MM au milieu topbar
|
||||
initAdminMenu(); // v5.0.0 : menu admin caché (5 clics sur titre)
|
||||
|
||||
// Initialiser la date = aujourd'hui
|
||||
state.currentDate = todayISO();
|
||||
@@ -714,6 +716,534 @@ function initAppFooter() {
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
||||
// v5.0.0 : horloge HH:MM au milieu de la topbar. Mise à jour toutes les 30s
|
||||
// (les secondes ne sont pas affichées donc pas besoin d'un tick plus rapide).
|
||||
function initAppClock() {
|
||||
const el = document.getElementById("app-clock");
|
||||
if (!el) return;
|
||||
const tick = () => {
|
||||
const d = new Date();
|
||||
const h = String(d.getHours()).padStart(2, "0");
|
||||
const m = String(d.getMinutes()).padStart(2, "0");
|
||||
el.textContent = `${h}:${m}`;
|
||||
// v5.0.0 : profite du tick pour mettre à jour la ligne rouge "now"
|
||||
updateNowLine();
|
||||
};
|
||||
tick();
|
||||
// Tick toutes les 30s : ça garantit une MAJ rapide au changement de min
|
||||
setInterval(tick, 30 * 1000);
|
||||
}
|
||||
|
||||
// v5.0.0 : ligne verticale rouge "heure actuelle" sur la timeline, visible
|
||||
// UNIQUEMENT quand on affiche aujourd'hui. Appelée à chaque tick de l'horloge
|
||||
// + après chaque render (cf renderFromData).
|
||||
function updateNowLine() {
|
||||
const isToday = state.currentDate === todayISO();
|
||||
// Retirer toutes les lignes existantes d'abord
|
||||
document.querySelectorAll(".timeline-now-line").forEach(el => el.remove());
|
||||
if (!isToday) return;
|
||||
// Calculer la position en % sur la timeline (DAY_START à DAY_END)
|
||||
const now = new Date();
|
||||
const nowMin = now.getHours() * 60 + now.getMinutes();
|
||||
if (nowMin < DAY_START || nowMin > DAY_END) return; // hors plage affichée
|
||||
const pct = ((nowMin - DAY_START) / DAY_LEN) * 100;
|
||||
// Ajouter une ligne sur chaque barre timeline visible
|
||||
document.querySelectorAll(".timeline-bar").forEach(bar => {
|
||||
const line = document.createElement("div");
|
||||
line.className = "timeline-now-line";
|
||||
line.style.left = pct + "%";
|
||||
bar.appendChild(line);
|
||||
});
|
||||
}
|
||||
|
||||
// v5.0.0 : menu admin caché. 5 clics consécutifs sur le titre "Planification"
|
||||
// (avec max 2 secondes entre chaque clic) ouvrent le panneau admin.
|
||||
function initAdminMenu() {
|
||||
const title = document.getElementById("app-title");
|
||||
if (!title) return;
|
||||
let clicks = 0;
|
||||
let resetTimer = null;
|
||||
title.addEventListener("click", () => {
|
||||
clicks++;
|
||||
if (resetTimer) clearTimeout(resetTimer);
|
||||
resetTimer = setTimeout(() => { clicks = 0; }, 2000);
|
||||
if (clicks >= 5) {
|
||||
clicks = 0;
|
||||
clearTimeout(resetTimer);
|
||||
showAdminPanel();
|
||||
}
|
||||
});
|
||||
// Cursor pointer pour indiquer (discrètement) qu'il est cliquable
|
||||
title.style.cursor = "default";
|
||||
}
|
||||
|
||||
// v5.0.0 : stockage des paramètres admin dans chrome.storage.local.
|
||||
// Clé unique : "admin_config". Contient la config éditable (équipe,
|
||||
// absences récurrentes, statuts etc.). Au 1er lancement : initialisée
|
||||
// avec les valeurs hardcodées actuelles.
|
||||
const ADMIN_CONFIG_KEY = "admin_config";
|
||||
|
||||
function getDefaultAdminConfig() {
|
||||
return {
|
||||
team: { ...TEAM }, // Clone pour ne pas modifier le hardcode
|
||||
recurringAbsences: { ...RECURRING_ABSENCES }, // idem
|
||||
groupId: "191",
|
||||
evOrigins: ["https://itsma.etat-de-vaud.ch", "https://itsma.vd.ch"],
|
||||
closedStatus: [...CLOSED_STATUS],
|
||||
resolvedStatus: [...RESOLVED_STATUS],
|
||||
cancelledStatus: [...CANCELLED_STATUS],
|
||||
dayStart: 8,
|
||||
dayEnd: 18,
|
||||
cacheDays: 7
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAdminConfig() {
|
||||
try {
|
||||
const stored = await chrome.storage.local.get(ADMIN_CONFIG_KEY);
|
||||
if (stored && stored[ADMIN_CONFIG_KEY]) {
|
||||
// Fusion avec les defaults (pour rajouter d'éventuelles nouvelles clés)
|
||||
return { ...getDefaultAdminConfig(), ...stored[ADMIN_CONFIG_KEY] };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[admin] loadAdminConfig err", e);
|
||||
}
|
||||
return getDefaultAdminConfig();
|
||||
}
|
||||
|
||||
async function saveAdminConfig(cfg) {
|
||||
try {
|
||||
await chrome.storage.local.set({ [ADMIN_CONFIG_KEY]: cfg });
|
||||
console.log("[admin] config sauvegardée");
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("[admin] saveAdminConfig err", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// v5.0.0 : affiche le panel admin plein écran.
|
||||
async function showAdminPanel() {
|
||||
// Ferme un éventuel panel existant
|
||||
const existing = document.getElementById("admin-panel");
|
||||
if (existing) existing.remove();
|
||||
|
||||
// Charge la config actuelle
|
||||
const cfg = await loadAdminConfig();
|
||||
|
||||
// Overlay plein écran
|
||||
const overlay = document.createElement("div");
|
||||
overlay.id = "admin-panel";
|
||||
overlay.className = "modal-overlay admin-overlay";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "admin-panel-card";
|
||||
|
||||
// En-tête
|
||||
const header = document.createElement("div");
|
||||
header.className = "admin-header";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = "⚙ Administration";
|
||||
title.className = "admin-title";
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.type = "button";
|
||||
closeBtn.className = "admin-close-btn";
|
||||
closeBtn.textContent = "×";
|
||||
closeBtn.title = "Fermer (Échap)";
|
||||
closeBtn.addEventListener("click", () => overlay.remove());
|
||||
header.appendChild(title);
|
||||
header.appendChild(closeBtn);
|
||||
card.appendChild(header);
|
||||
|
||||
// Navigation latérale (onglets)
|
||||
const body = document.createElement("div");
|
||||
body.className = "admin-body";
|
||||
|
||||
const sidebar = document.createElement("nav");
|
||||
sidebar.className = "admin-sidebar";
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.className = "admin-content";
|
||||
|
||||
const sections = [
|
||||
{ id: "team", label: "Équipe", render: renderAdminSectionTeam },
|
||||
{ id: "easyvista", label: "EasyVista", render: renderAdminSectionEV },
|
||||
{ id: "appearance", label: "Apparence", render: renderAdminSectionAppearance },
|
||||
{ id: "statuses", label: "Statuts", render: renderAdminSectionStatuses },
|
||||
{ id: "diagnostics",label: "Diagnostics", render: renderAdminSectionDiagnostics }
|
||||
];
|
||||
|
||||
let currentSection = "team";
|
||||
|
||||
const navButtons = {};
|
||||
for (const section of sections) {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "admin-nav-btn";
|
||||
btn.textContent = section.label;
|
||||
btn.dataset.section = section.id;
|
||||
if (section.id === currentSection) btn.classList.add("active");
|
||||
btn.addEventListener("click", () => {
|
||||
currentSection = section.id;
|
||||
for (const k in navButtons) navButtons[k].classList.remove("active");
|
||||
btn.classList.add("active");
|
||||
content.innerHTML = "";
|
||||
section.render(content, cfg, () => saveAndReload(cfg));
|
||||
});
|
||||
navButtons[section.id] = btn;
|
||||
sidebar.appendChild(btn);
|
||||
}
|
||||
|
||||
body.appendChild(sidebar);
|
||||
body.appendChild(content);
|
||||
card.appendChild(body);
|
||||
overlay.appendChild(card);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Rendu initial : section "Équipe"
|
||||
sections[0].render(content, cfg, () => saveAndReload(cfg));
|
||||
|
||||
// Échap ferme le panel
|
||||
const escHandler = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
overlay.remove();
|
||||
document.removeEventListener("keydown", escHandler);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", escHandler);
|
||||
|
||||
async function saveAndReload(updatedCfg) {
|
||||
const ok = await saveAdminConfig(updatedCfg);
|
||||
if (ok) {
|
||||
showToast("Config enregistrée", "Rechargez l'extension pour appliquer");
|
||||
} else {
|
||||
showAlertModal({
|
||||
title: "Erreur",
|
||||
message: "Impossible d'enregistrer la configuration.",
|
||||
buttons: [{ label: "OK", variant: "secondary", action: () => {} }]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v5.0.0 : section "Équipe" du panel admin.
|
||||
// v5.0.1 : affiche la liste complète du groupe EasyVista (20+ personnes),
|
||||
// avec case à cocher "inclure dans la planification" pour chacune.
|
||||
function renderAdminSectionTeam(container, cfg, saveFn) {
|
||||
const h = document.createElement("h3");
|
||||
h.textContent = "Équipe";
|
||||
h.className = "admin-section-title";
|
||||
container.appendChild(h);
|
||||
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "admin-section-desc";
|
||||
desc.textContent = "Sélectionnez les personnes qui doivent apparaître dans la planification. Les IDs viennent d'EasyVista (bouton Détecter) ou peuvent être saisis manuellement.";
|
||||
container.appendChild(desc);
|
||||
|
||||
// État local : liste {id, name, included, days:[0..6]}
|
||||
// Au départ on remplit depuis cfg.team actuel, puis la détection EV
|
||||
// enrichit cette liste.
|
||||
const rows = [];
|
||||
for (const [id, name] of Object.entries(cfg.team || {})) {
|
||||
rows.push({
|
||||
id,
|
||||
name,
|
||||
included: true,
|
||||
days: (cfg.recurringAbsences[id] || []).slice()
|
||||
});
|
||||
}
|
||||
|
||||
const tableWrap = document.createElement("div");
|
||||
tableWrap.className = "admin-team-wrap";
|
||||
container.appendChild(tableWrap);
|
||||
|
||||
function render() {
|
||||
tableWrap.innerHTML = "";
|
||||
|
||||
// Bouton "Détecter depuis EasyVista"
|
||||
const detectBtn = document.createElement("button");
|
||||
detectBtn.type = "button";
|
||||
detectBtn.className = "btn btn-secondary";
|
||||
detectBtn.textContent = "🔍 Détecter depuis EasyVista (groupe complet)";
|
||||
detectBtn.style.marginBottom = "12px";
|
||||
detectBtn.addEventListener("click", async () => {
|
||||
detectBtn.disabled = true;
|
||||
detectBtn.textContent = "Détection en cours…";
|
||||
try {
|
||||
const resp = await sendMessage({ type: "detectTeam" });
|
||||
if (resp && resp.ok && resp.members && resp.members.length) {
|
||||
// Merge : pour chaque membre détecté, ajoute à `rows` s'il n'y est
|
||||
// pas déjà. S'il y est déjà, met à jour le nom (si meilleur).
|
||||
for (const m of resp.members) {
|
||||
const existing = rows.find(r => r.id === m.id);
|
||||
if (existing) {
|
||||
// Améliorer le nom si le nom actuel commence par "?"
|
||||
if (m.name && !m.name.startsWith("?") && existing.name.startsWith("?")) {
|
||||
existing.name = m.name;
|
||||
}
|
||||
} else {
|
||||
rows.push({
|
||||
id: m.id,
|
||||
name: m.name || "? (" + m.id + ")",
|
||||
included: !!m.alreadyInTeam, // coché si déjà dans l'équipe
|
||||
days: []
|
||||
});
|
||||
}
|
||||
}
|
||||
showToast("Détecté", resp.members.length + " personne(s) dans le groupe");
|
||||
render();
|
||||
} else {
|
||||
showAlertModal({
|
||||
title: "Détection impossible",
|
||||
message: (resp && resp.error) || "Aucune personne trouvée. Vérifiez que vous êtes connecté à EasyVista.",
|
||||
buttons: [{ label: "OK", variant: "secondary", action: () => {} }]
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[admin] detectTeam err", err);
|
||||
} finally {
|
||||
detectBtn.disabled = false;
|
||||
detectBtn.textContent = "🔍 Détecter depuis EasyVista (groupe complet)";
|
||||
}
|
||||
});
|
||||
tableWrap.appendChild(detectBtn);
|
||||
|
||||
// Stats : nb inclus / total
|
||||
const included = rows.filter(r => r.included).length;
|
||||
const stats = document.createElement("div");
|
||||
stats.className = "admin-section-desc";
|
||||
stats.style.marginTop = "0";
|
||||
stats.textContent = `${included} personne(s) incluse(s) sur ${rows.length} connue(s).`;
|
||||
tableWrap.appendChild(stats);
|
||||
|
||||
// Table
|
||||
const table = document.createElement("table");
|
||||
table.className = "admin-team-table";
|
||||
const thead = document.createElement("thead");
|
||||
thead.innerHTML = "<tr><th>Inclure</th><th>ID</th><th>Nom affiché</th><th>Absences récurrentes</th><th></th></tr>";
|
||||
table.appendChild(thead);
|
||||
const tbody = document.createElement("tbody");
|
||||
table.appendChild(tbody);
|
||||
|
||||
const days = ["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"];
|
||||
rows.forEach((r, idx) => {
|
||||
const tr = document.createElement("tr");
|
||||
if (!r.included) tr.classList.add("admin-row-excluded");
|
||||
|
||||
// Checkbox inclure
|
||||
const tdInc = document.createElement("td");
|
||||
const cb = document.createElement("input");
|
||||
cb.type = "checkbox";
|
||||
cb.checked = r.included;
|
||||
cb.addEventListener("change", () => {
|
||||
r.included = cb.checked;
|
||||
tr.classList.toggle("admin-row-excluded", !r.included);
|
||||
stats.textContent = `${rows.filter(x => x.included).length} personne(s) incluse(s) sur ${rows.length} connue(s).`;
|
||||
});
|
||||
tdInc.appendChild(cb);
|
||||
tr.appendChild(tdInc);
|
||||
|
||||
// ID
|
||||
const tdId = document.createElement("td");
|
||||
const inpId = document.createElement("input");
|
||||
inpId.type = "text";
|
||||
inpId.value = r.id;
|
||||
inpId.placeholder = "76272";
|
||||
inpId.className = "admin-input admin-input-id";
|
||||
inpId.addEventListener("input", () => { r.id = inpId.value.trim(); });
|
||||
tdId.appendChild(inpId);
|
||||
tr.appendChild(tdId);
|
||||
|
||||
// Nom
|
||||
const tdName = document.createElement("td");
|
||||
const inpName = document.createElement("input");
|
||||
inpName.type = "text";
|
||||
inpName.value = r.name;
|
||||
inpName.placeholder = "Dupont, Jean";
|
||||
inpName.className = "admin-input";
|
||||
inpName.addEventListener("input", () => { r.name = inpName.value.trim(); });
|
||||
tdName.appendChild(inpName);
|
||||
tr.appendChild(tdName);
|
||||
|
||||
// Jours d'absence récurrente
|
||||
const tdAbs = document.createElement("td");
|
||||
for (let d = 0; d < 7; d++) {
|
||||
const lbl = document.createElement("label");
|
||||
lbl.className = "admin-day-cb";
|
||||
const cbd = document.createElement("input");
|
||||
cbd.type = "checkbox";
|
||||
cbd.checked = r.days.includes(d);
|
||||
cbd.addEventListener("change", () => {
|
||||
if (cbd.checked && !r.days.includes(d)) r.days.push(d);
|
||||
if (!cbd.checked) r.days = r.days.filter(x => x !== d);
|
||||
});
|
||||
lbl.appendChild(cbd);
|
||||
lbl.appendChild(document.createTextNode(days[d]));
|
||||
tdAbs.appendChild(lbl);
|
||||
}
|
||||
tr.appendChild(tdAbs);
|
||||
|
||||
// Bouton supprimer ligne
|
||||
const tdDel = document.createElement("td");
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.type = "button";
|
||||
delBtn.className = "admin-del-btn";
|
||||
delBtn.textContent = "🗑";
|
||||
delBtn.title = "Retirer cette ligne";
|
||||
delBtn.addEventListener("click", () => {
|
||||
rows.splice(idx, 1);
|
||||
render();
|
||||
});
|
||||
tdDel.appendChild(delBtn);
|
||||
tr.appendChild(tdDel);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
tableWrap.appendChild(table);
|
||||
|
||||
// Bouton Ajouter manuellement
|
||||
const addBtn = document.createElement("button");
|
||||
addBtn.type = "button";
|
||||
addBtn.className = "btn btn-secondary";
|
||||
addBtn.textContent = "+ Ajouter manuellement";
|
||||
addBtn.style.marginTop = "10px";
|
||||
addBtn.addEventListener("click", () => {
|
||||
rows.push({ id: "", name: "", included: true, days: [] });
|
||||
render();
|
||||
});
|
||||
tableWrap.appendChild(addBtn);
|
||||
|
||||
// Bouton Enregistrer
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.type = "button";
|
||||
saveBtn.className = "btn btn-primary";
|
||||
saveBtn.textContent = "💾 Enregistrer";
|
||||
saveBtn.style.marginTop = "20px";
|
||||
saveBtn.style.marginLeft = "10px";
|
||||
saveBtn.addEventListener("click", () => {
|
||||
// Reconstruire cfg.team et cfg.recurringAbsences à partir de rows
|
||||
const newTeam = {};
|
||||
const newRecAbs = {};
|
||||
for (const r of rows) {
|
||||
if (!r.included || !r.id) continue;
|
||||
newTeam[r.id] = r.name || ("? (" + r.id + ")");
|
||||
if (r.days && r.days.length > 0) newRecAbs[r.id] = r.days.slice();
|
||||
}
|
||||
cfg.team = newTeam;
|
||||
cfg.recurringAbsences = newRecAbs;
|
||||
saveFn();
|
||||
});
|
||||
tableWrap.appendChild(saveBtn);
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
// v5.0.0 : sections suivantes (placeholders, à enrichir v5.0.1+)
|
||||
function renderAdminSectionEV(container, cfg, saveFn) {
|
||||
const h = document.createElement("h3");
|
||||
h.textContent = "EasyVista";
|
||||
h.className = "admin-section-title";
|
||||
container.appendChild(h);
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "admin-section-desc";
|
||||
desc.textContent = "Section à venir dans v5.0.1. Origines EasyVista + group_id.";
|
||||
container.appendChild(desc);
|
||||
// Infos lecture seule pour l'instant
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "admin-readonly";
|
||||
pre.textContent = JSON.stringify({
|
||||
evOrigins: cfg.evOrigins,
|
||||
groupId: cfg.groupId
|
||||
}, null, 2);
|
||||
container.appendChild(pre);
|
||||
}
|
||||
|
||||
function renderAdminSectionAppearance(container, cfg, saveFn) {
|
||||
const h = document.createElement("h3");
|
||||
h.textContent = "Apparence";
|
||||
h.className = "admin-section-title";
|
||||
container.appendChild(h);
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "admin-section-desc";
|
||||
desc.textContent = "Section à venir dans v5.0.x. Heures journée, durée cache, thème.";
|
||||
container.appendChild(desc);
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "admin-readonly";
|
||||
pre.textContent = JSON.stringify({
|
||||
dayStart: cfg.dayStart,
|
||||
dayEnd: cfg.dayEnd,
|
||||
cacheDays: cfg.cacheDays
|
||||
}, null, 2);
|
||||
container.appendChild(pre);
|
||||
}
|
||||
|
||||
function renderAdminSectionStatuses(container, cfg, saveFn) {
|
||||
const h = document.createElement("h3");
|
||||
h.textContent = "Statuts";
|
||||
h.className = "admin-section-title";
|
||||
container.appendChild(h);
|
||||
const desc = document.createElement("p");
|
||||
desc.className = "admin-section-desc";
|
||||
desc.textContent = "Section à venir dans v5.0.x. Mots-clés Clôturé / Résolu / Annulé.";
|
||||
container.appendChild(desc);
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "admin-readonly";
|
||||
pre.textContent = JSON.stringify({
|
||||
closed: cfg.closedStatus,
|
||||
resolved: cfg.resolvedStatus,
|
||||
cancelled: cfg.cancelledStatus
|
||||
}, null, 2);
|
||||
container.appendChild(pre);
|
||||
}
|
||||
|
||||
function renderAdminSectionDiagnostics(container, cfg, saveFn) {
|
||||
const h = document.createElement("h3");
|
||||
h.textContent = "Diagnostics";
|
||||
h.className = "admin-section-title";
|
||||
container.appendChild(h);
|
||||
|
||||
const version = (chrome && chrome.runtime && chrome.runtime.getManifest)
|
||||
? chrome.runtime.getManifest().version : "?";
|
||||
|
||||
const info = document.createElement("div");
|
||||
info.className = "admin-diag-grid";
|
||||
info.innerHTML = `
|
||||
<div><strong>Version</strong></div><div>${escapeHtml(version)}</div>
|
||||
<div><strong>Date courante</strong></div><div>${escapeHtml(state.currentDate || "?")}</div>
|
||||
<div><strong>Aujourd'hui</strong></div><div>${escapeHtml(todayISO())}</div>
|
||||
<div><strong>Session EasyVista</strong></div><div>${state.session ? "✓ connecté (" + (state.session.origin || "?") + ")" : "✗ non détecté"}</div>
|
||||
<div><strong>Popups épinglées</strong></div><div>${pinnedPopups.length}</div>
|
||||
`;
|
||||
container.appendChild(info);
|
||||
|
||||
// Bouton reset
|
||||
const resetBtn = document.createElement("button");
|
||||
resetBtn.type = "button";
|
||||
resetBtn.className = "btn btn-danger";
|
||||
resetBtn.textContent = "⚠ Réinitialiser la configuration (équipe, etc.)";
|
||||
resetBtn.style.marginTop = "20px";
|
||||
resetBtn.addEventListener("click", () => {
|
||||
showAlertModal({
|
||||
title: "Confirmer la réinitialisation",
|
||||
message: "Remettre TOUTES les configurations aux valeurs par défaut ? (les techniciens ajoutés manuellement seront perdus)",
|
||||
buttons: [
|
||||
{ label: "Annuler", variant: "secondary", action: () => {} },
|
||||
{
|
||||
label: "Réinitialiser",
|
||||
variant: "danger",
|
||||
action: async () => {
|
||||
await chrome.storage.local.remove(ADMIN_CONFIG_KEY);
|
||||
showToast("Réinitialisé", "Rechargez la page pour voir les défauts");
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
container.appendChild(resetBtn);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// v4.2.6 : Modals Absence et Douchette
|
||||
// ============================================================================
|
||||
@@ -823,6 +1353,11 @@ function showAbsenceModal() {
|
||||
card.className = "modal-card modal-wide";
|
||||
card.setAttribute("role", "dialog");
|
||||
|
||||
// v5.0.0 : on mémorise la date affichée au moment de l'ouverture de la
|
||||
// modal. Le reload après création se fait sur cette date précise, pas
|
||||
// sur state.currentDate (qui aurait pu changer entre-temps).
|
||||
const dateAtOpen = state.currentDate || todayISO();
|
||||
|
||||
const title = document.createElement("h2");
|
||||
title.className = "modal-title";
|
||||
title.textContent = "Créer une absence";
|
||||
@@ -889,6 +1424,17 @@ function showAbsenceModal() {
|
||||
endGroup.appendChild(endRow);
|
||||
card.appendChild(endGroup);
|
||||
|
||||
// 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
|
||||
// changer juste le start doit mettre à jour le end aussi.
|
||||
let endDateTouched = false;
|
||||
endDate.addEventListener("input", () => { endDateTouched = true; });
|
||||
startDate.addEventListener("input", () => {
|
||||
if (!endDateTouched || endDate.value < startDate.value) {
|
||||
endDate.value = startDate.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Type d'absence
|
||||
const typeGroup = document.createElement("div");
|
||||
typeGroup.className = "modal-form-group";
|
||||
@@ -953,6 +1499,17 @@ function showAbsenceModal() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// v5.0.0 : validation fin >= début pour ne pas envoyer des absences
|
||||
// inversées à EasyVista (il les accepte mais elles n'apparaissent jamais
|
||||
// dans le planning, cf bug constaté).
|
||||
if (ed < sd || (ed === sd && et <= st)) {
|
||||
showAlertModal({
|
||||
title: "Dates incohérentes",
|
||||
message: "La date/heure de fin doit être après la date/heure de début.",
|
||||
buttons: [{ label: "OK", variant: "secondary", action: () => {} }]
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Désactiver le bouton pendant l'envoi
|
||||
applyBtn.disabled = true;
|
||||
applyBtn.textContent = "Envoi…";
|
||||
@@ -967,9 +1524,11 @@ function showAbsenceModal() {
|
||||
});
|
||||
overlay.remove();
|
||||
showToast("Absence créée", techIds.length + " tech" + (techIds.length > 1 ? "s" : ""));
|
||||
// Reload le planning du jour pour voir l'absence
|
||||
// v5.0.0 : reload le planning DE LA DATE AFFICHÉE AVANT (dateAtOpen),
|
||||
// pas de state.currentDate qui a pu être modifié entre-temps (bug
|
||||
// où le planning sautait à la date de début de l'absence).
|
||||
if (state.session) {
|
||||
await loadForDate(state.currentDate, { forceRefetch: true });
|
||||
await loadForDate(dateAtOpen, { forceRefetch: true });
|
||||
}
|
||||
} catch (err) {
|
||||
applyBtn.disabled = false;
|
||||
@@ -3414,10 +3973,13 @@ function buildCard(tech, isoDate) {
|
||||
// Timeline
|
||||
// ============================================================================
|
||||
|
||||
// v5.0.0 : constantes timeline globales (avant : locales à buildTimeline),
|
||||
// pour que updateNowLine puisse les utiliser aussi.
|
||||
const DAY_START = 8 * 60; // 08:00 en minutes
|
||||
const DAY_END = 18 * 60; // 18:00 en minutes
|
||||
const DAY_LEN = DAY_END - DAY_START;
|
||||
|
||||
function buildTimeline(realInterventions, pompierBlocks, absenceBlocks, cardEl, isPompier, isAbsent) {
|
||||
const DAY_START = 8 * 60;
|
||||
const DAY_END = 18 * 60;
|
||||
const DAY_LEN = DAY_END - DAY_START;
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "timeline";
|
||||
@@ -5577,6 +6139,50 @@ function bindTooltipInteractions() {
|
||||
}, 1200);
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else if (action === "delete-item") {
|
||||
// v5.0.0 : supprimer absence/réservation
|
||||
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: () => {} }]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5608,6 +6214,23 @@ function buildTooltipHTML(iv) {
|
||||
}
|
||||
if (iv.reservationLabel) rows.push(row("Sujet", iv.reservationLabel));
|
||||
if (iv.reservationCreator) rows.push(row("Par", iv.reservationCreator));
|
||||
// v5.0.0 : bouton supprimer pour les réservations (avec confirmation)
|
||||
rows.push(`<dt></dt><dd><button type="button" class="tooltip-delete-btn" data-action="delete-item" data-action-id="${escapeHtml(iv.actionId || "")}" data-kind="reservation">🗑 Supprimer cette réservation</button></dd>`);
|
||||
return `<dl>${rows.join("")}</dl>`;
|
||||
}
|
||||
|
||||
// v5.0.0 : cas spécial absence (congé, maladie, formation, pompier, ...)
|
||||
if (iv.type === "AL-Absence") {
|
||||
const label = iv.label || "Absence";
|
||||
rows.push(`<dt>Type</dt><dd><span class="status-pill other">${escapeHtml(label)}</span></dd>`);
|
||||
if (iv.startTime && iv.endTime) {
|
||||
rows.push(row("Horaire", `${iv.startTime}–${iv.endTime}`));
|
||||
}
|
||||
// Pour les absences récurrentes (Pillonel vendredi), pas d'actionId réel
|
||||
// → pas de bouton supprimer. Pour les autres → oui.
|
||||
if (iv.actionId) {
|
||||
rows.push(`<dt></dt><dd><button type="button" class="tooltip-delete-btn" data-action="delete-item" data-action-id="${escapeHtml(iv.actionId)}" data-kind="absence">🗑 Supprimer cette absence</button></dd>`);
|
||||
}
|
||||
return `<dl>${rows.join("")}</dl>`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user