| <!DOCTYPE html> |
| <html lang="fr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>BuSCA - Veille Sanitaire 2026</title> |
| |
| |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
| |
| <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"> |
|
|
| <style> |
| :root { |
| --primary: #1E88E5; --dark: #2E3B4E; --light: #f8f9fa; |
| --white: #fff; --grey: #e0e0e0; --success: #28a745; --danger: #dc3545; |
| } |
| * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Roboto', sans-serif; } |
| body { background: var(--light); color: #333; display: flex; height: 100vh; overflow: hidden; } |
| |
| |
| aside { |
| width: 280px; background: var(--dark); color: #ccc; |
| display: flex; flex-direction: column; border-right: 1px solid #444; |
| flex-shrink: 0; |
| } |
| .brand { padding: 15px; border-bottom: 1px solid #444; display: flex; align-items: center; gap: 10px; color: white; } |
| .brand img { width: 35px; border-radius: 5px; } |
| |
| .controls { padding: 15px; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; } |
| |
| button, .btn-upload { |
| background: #3a4a60; border: none; color: white; padding: 10px; border-radius: 5px; |
| cursor: pointer; display: flex; align-items: center; gap: 10px; font-size: 0.9em; |
| transition: 0.2s; text-decoration: none; justify-content: flex-start; |
| } |
| button:hover, .btn-upload:hover { background: #4a5a70; transform: translateX(3px); } |
| button:disabled { opacity: 0.5; cursor: not-allowed; } |
| |
| .btn-primary { background: var(--primary); } |
| .btn-success { background: var(--success); } |
| .btn-warning { background: #ffc107; color: #333; animation: pulse 2s infinite; } |
| |
| @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(255, 193, 7, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); } } |
| |
| |
| .filter-box { margin-top: 15px; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 5px; } |
| .filter-box label { display: block; margin-bottom: 5px; font-size: 0.85em; color: #aaa; } |
| textarea { |
| width: 100%; background: #222; border: 1px solid #555; color: white; padding: 8px; |
| border-radius: 4px; resize: vertical; min-height: 80px; |
| } |
| textarea:focus { outline: 1px solid var(--primary); } |
| |
| |
| main { flex: 1; display: flex; flex-direction: column; overflow: hidden; } |
| |
| header { |
| height: 60px; background: white; border-bottom: 1px solid var(--grey); |
| display: flex; align-items: center; padding: 0 20px; font-size: 1.2em; font-weight: 500; |
| } |
| |
| .scroll-area { flex: 1; overflow-y: auto; padding: 20px; } |
| |
| |
| .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin-bottom: 20px; } |
| .stat-item { background: white; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); border-top: 4px solid var(--primary); } |
| .stat-val { font-size: 1.8em; font-weight: bold; color: var(--dark); } |
| .stat-label { font-size: 0.8em; color: #666; } |
| |
| |
| .card { background: white; border-radius: 8px; margin-bottom: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); overflow: hidden; } |
| .card-header { |
| padding: 15px; background: linear-gradient(to right, #1E88E5, #42A5F5); color: white; |
| cursor: pointer; display: flex; justify-content: space-between; align-items: center; |
| } |
| .card-body { padding: 20px; display: none; border-top: 1px solid #eee; } |
| .card-body.open { display: block; animation: fadeIn 0.3s; } |
| @keyframes fadeIn { from { opacity:0; transform:translateY(-5px); } to { opacity:1; transform:translateY(0); } } |
| |
| .tags { margin-bottom: 15px; } |
| .tag { padding: 3px 8px; border-radius: 4px; font-size: 0.85em; margin-right: 5px; display: inline-block; } |
| .tag-danger { background: #ffebee; color: #c62828; border: 1px solid #ffcdd2; } |
| .tag-matrix { background: #e3f2fd; color: #1565C0; border: 1px solid #bbdefb; } |
| |
| |
| .loader { text-align: center; padding: 50px; color: #666; } |
| .msg-box { padding: 15px; border-radius: 5px; margin-bottom: 20px; text-align: center; } |
| .msg-error { background: #fff3cd; color: #856404; border: 1px solid #ffeeba; } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <aside> |
| <div class="brand"> |
| <i class="fas fa-search-plus"></i> <span>BuSCA 2026</span> |
| </div> |
| |
| <div class="controls"> |
| |
| <button id="btnWeb" class="btn-primary"> |
| <i class="fas fa-cloud-download-alt"></i> Web Auto (Proxy) |
| </button> |
| |
| |
| <label for="inputFile" id="btnManual" class="btn-upload"> |
| <i class="fas fa-folder-open"></i> Ouvrir Fichier Excel |
| </label> |
| <input type="file" id="inputFile" accept=".xlsx, .xls" style="display:none;"> |
|
|
| |
| <div class="filter-box"> |
| <label><i class="fas fa-filter"></i> Filtrer par mots-clés :</label> |
| <textarea id="txtFilter" placeholder="ex: Salmonella, Lait... (séparés par virgule)"></textarea> |
| <button id="btnApply" class="btn-success" style="width:100%; margin-top:10px; justify-content:center;"> |
| <i class="fas fa-check"></i> Appliquer |
| </button> |
| <button id="btnClear" style="width:100%; margin-top:5px; justify-content:center; background: #555;"> |
| <i class="fas fa-eraser"></i> Effacer |
| </button> |
| </div> |
|
|
| <div style="margin-top:auto; border-top:1px solid #444; padding-top:10px;"> |
| <button id="btnExport" style="width:100%"><i class="fas fa-file-csv"></i> Exporter CSV</button> |
| </div> |
| </div> |
| </aside> |
|
|
| |
| <main> |
| <header> |
| <div id="headerTitle">Veille Sanitaire</div> |
| </header> |
|
|
| <div class="scroll-area"> |
| |
| <div class="stats" id="statsArea"></div> |
| |
| |
| <div id="resultsArea"> |
| <div class="loader"> |
| <i class="fas fa-info-circle"></i><br><br> |
| Chargez les données via le bouton bleu (Web) ou gris (Fichier). |
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| <script> |
| // --- VARIABLES GLOBALES --- |
| let allData = []; // Toutes les données chargées |
| let filteredData = []; // Données affichées après filtre |
| |
| // URLS |
| const URL_SOURCE = 'https://www.plateforme-sca.fr/media/398/download'; |
| const URL_PROXY = `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(URL_SOURCE)}`; |
| |
| // --- INIT --- |
| document.addEventListener('DOMContentLoaded', () => { |
| setupEvents(); |
| // On tente le chargement auto au démarrage |
| fetchDataWeb(); |
| }); |
| |
| // --- 1. CHARGEMENT DES DONNÉES --- |
| |
| // Méthode Web (Proxy) |
| async function fetchDataWeb() { |
| showLoader("Connexion au serveur SCA..."); |
| try { |
| const response = await fetch(URL_PROXY); |
| if (!response.ok) throw new Error("Erreur Proxy"); |
| const buffer = await response.arrayBuffer(); |
| parseExcel(buffer); |
| } catch (e) { |
| console.warn(e); |
| showError(); // Affiche le message pour passer en manuel |
| } |
| } |
| |
| // Méthode Manuelle (Fichier Local) |
| function handleFile(e) { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| showLoader("Lecture du fichier..."); |
| const reader = new FileReader(); |
| reader.onload = (evt) => parseExcel(evt.target.result); |
| reader.readAsArrayBuffer(file); |
| e.target.value = ''; // Reset input |
| } |
| |
| // Lecture Excel commune |
| function parseExcel(buffer) { |
| try { |
| const wb = XLSX.read(buffer, {type: 'array'}); |
| const ws = wb.Sheets[wb.SheetNames[0]]; |
| const json = XLSX.utils.sheet_to_json(ws); |
| |
| if (!json || json.length === 0) throw new Error("Fichier vide"); |
| |
| // Mapping des colonnes |
| allData = json.map(row => ({ |
| buscA: parseInt(row['BuSCA'] || 0), |
| titre: String(row['Titre'] || '').trim(), |
| texte: String(row['Texte'] || '').trim(), |
| matrice: String(row['Matrices'] || row['Matrice'] || 'Non spécifié').trim(), |
| danger: String(row['Dangers'] || row['Danger'] || 'Non spécifié').trim(), |
| lien1: row['Lien'] || '', |
| lien2: row['Lien2'] || '' |
| })).filter(x => x.buscA > 0 && x.titre !== ""); |
| |
| // Initialisation : Tout afficher au début |
| filteredData = [...allData]; |
| |
| // Réinitialiser les filtres visuels |
| document.getElementById('txtFilter').value = ''; |
| |
| // Mettre à jour l'interface |
| document.getElementById('btnManual').classList.remove('btn-warning'); // Enlever l'alerte jaune |
| updateUI(); |
| alert(`${allData.length} bulletins chargés avec succès !`); |
| |
| } catch (err) { |
| console.error(err); |
| document.getElementById('resultsArea').innerHTML = `<div class="msg-box msg-error">Erreur de lecture du fichier Excel.<br>Vérifiez le format.</div>`; |
| } |
| } |
| |
| // --- 2. GESTION DES FILTRES --- |
| |
| function applyFilters() { |
| const input = document.getElementById('txtFilter').value; |
| |
| // On découpe par virgule, on enlève les espaces, on met en minuscule |
| const keywords = input.toLowerCase().split(',').map(k => k.trim()).filter(k => k !== ''); |
| |
| if (keywords.length === 0) { |
| // Si vide, on remet tout |
| filteredData = [...allData]; |
| } else { |
| // Sinon on filtre |
| filteredData = allData.filter(item => { |
| // On crée une grande chaîne de texte avec tout le contenu de l'item |
| const fullText = (item.titre + " " + item.texte + " " + item.matrice + " " + item.danger).toLowerCase(); |
| |
| // On vérifie que TOUS les mots clés sont présents |
| return keywords.every(key => fullText.includes(key)); |
| }); |
| } |
| updateUI(); |
| } |
| |
| // --- 3. AFFICHAGE (RENDERING) --- |
| |
| function updateUI() { |
| const container = document.getElementById('resultsArea'); |
| const stats = document.getElementById('statsArea'); |
| |
| // Mise à jour des Stats |
| const nbBulletins = filteredData.length; |
| const nbMatrices = new Set(filteredData.map(i => i.matrice)).size; |
| const nbDangers = new Set(filteredData.map(i => i.danger)).size; |
| |
| stats.innerHTML = ` |
| <div class="stat-item"><div class="stat-val">${nbBulletins}</div><div class="stat-label">Bulletins</div></div> |
| <div class="stat-item" style="border-color:#17a2b8"><div class="stat-val">${nbMatrices}</div><div class="stat-label">Matrices</div></div> |
| <div class="stat-item" style="border-color:#ffc107"><div class="stat-val">${nbDangers}</div><div class="stat-label">Dangers</div></div> |
| `; |
| |
| // Affichage Liste |
| if (filteredData.length === 0) { |
| container.innerHTML = `<div class="loader">Aucun résultat trouvé pour cette recherche.</div>`; |
| return; |
| } |
| |
| // Tri par N° BuSCA décroissant |
| const sorted = filteredData.sort((a,b) => b.buscA - a.buscA); |
| |
| container.innerHTML = sorted.map(item => ` |
| <article class="card"> |
| <div class="card-header" onclick="this.nextElementSibling.classList.toggle('open')"> |
| <div style="font-weight:bold;">${item.titre}</div> |
| <div style="font-size:0.8em; background:rgba(255,255,255,0.2); padding:2px 6px; border-radius:4px;">N° ${item.buscA}</div> |
| </div> |
| <div class="card-body"> |
| <div class="tags"> |
| <span class="tag tag-matrix"><i class="fas fa-vial"></i> ${item.matrice}</span> |
| <span class="tag tag-danger"><i class="fas fa-biohazard"></i> ${item.danger}</span> |
| </div> |
| <p style="line-height:1.6; color:#444; margin-bottom:15px;">${item.texte.replace(/\n/g, '<br>')}</p> |
| <div style="border-top:1px dashed #ddd; padding-top:10px;"> |
| ${item.lien1 ? `<a href="${item.lien1}" target="_blank" style="color:var(--primary); font-weight:bold; margin-right:15px; text-decoration:none;">🔗 Lien Principal</a>` : ''} |
| ${item.lien2 ? `<a href="${item.lien2}" target="_blank" style="color:#555; text-decoration:none;">🔗 Lien Secondaire</a>` : ''} |
| </div> |
| </div> |
| </article> |
| `).join(''); |
| } |
| |
| // --- 4. UTILITAIRES --- |
| |
| function setupEvents() { |
| document.getElementById('btnWeb').onclick = fetchDataWeb; |
| document.getElementById('inputFile').onchange = handleFile; |
| |
| // Filtres |
| document.getElementById('btnApply').onclick = applyFilters; |
| document.getElementById('btnClear').onclick = () => { |
| document.getElementById('txtFilter').value = ''; |
| applyFilters(); |
| }; |
| |
| // Touche Entrée dans la zone de texte |
| document.getElementById('txtFilter').addEventListener('keyup', (e) => { |
| if (e.key === 'Enter') applyFilters(); |
| }); |
| |
| // Export |
| document.getElementById('btnExport').onclick = () => { |
| if(!filteredData.length) return alert("Rien à exporter"); |
| const csv = ["BuSCA,Titre,Matrice,Danger,Texte"].concat( |
| filteredData.map(d => `${d.buscA},"${d.titre}","${d.matrice}","${d.danger}","${d.texte.replace(/"/g,'""')}"`) |
| ).join('\n'); |
| const a = document.createElement('a'); |
| a.href = URL.createObjectURL(new Blob([csv], {type:'text/csv'})); |
| a.download = 'busca_export.csv'; |
| a.click(); |
| }; |
| } |
| |
| function showLoader(msg) { |
| document.getElementById('resultsArea').innerHTML = `<div class="loader"><i class="fas fa-circle-notch fa-spin fa-2x"></i><br><br>${msg}</div>`; |
| } |
| |
| function showError() { |
| document.getElementById('btnManual').classList.add('btn-warning'); |
| document.getElementById('resultsArea').innerHTML = ` |
| <div class="msg-box msg-error"> |
| <strong>Accès Web Bloqué (Sécurité Serveur)</strong><br><br> |
| Le serveur SCA refuse l'accès automatique.<br> |
| 1. <a href="${URL_SOURCE}" target="_blank">Téléchargez le fichier Excel ici</a><br> |
| 2. Cliquez sur le bouton jaune <strong>"Ouvrir Fichier Excel"</strong> à gauche. |
| </div> |
| `; |
| } |
| </script> |
| </body> |
| </html> |