diff --git a/background.js b/background.js new file mode 100644 index 0000000..0cccf61 --- /dev/null +++ b/background.js @@ -0,0 +1,68 @@ +// background.js — Service worker (Manifest V3) +// +// Quand l'utilisateur clique sur l'icône de l'extension : +// 1. On vérifie qu'il est bien sur itsma.vd.ch +// 2. On injecte un script dans l'onglet qui récupère le HTML complet +// de la page + tous les liens vers les fiches d'intervention +// 3. On stocke ça dans chrome.storage.local +// 4. On ouvre un nouvel onglet avec viewer.html + +chrome.action.onClicked.addListener(async (tab) => { + try { + if (!tab.url || !tab.url.startsWith("https://itsma.vd.ch/")) { + await chrome.storage.local.set({ + planningError: + "Cette extension ne fonctionne que sur https://itsma.vd.ch/. " + + "Va d'abord sur la page du planning, puis reclique sur l'icône." + }); + await openViewer(); + return; + } + + // Injecter un script dans l'onglet pour extraire le HTML + const results = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: extractPlanningFromPage + }); + + const data = results[0]?.result; + if (!data || !data.html) { + await chrome.storage.local.set({ + planningError: + "Impossible de lire le contenu de la page. " + + "Assure-toi d'être bien sur la page du planning des techniciens." + }); + await openViewer(); + return; + } + + // Stocker le HTML capturé + l'URL de base pour les requêtes futures + await chrome.storage.local.set({ + planningHtml: data.html, + planningUrl: tab.url, + planningCapturedAt: Date.now(), + planningError: null + }); + + await openViewer(); + } catch (err) { + console.error("Erreur extension:", err); + await chrome.storage.local.set({ + planningError: "Erreur inattendue : " + (err?.message || String(err)) + }); + await openViewer(); + } +}); + +// Fonction injectée dans la page du planning +// (elle s'exécute dans le contexte de itsma.vd.ch, pas dans le service worker) +function extractPlanningFromPage() { + // Récupérer tout le HTML de la page + const html = document.documentElement.outerHTML; + return { html: html }; +} + +async function openViewer() { + const viewerUrl = chrome.runtime.getURL("viewer.html"); + await chrome.tabs.create({ url: viewerUrl }); +} diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..87db8b0 Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..b4fff8c Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..8e50109 Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f4cb770 --- /dev/null +++ b/manifest.json @@ -0,0 +1,37 @@ +{ + "manifest_version": 3, + "name": "Planning Techniciens — Vue claire", + "version": "1.0.0", + "description": "Réaffiche le planning du jour (itsma.vd.ch) avec pompier, absents et détails d'interventions à la demande.", + "permissions": [ + "activeTab", + "scripting", + "storage" + ], + "host_permissions": [ + "https://itsma.vd.ch/*" + ], + "action": { + "default_title": "Ouvrir la vue claire du planning" + }, + "background": { + "service_worker": "background.js" + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "web_accessible_resources": [ + { + "resources": [ + "viewer.html", + "viewer.js", + "viewer.css" + ], + "matches": [ + "https://itsma.vd.ch/*" + ] + } + ] +} diff --git a/viewer.css b/viewer.css new file mode 100644 index 0000000..867ecf0 --- /dev/null +++ b/viewer.css @@ -0,0 +1,360 @@ +/* viewer.css — Style de la vue claire du planning */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: #f5f7fa; + color: #1f2937; + line-height: 1.5; + min-height: 100vh; + padding-bottom: 40px; +} + +.hidden { + display: none !important; +} + +/* === Top bar === */ +.topbar { + background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); + color: white; + padding: 20px 32px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + flex-wrap: wrap; + gap: 16px; +} + +.topbar h1 { + font-size: 24px; + font-weight: 600; +} + +.subtitle { + font-size: 13px; + opacity: 0.85; + margin-top: 4px; +} + +.topbar-right { + display: flex; + gap: 12px; +} + +.topbar button { + background: rgba(255,255,255,0.15); + color: white; + border: 1px solid rgba(255,255,255,0.3); + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background 0.2s; +} + +.topbar button:hover { + background: rgba(255,255,255,0.25); +} + +.topbar button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* === Error zone === */ +.error-zone { + margin: 24px 32px; + padding: 16px 20px; + background: #fef2f2; + border: 1px solid #fca5a5; + color: #991b1b; + border-radius: 8px; +} + +/* === Summary cards (pompier, absents, stats) === */ +.summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; + margin: 24px 32px; +} + +.summary-card { + background: white; + padding: 18px 22px; + border-radius: 10px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + border-left: 4px solid #e5e7eb; +} + +.summary-card.summary-pompier { + border-left-color: #dc2626; +} + +.summary-card.summary-absents { + border-left-color: #f59e0b; +} + +.summary-card.summary-stats { + border-left-color: #10b981; +} + +.summary-label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #6b7280; + font-weight: 600; + margin-bottom: 6px; +} + +.summary-value { + font-size: 20px; + font-weight: 600; + color: #111827; +} + +.summary-value .muted { + font-weight: 400; + color: #6b7280; + font-size: 14px; +} + +/* === Main content : cartes techniciens === */ +#main-content { + margin: 0 32px; +} + +.placeholder { + background: white; + padding: 48px 32px; + border-radius: 10px; + text-align: center; + color: #9ca3af; + font-size: 16px; +} + +.tech-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 20px; +} + +.tech-card { + background: white; + border-radius: 10px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.tech-card.tech-absent { + opacity: 0.7; + background: #fafafa; +} + +.tech-card.tech-pompier { + box-shadow: 0 0 0 2px #dc2626, 0 2px 6px rgba(220,38,38,0.2); +} + +.tech-header { + padding: 14px 18px; + background: #f9fafb; + border-bottom: 1px solid #e5e7eb; + display: flex; + justify-content: space-between; + align-items: center; +} + +.tech-card.tech-pompier .tech-header { + background: #fef2f2; +} + +.tech-card.tech-absent .tech-header { + background: #fef3c7; +} + +.tech-name { + font-weight: 600; + font-size: 16px; + color: #111827; +} + +.tech-badge { + font-size: 11px; + padding: 3px 8px; + border-radius: 12px; + font-weight: 600; + text-transform: uppercase; +} + +.tech-badge.badge-pompier { + background: #dc2626; + color: white; +} + +.tech-badge.badge-absent { + background: #f59e0b; + color: white; +} + +.tech-badge.badge-count { + background: #e5e7eb; + color: #4b5563; +} + +.tech-interventions { + padding: 8px 0; + flex: 1; +} + +.tech-empty { + padding: 24px; + text-align: center; + color: #9ca3af; + font-style: italic; + font-size: 14px; +} + +/* === Intervention items === */ +.intervention { + padding: 10px 18px; + border-bottom: 1px solid #f3f4f6; + cursor: pointer; + transition: background 0.15s; + position: relative; +} + +.intervention:last-child { + border-bottom: none; +} + +.intervention:hover { + background: #eff6ff; +} + +.interv-header { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; +} + +.interv-time { + font-weight: 600; + color: #1e40af; + font-size: 14px; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.interv-ref { + font-size: 12px; + color: #6b7280; + font-family: ui-monospace, "SF Mono", Consolas, monospace; +} + +.interv-summary { + font-size: 13px; + color: #374151; + margin-top: 4px; + line-height: 1.4; +} + +.interv-contact { + font-weight: 500; +} + +.interv-location { + color: #6b7280; +} + +.interv-details { + margin-top: 12px; + padding: 12px; + background: #f9fafb; + border-radius: 6px; + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + max-height: 400px; + overflow-y: auto; +} + +.interv-details.loading { + color: #9ca3af; + font-style: italic; +} + +.interv-details.error { + color: #991b1b; + background: #fef2f2; +} + +.interv-open-link { + display: inline-block; + margin-top: 8px; + font-size: 12px; + color: #2563eb; + text-decoration: none; +} + +.interv-open-link:hover { + text-decoration: underline; +} + +/* === Absence item (affichée dans la carte du tech absent) === */ +.absence-info { + padding: 12px 18px; + font-size: 14px; + color: #78350f; + background: #fef3c7; + border-left: 3px solid #f59e0b; +} + +.pompier-info { + padding: 12px 18px; + font-size: 14px; + color: #7f1d1d; + background: #fee2e2; + border-left: 3px solid #dc2626; + font-weight: 500; +} + +/* === Tooltip flottant === */ +.tooltip { + position: fixed; + background: #1f2937; + color: white; + padding: 12px 14px; + border-radius: 6px; + font-size: 13px; + line-height: 1.5; + max-width: 400px; + z-index: 1000; + pointer-events: none; + white-space: pre-wrap; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); +} + +/* === Responsive === */ +@media (max-width: 768px) { + .topbar { + padding: 16px 20px; + } + .summary, #main-content { + margin: 20px 16px; + } + .tech-grid { + grid-template-columns: 1fr; + } +} diff --git a/viewer.html b/viewer.html new file mode 100644 index 0000000..64a3d26 --- /dev/null +++ b/viewer.html @@ -0,0 +1,50 @@ + + +
+ +