MMOON commited on
Commit
fba1739
·
verified ·
1 Parent(s): 2c01a53

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +114 -35
index.html CHANGED
@@ -6,7 +6,9 @@
6
  <title>BuSCA - Veille Sanitaire</title>
7
 
8
  <!-- Bibliothèques externes -->
 
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
 
10
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
11
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
12
 
@@ -133,6 +135,7 @@
133
  </head>
134
  <body>
135
  <div class="app-container">
 
136
  <aside class="sidebar" id="sidebar">
137
  <div class="sidebar-header">
138
  <div class="logo-title-group">
@@ -159,13 +162,14 @@
159
  </div>
160
  </aside>
161
 
 
162
  <div class="content-wrapper">
163
  <div class="top-bar">
164
  <button class="open-sidebar-btn" id="openSidebarBtn" aria-label="Ouvrir le menu">☰</button>
165
  <h1 class="top-bar-title">Veille Sanitaire BuSCA</h1>
166
  </div>
167
  <main class="main-content">
168
- <div class="alert"><strong>Bienvenue !</strong> Les données sont chargées directement depuis la Plateforme SCA. Cliquez sur un bulletin pour voir les détails.</div>
169
  <section class="stats-container" id="statsContainer"></section>
170
  <section class="results-section">
171
  <h2>📑 Derniers bulletins</h2>
@@ -177,6 +181,7 @@
177
 
178
  <script>
