|
|
<!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> |