BUSCAv2 / index.html
MMOON's picture
Update index.html
1d75e1e verified
<!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>
<!-- SheetJS pour Excel -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<!-- FontAwesome & Google Fonts -->
<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; }
/* --- SIDEBAR --- */
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); } }
/* ZONE FILTRES */
.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 CONTENT --- */
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 */
.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; }
/* CARDS */
.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; }
/* NOTIFICATIONS & LOADERS */
.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>
<!-- BARRE LATERALE -->
<aside>
<div class="brand">
<i class="fas fa-search-plus"></i> <span>BuSCA 2026</span>
</div>
<div class="controls">
<!-- Bouton Web -->
<button id="btnWeb" class="btn-primary">
<i class="fas fa-cloud-download-alt"></i> Web Auto (Proxy)
</button>
<!-- Bouton Manuel -->
<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;">
<!-- Section Filtres -->
<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>
<!-- CONTENU PRINCIPAL -->
<main>
<header>
<div id="headerTitle">Veille Sanitaire</div>
</header>
<div class="scroll-area">
<!-- Statistiques -->
<div class="stats" id="statsArea"></div>
<!-- Résultats -->
<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>