179
  document.addEventListener('DOMContentLoaded', () => {
 
180
  const ui = {
181
  sidebar: document.getElementById('sidebar'),
182
  openSidebarBtn: document.getElementById('openSidebarBtn'),
@@ -191,49 +196,78 @@
191
  keywords: document.getElementById('keywordsFilter'),
192
  }
193
  };
194
- // --- MODIFICATION ICI ---
195
- const REAL_DATA_URL = 'https://www.plateforme-sca.fr/sites/default/files/2025-11/BuSCA-Base2.xlsx';
 
 
 
196
  const PROXY_URL = `https://corsproxy.io/?${encodeURIComponent(REAL_DATA_URL)}`;
 
197
  let allData = [];
198
  let filteredData = [];
 
 
199
  function initializeApp() {
200
  setupEventListeners();
201
  updateData();
202
  }
 
 
203
  async function updateData() {
204
- ui.resultsContainer.innerHTML = '<div class="loading-overlay">Chargement des données...</div>';
205
  ui.updateDataBtn.disabled = true;
206
  ui.updateDataBtn.querySelector('i').classList.add('fa-spin');
 
207
  try {
 
208
  const response = await fetch(PROXY_URL);
209
  if (!response.ok) {
210
  throw new Error(`Erreur réseau: ${response.status} ${response.statusText}`);
211
  }
 
 
212
  const arrayBuffer = await response.arrayBuffer();
213
  const workbook = XLSX.read(arrayBuffer, { type: 'array' });
214
- const worksheet = workbook.Sheets[workbook.SheetNames[0]];
 
 
 
 
 
215
  const rawJsonData = XLSX.utils.sheet_to_json(worksheet);
 
216
  if (!rawJsonData || !Array.isArray(rawJsonData)) {
217
- throw new Error("Les données reçues sont invalides ou vides.");
218
  }
219
- // --- CORRECTION MAJEURE ICI ---
220
- // On utilise les nouveaux noms de colonnes 'Matrices' et 'Dangers' du fichier Excel
221
- allData = rawJsonData.map(item => ({
222
- 'BuSCA': parseInt(item['BuSCA'], 10) || 0,
223
- 'Titre': String(item['Titre'] || '').trim(),
224
- 'Texte': String(item['Texte'] || '').trim(),
225
- 'Matrice': String(item['Matrices'] || 'Non spécifié').trim(), // Lecture de 'Matrices'
226
- 'Danger': String(item['Dangers'] || 'Non spécifié').trim(), // Lecture de 'Dangers'
227
- 'Section': String(item['Section'] || 'Non spécifié').trim(),
228
- 'Lien': String(item['Lien'] || '').trim(),
229
- 'Lien2': String(item['Lien2'] || '').trim()
230
- })).filter(item => item.BuSCA > 0 && item.Titre);
231
-
 
 
 
 
232
  showNotification('Données mises à jour avec succès !', 'success');
233
  processData();
 
234
  } catch (error) {
235
  console.error('Erreur lors de la mise à jour:', error);
236
- ui.resultsContainer.innerHTML = `<div class="empty-state" style="color: red;">Échec du chargement des données.<br>${error.message}</div>`;
 
 
 
 
 
 
237
  showNotification('Erreur de mise à jour.', 'error');
238
  } finally {
239
  ui.updateDataBtn.disabled = false;
@@ -241,18 +275,20 @@
241
  }
242
  }
243
 
 
244
  function processData() {
245
  applyFilters();
246
  }
 
247
  function applyFilters() {
248
  const criteria = {
249
  keywords: ui.filters.keywords.value.toLowerCase().split(',').map(k => k.trim()).filter(Boolean)
250
  };
 
251
  if (criteria.keywords.length === 0) {
252
  filteredData = allData;
253
  } else {
254
  filteredData = allData.filter(item => {
255
- // On cherche dans les clés internes 'Danger' et 'Matrice'
256
  const itemText = (item.Titre + ' ' + item.Texte + ' ' + item.Danger + ' ' + item.Matrice).toLowerCase();
257
  return criteria.keywords.every(k => itemText.includes(k));
258
  });
@@ -267,27 +303,35 @@
267
  applyFilters();
268
  showNotification('Filtres réinitialisés.', 'info');
269
  }
 
270
  function displayResults() {
271
  if (filteredData.length === 0) {
272
  ui.resultsContainer.innerHTML = '<div class="empty-state">Aucun bulletin ne correspond à vos critères.</div>';
273
  return;
274
  }
 
 
275
  const sortedData = [...filteredData].sort((a, b) => b.BuSCA - a.BuSCA);
276
- // On utilise les clés internes 'Matrice' et 'Danger' pour l'affichage
277
  ui.resultsContainer.innerHTML = sortedData.map(item => `
278
  <article class="bulletin-card">
279
  <div class="bulletin-header" data-toggle="content">
280
- <h3>${item.Titre || 'Titre non disponible'}</h3>
281
  <span class="bulletin-number">BuSCA n°${item.BuSCA}</span>
282
  </div>
283
  <div class="bulletin-content">
284
- <p class="bulletin-summary"><strong>Texte:</strong><br>${item.Texte.replace(/\n/g, '<br>') || 'Contenu non disponible.'}</p>
285
  <div class="bulletin-meta">
286
- <strong>Matrice:</strong> ${item.Matrice} | <strong>Danger:</strong> ${item.Danger}
 
 
 
 
 
287
  </div>
288
- <div class="bulletin-links">
289
- ${item.Lien ? `<a href="${item.Lien}" target="_blank" rel="noopener noreferrer">🔗 Lien 1</a>` : ''}
290
- ${item.Lien2 ? `<a href="${item.Lien2}" target="_blank" rel="noopener noreferrer" style="margin-left: 10px;">🔗 Lien 2</a>` : ''}
 
291
  </div>
292
  </div>
293
  </article>`).join('');
@@ -298,28 +342,45 @@
298
  const foundBulletins = filteredData.length;
299
  const uniqueMatrices = new Set(filteredData.map(i => i.Matrice)).size;
300
  const uniqueDangers = new Set(filteredData.map(i => i.Danger)).size;
 
301
  ui.statsContainer.innerHTML = `
302
- <div class="stat-card"><div class="stat-number">${foundBulletins} / ${totalBulletins}</div><div class="stat-label">Bulletins Trouvés</div></div>
303
  <div class="stat-card"><div class="stat-number">${uniqueMatrices}</div><div class="stat-label">Matrices Concernées</div></div>
304
  <div class="stat-card"><div class="stat-number">${uniqueDangers}</div><div class="stat-label">Dangers Identifiés</div></div>
305
  `;
306
  }
 
 
307
  function setupEventListeners() {
308
  ui.openSidebarBtn.addEventListener('click', () => ui.sidebar.classList.remove('collapsed'));
309
  ui.sidebarToggle.addEventListener('click', () => ui.sidebar.classList.add('collapsed'));
 
310
  ui.updateDataBtn.addEventListener('click', updateData);
 
311
  ui.applyFiltersBtn.addEventListener('click', () => {
312
  applyFilters();
313
  showNotification('Filtres appliqués !', 'success');
314
  });
 
315
  ui.clearFiltersBtn.addEventListener('click', clearFilters);
316
  ui.exportCsvBtn.addEventListener('click', exportToCSV);
317
 
 
318
  ui.resultsContainer.addEventListener('click', (e) => {
319
  const header = e.target.closest('[data-toggle="content"]');
320
- if (header) header.nextElementSibling.classList.toggle('active');
 
 
 
 
 
 
 
 
 
321
  });
322
 
 
323
  document.addEventListener('keydown', (e) => {
324
  if (e.key === 'Escape' && !ui.sidebar.classList.contains('collapsed')) {
325
  ui.sidebar.classList.add('collapsed');
@@ -330,42 +391,60 @@
330
  }
331
  });
332
  }
 
333
  function showNotification(message, type = 'info') {
334
  const notification = document.createElement('div');
335
  notification.className = `notification-toast ${type}`;
336
  notification.textContent = message;
337
  document.body.appendChild(notification);
 
 
338
  requestAnimationFrame(() => notification.classList.add('show'));
 
339
  setTimeout(() => {
340
  notification.classList.remove('show');
341
  notification.addEventListener('transitionend', () => notification.remove());
342
  }, 3000);
343
  }
 
344
  function exportToCSV() {
345
  if (filteredData.length === 0) {
346
  showNotification('Aucune donnée à exporter', 'error');
347
  return;
348
  }
 
349
  const headers = ['BuSCA', 'Titre', 'Matrice', 'Danger', 'Texte', 'Lien', 'Lien2'];
350
  const escapeCsv = (str) => `"${String(str || '').replace(/"/g, '""')}"`;
 
351
  const csvContent = [
352
  headers.join(','),
353
  ...filteredData.map(item => [
354
- item.BuSCA, escapeCsv(item.Titre), escapeCsv(item.Matrice),
355
- escapeCsv(item.Danger), escapeCsv(item.Texte),
356
- escapeCsv(item.Lien), escapeCsv(item.Lien2)
 
 
 
 
357
  ].join(','))
358
  ].join('\n');
 
359
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
360
  const link = document.createElement('a');
361
- link.href = URL.createObjectURL(blob);
362
- link.download = `busca_export_${new Date().toISOString().split('T')[0]}.csv`;
 
 
 
 
363
  document.body.appendChild(link);
364
  link.click();
365
  document.body.removeChild(link);
 
366
  showNotification('Export CSV généré !', 'success');
367
  }
368
 
 
369
  initializeApp();
370
  });
371
  </script>
 
6
  <title>BuSCA - Veille Sanitaire</title>
7
 
8
  <!-- Bibliothèques externes -->
9
+ <!-- SheetJS pour lire le fichier Excel -->
10
  <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
11
+ <!-- Polices et Icônes -->
12
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
13
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
14
 
 
135
  </head>
136
  <body>
137
  <div class="app-container">
138
+ <!-- BARRE LATÉRALE -->
139
  <aside class="sidebar" id="sidebar">
140
  <div class="sidebar-header">
141
  <div class="logo-title-group">
 
162
  </div>
163
  </aside>
164
 
165
+ <!-- CONTENU PRINCIPAL -->
166
  <div class="content-wrapper">
167
  <div class="top-bar">
168
  <button class="open-sidebar-btn" id="openSidebarBtn" aria-label="Ouvrir le menu">☰</button>
169
  <h1 class="top-bar-title">Veille Sanitaire BuSCA</h1>
170
  </div>
171
  <main class="main-content">
172
+ <div class="alert"><strong>Info :</strong> Les données sont chargées depuis le fichier "Download" de la plateforme SCA.</div>
173
  <section class="stats-container" id="statsContainer"></section>
174
  <section class="results-section">
175
  <h2>📑 Derniers bulletins</h2>
 
181
 
182
  <script>
183
  document.addEventListener('DOMContentLoaded', () => {
184
+ // --- ÉLÉMENTS UI ---
185
  const ui = {
186
  sidebar: document.getElementById('sidebar'),
187
  openSidebarBtn: document.getElementById('openSidebarBtn'),
 
196
  keywords: document.getElementById('keywordsFilter'),
197
  }
198
  };
199
+
200
+ // --- CONFIGURATION DE L'URL ---
201
+ // Nouvelle URL de téléchargement direct
202
+ const REAL_DATA_URL = 'https://www.plateforme-sca.fr/media/398/download';
203
+ // Utilisation d'un proxy CORS pour permettre au navigateur de lire le fichier distant
204
  const PROXY_URL = `https://corsproxy.io/?${encodeURIComponent(REAL_DATA_URL)}`;
205
+
206
  let allData = [];
207
  let filteredData = [];
208
+
209
+ // --- INITIALISATION ---
210
  function initializeApp() {
211
  setupEventListeners();
212
  updateData();
213
  }
214
+
215
+ // --- CHARGEMENT DES DONNÉES ---
216
  async function updateData() {
217
+ ui.resultsContainer.innerHTML = '<div class="loading-overlay">Chargement des données depuis la plateforme SCA...</div>';
218
  ui.updateDataBtn.disabled = true;
219
  ui.updateDataBtn.querySelector('i').classList.add('fa-spin');
220
+
221
  try {
222
+ // Fetch via le proxy
223
  const response = await fetch(PROXY_URL);
224
  if (!response.ok) {
225
  throw new Error(`Erreur réseau: ${response.status} ${response.statusText}`);
226
  }
227
+
228
+ // Lecture du fichier binaire (Excel)
229
  const arrayBuffer = await response.arrayBuffer();
230
  const workbook = XLSX.read(arrayBuffer, { type: 'array' });
231
+
232
+ // On prend la première feuille du classeur
233
+ const firstSheetName = workbook.SheetNames[0];
234
+ const worksheet = workbook.Sheets[firstSheetName];
235
+
236
+ // Conversion en JSON
237
  const rawJsonData = XLSX.utils.sheet_to_json(worksheet);
238
+
239
  if (!rawJsonData || !Array.isArray(rawJsonData)) {
240
+ throw new Error("Le fichier Excel est vide ou illisible.");
241
  }
242
+
243
+ // --- MAPPING ET NETTOYAGE ---
244
+ // On s'assure de lire les bonnes colonnes même si les noms varient légèrement (Pluriel/Singulier)
245
+ allData = rawJsonData.map(item => {
246
+ return {
247
+ 'BuSCA': parseInt(item['BuSCA'], 10) || 0,
248
+ 'Titre': String(item['Titre'] || '').trim(),
249
+ 'Texte': String(item['Texte'] || '').trim(),
250
+ // Gestion flexible des noms de colonnes : 'Matrices' ou 'Matrice'
251
+ 'Matrice': String(item['Matrices'] || item['Matrice'] || 'Non spécifié').trim(),
252
+ // Gestion flexible des noms de colonnes : 'Dangers' ou 'Danger'
253
+ 'Danger': String(item['Dangers'] || item['Danger'] || 'Non spécifié').trim(),
254
+ 'Lien': String(item['Lien'] || '').trim(),
255
+ 'Lien2': String(item['Lien2'] || '').trim()
256
+ };
257
+ }).filter(item => item.BuSCA > 0 && item.Titre); // On garde seulement les lignes avec un N° BuSCA et un titre
258
+
259
  showNotification('Données mises à jour avec succès !', 'success');
260
  processData();
261
+
262
  } catch (error) {
263
  console.error('Erreur lors de la mise à jour:', error);
264
+ ui.resultsContainer.innerHTML = `
265
+ <div class="empty-state" style="color: #dc3545;">
266
+ <i class="fas fa-exclamation-triangle fa-2x" style="margin-bottom:15px;"></i><br>
267
+ <strong>Échec du chargement.</strong><br>
268
+ Le fichier source est inaccessible ou le format a changé.<br>
269
+ <small>${error.message}</small>
270
+ </div>`;
271
  showNotification('Erreur de mise à jour.', 'error');
272
  } finally {
273
  ui.updateDataBtn.disabled = false;
 
275
  }
276
  }
277
 
278
+ // --- TRAITEMENT ET AFFICHAGE ---
279
  function processData() {
280
  applyFilters();
281
  }
282
+
283
  function applyFilters() {
284
  const criteria = {
285
  keywords: ui.filters.keywords.value.toLowerCase().split(',').map(k => k.trim()).filter(Boolean)
286
  };
287
+
288
  if (criteria.keywords.length === 0) {
289
  filteredData = allData;
290
  } else {
291
  filteredData = allData.filter(item => {
 
292
  const itemText = (item.Titre + ' ' + item.Texte + ' ' + item.Danger + ' ' + item.Matrice).toLowerCase();
293
  return criteria.keywords.every(k => itemText.includes(k));
294
  });
 
303
  applyFilters();
304
  showNotification('Filtres réinitialisés.', 'info');
305
  }
306
+
307
  function displayResults() {
308
  if (filteredData.length === 0) {
309
  ui.resultsContainer.innerHTML = '<div class="empty-state">Aucun bulletin ne correspond à vos critères.</div>';
310
  return;
311
  }
312
+
313
+ // Tri par numéro BuSCA décroissant (les plus récents en premier)
314
  const sortedData = [...filteredData].sort((a, b) => b.BuSCA - a.BuSCA);
315
+
316
  ui.resultsContainer.innerHTML = sortedData.map(item => `
317
  <article class="bulletin-card">
318
  <div class="bulletin-header" data-toggle="content">
319
+ <h3>${item.Titre || 'Sans titre'}</h3>
320
  <span class="bulletin-number">BuSCA n°${item.BuSCA}</span>
321
  </div>
322
  <div class="bulletin-content">
 
323
  <div class="bulletin-meta">
324
+ <span style="background: #e3f2fd; color: #1565C0; padding: 4px 8px; border-radius: 4px; margin-right: 10px;">
325
+ <i class="fas fa-vial"></i> <strong>Matrice:</strong> ${item.Matrice}
326
+ </span>
327
+ <span style="background: #ffebee; color: #c62828; padding: 4px 8px; border-radius: 4px;">
328
+ <i class="fas fa-exclamation-circle"></i> <strong>Danger:</strong> ${item.Danger}
329
+ </span>
330
  </div>
331
+ <p class="bulletin-summary"><strong>Résumé :</strong><br>${item.Texte.replace(/\n/g, '<br>') || 'Contenu non disponible.'}</p>
332
+ <div class="bulletin-links" style="margin-top: 15px; border-top: 1px dashed #eee; padding-top: 10px;">
333
+ ${item.Lien ? `<a href="${item.Lien}" class="nav-button" style="display:inline-flex; background:#1E88E5; width:auto; font-size:0.8em;" target="_blank">🔗 Lien principal</a>` : ''}
334
+ ${item.Lien2 ? `<a href="${item.Lien2}" class="nav-button" style="display:inline-flex; background:#555; width:auto; font-size:0.8em; margin-left: 10px;" target="_blank">🔗 Lien secondaire</a>` : ''}
335
  </div>
336
  </div>
337
  </article>`).join('');
 
342
  const foundBulletins = filteredData.length;
343
  const uniqueMatrices = new Set(filteredData.map(i => i.Matrice)).size;
344
  const uniqueDangers = new Set(filteredData.map(i => i.Danger)).size;
345
+
346
  ui.statsContainer.innerHTML = `
347
+ <div class="stat-card"><div class="stat-number">${foundBulletins} <small style="font-size:0.5em; color:#999">/ ${totalBulletins}</small></div><div class="stat-label">Bulletins Affichés</div></div>
348
  <div class="stat-card"><div class="stat-number">${uniqueMatrices}</div><div class="stat-label">Matrices Concernées</div></div>
349
  <div class="stat-card"><div class="stat-number">${uniqueDangers}</div><div class="stat-label">Dangers Identifiés</div></div>
350
  `;
351
  }
352
+
353
+ // --- ÉVÉNEMENTS ---
354
  function setupEventListeners() {
355
  ui.openSidebarBtn.addEventListener('click', () => ui.sidebar.classList.remove('collapsed'));
356
  ui.sidebarToggle.addEventListener('click', () => ui.sidebar.classList.add('collapsed'));
357
+
358
  ui.updateDataBtn.addEventListener('click', updateData);
359
+
360
  ui.applyFiltersBtn.addEventListener('click', () => {
361
  applyFilters();
362
  showNotification('Filtres appliqués !', 'success');
363
  });
364
+
365
  ui.clearFiltersBtn.addEventListener('click', clearFilters);
366
  ui.exportCsvBtn.addEventListener('click', exportToCSV);
367
 
368
+ // Délégation d'événement pour l'accordéon des résultats
369
  ui.resultsContainer.addEventListener('click', (e) => {
370
  const header = e.target.closest('[data-toggle="content"]');
371
+ if (header) {
372
+ const content = header.nextElementSibling;
373
+ const isActive = content.classList.contains('active');
374
+
375
+ // Optionnel : fermer les autres si on veut un accordéon strict
376
+ // document.querySelectorAll('.bulletin-content').forEach(c => c.classList.remove('active'));
377
+
378
+ if (!isActive) content.classList.add('active');
379
+ else content.classList.remove('active');
380
+ }
381
  });
382
 
383
+ // Raccourcis clavier
384
  document.addEventListener('keydown', (e) => {
385
  if (e.key === 'Escape' && !ui.sidebar.classList.contains('collapsed')) {
386
  ui.sidebar.classList.add('collapsed');
 
391
  }
392
  });
393
  }
394
+
395
  function showNotification(message, type = 'info') {
396
  const notification = document.createElement('div');
397
  notification.className = `notification-toast ${type}`;
398
  notification.textContent = message;
399
  document.body.appendChild(notification);
400
+
401
+ // Animation
402
  requestAnimationFrame(() => notification.classList.add('show'));
403
+
404
  setTimeout(() => {
405
  notification.classList.remove('show');
406
  notification.addEventListener('transitionend', () => notification.remove());
407
  }, 3000);
408
  }
409
+
410
  function exportToCSV() {
411
  if (filteredData.length === 0) {
412
  showNotification('Aucune donnée à exporter', 'error');
413
  return;
414
  }
415
+
416
  const headers = ['BuSCA', 'Titre', 'Matrice', 'Danger', 'Texte', 'Lien', 'Lien2'];
417
  const escapeCsv = (str) => `"${String(str || '').replace(/"/g, '""')}"`;
418
+
419
  const csvContent = [
420
  headers.join(','),
421
  ...filteredData.map(item => [
422
+ item.BuSCA,
423
+ escapeCsv(item.Titre),
424
+ escapeCsv(item.Matrice),
425
+ escapeCsv(item.Danger),
426
+ escapeCsv(item.Texte),
427
+ escapeCsv(item.Lien),
428
+ escapeCsv(item.Lien2)
429
  ].join(','))
430
  ].join('\n');
431
+
432
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
433
  const link = document.createElement('a');
434
+ const url = URL.createObjectURL(blob);
435
+
436
+ link.setAttribute('href', url);
437
+ link.setAttribute('download', `busca_export_${new Date().toISOString().split('T')[0]}.csv`);
438
+ link.style.visibility = 'hidden';
439
+
440
  document.body.appendChild(link);
441
  link.click();
442
  document.body.removeChild(link);
443
+
444
  showNotification('Export CSV généré !', 'success');
445
  }
446
 
447
+ // Lancement de l'app
448
  initializeApp();
449
  });
450
  </script>