MMOON commited on
Commit
00a629c
·
verified ·
1 Parent(s): 15fd474

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +234 -94
index.html CHANGED
@@ -19,25 +19,30 @@
19
  .header-content { max-width: 1600px; margin: 0 auto; padding: 0 2rem; display: flex; align-items: center; justify-content: space-between; }
20
  .logo { display: flex; align-items: center; gap: 0.75rem; color: var(--primary-color); font-weight: 600; font-size: 1.25rem; }
21
  .logo-icon { width: 32px; height: 32px; background-color: var(--primary-color); border-radius: 6px; display: flex; align-items: center; justify-content: center; color: white; font-size: 1rem; }
 
 
22
  .container { max-width: 1600px; margin: 2rem auto; padding: 0 2rem; }
 
 
 
 
 
 
 
23
 
24
  .controls-panel { background: var(--surface-color); border-radius: var(--border-radius); padding: 1.5rem; margin-bottom: 2rem; box-shadow: var(--shadow-md); border: 1px solid var(--border-color); }
25
  .view-selector { display: flex; flex-wrap: wrap; gap: 0.5rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; margin-bottom: 1rem; }
26
  .view-btn { padding: 0.6rem 1.2rem; border: 1px solid transparent; background: none; color: var(--text-secondary); cursor: pointer; font-size: 0.95rem; font-weight: 500; border-bottom: 3px solid transparent; border-radius: 6px; }
27
  .view-btn.active { color: var(--primary-light); background-color: #e7f3ff; border-color: var(--primary-light); }
28
 
29
- #main-view-controls { display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: center; }
30
  #comparison-view-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; }
31
-
32
  .search-container { position: relative; }
33
  .search-input { width: 100%; padding: 0.75rem 1rem 0.75rem 2.5rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); font-size: 1rem; transition: var(--transition); }
34
  .search-input:focus { outline: none; border-color: var(--primary-light); box-shadow: 0 0 0 3px rgba(0, 85, 164, 0.1); }
35
  .search-icon { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: var(--text-secondary); }
36
  .filter-select { padding: 0.75rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); background: var(--surface-color); color: var(--text-primary); font-size: 0.9rem; width: 100%; }
37
 
38
- .main-view-content, .comparison-view-content { display: none; }
39
- .main-view-content.active, .comparison-view-content.active { display: block; }
40
-
41
  .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
42
  .metric-card { background: var(--surface-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 1px solid var(--border-color); }
43
  .metric-label { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem; }
@@ -49,8 +54,7 @@
49
  .detail-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: start; }
50
  .chart-title { font-weight: 600; color: var(--text-primary); margin: 0 0 1rem 0; font-size: 1.1rem; word-break: break-word; }
51
  .chart-canvas-container { position: relative; height: 320px; }
52
- #comparison-chart-container { height: auto; min-height: 600px; }
53
-
54
  #takeaways-section h4 { margin: 0 0 1rem 0; color: var(--primary-color); }
55
  #takeaways-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.75rem; }
56
  #takeaways-list li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.9rem; }
@@ -79,16 +83,44 @@
79
  .score-low { background-color: #e6f4ea; color: #1b5e20; }
80
  #loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.9); z-index: 3000; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 1.2rem; color: var(--primary-color); transition: opacity 0.3s; }
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  @keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
83
- @media (max-width: 992px) { #main-view-controls, #comparison-view-controls { grid-template-columns: 1fr; } .detail-layout { grid-template-columns: 1fr; } }
84
  @media (max-width: 768px) { .header-content, .container { padding: 0 1rem; } }
85
  </style>
86
  </head>
87
  <body>
88
  <div id="loading-overlay"><div><h2>Chargement des données...</h2><p>Veuillez patienter.</p></div></div>
89
- <header class="header"><div class="header-content"><div class="logo"><div class="logo-icon">🛡️</div><span>FDA Risk Analyzer</span></div></div></header>
 
 
 
 
 
90
 
91
  <div class="container">
 
 
 
 
 
 
 
 
 
 
92
  <div class="controls-panel">
93
  <div class="view-selector">
94
  <button class="view-btn active" id="btn-ftl">Vue FTL</button>
@@ -96,15 +128,22 @@
96
  <button class="view-btn" id="btn-pairs">Analyse Détaillée</button>
97
  <button class="view-btn" id="btn-comparison">Analyse Comparative</button>
98
  </div>
99
-
100
- <div id="main-controls">
101
- <div class="search-container"><span class="search-icon">🔍</span><input type="search" class="search-input" id="searchInput" placeholder="Rechercher..."></div>
102
- <div id="hazard-filter-container" style="display: none;"><select class="filter-select" id="hazard-filter"><option value="">Tous les Dangers</option></select></div>
 
 
 
 
 
 
103
  </div>
104
-
105
- <div id="comparison-controls" style="display: none;">
106
- <select class="filter-select" id="commodity-filter"></select>
107
- <select class="filter-select" id="comparison-hazard-filter"><option value="All">Tous les Dangers</option></select>
 
108
  </div>
109
  </div>
110
 
@@ -115,11 +154,19 @@
115
  <div class="metric-card"><div class="metric-label">Catégories Uniques</div><div class="metric-value" id="categories-count">-</div></div>
116
  <div class="metric-card"><div class="metric-label">Score Moyen</div><div class="metric-value" id="avg-score">-</div></div>
117
  </div>
118
- <div id="detail-panel">
119
- <div class="detail-card"><div class="detail-layout">
120
- <div><h3 class="chart-title" id="criteria-chart-title"></h3><div class="chart-canvas-container"><canvas id="criteriaChart"></canvas></div></div>
121
- <div id="takeaways-section"><h4>Points de Vigilance & Actions</h4><ul id="takeaways-list"></ul></div>
122
- </div></div>
 
 
 
 
 
 
 
 
123
  </div>
124
  <div class="table-container">
125
  <div class="table-header"><h3 class="table-title" id="table-title"></h3><div class="results-count" id="results-count"></div></div>
@@ -128,51 +175,90 @@
128
  </div>
129
 
130
  <div id="comparison-view-content">
131
- <div class="chart-container">
132
  <h3 class="chart-title" id="stacked-chart-title">Analyse des Critères Pondérés</h3>
133
  <div class="chart-canvas-container" id="comparison-chart-container"><canvas id="stackedBarChart"></canvas></div>
134
  </div>
135
  </div>
136
  </div>
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
139
  <script>
140
  document.addEventListener('DOMContentLoaded', function() {
141
  let state = {
142
- currentDataset: 'ftl', searchTerm: '', hazardFilter: '',
143
- sortColumn: 'agg_score', sortDirection: 'desc', charts: {},
144
- selectedCommodity: '', selectedHazard: 'All'
 
 
 
 
 
145
  };
146
  let fdaData = { aggregated_ftl: [], aggregated_all: [], pairs_all: [] };
147
-
148
  const viewConfig = {
149
  'ftl': { dataKey: 'aggregated_ftl', title: 'Vue FTL', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'agg_score' },
150
  'all': { dataKey: 'aggregated_all', title: 'Vue Globale', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }, { key: 'ftl', label: 'FTL', sortable: true, type: 'boolean' }], scoreColumn: 'agg_score' },
151
- 'pairs': { dataKey: 'pairs_all', title: 'Analyse Détaillée', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'hazardRef', label: 'Danger', sortable: true }, { key: 'risk_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'risk_score' },
152
- 'comparison': { dataKey: 'pairs_all' }
153
  };
154
 
155
  const el = {
156
  loadingOverlay: document.getElementById('loading-overlay'),
157
- mainViewContent: document.getElementById('main-view-content'), comparisonViewContent: document.getElementById('comparison-view-content'),
158
- mainControls: document.getElementById('main-controls'), comparisonControls: document.getElementById('comparison-controls'),
159
- searchInput: document.getElementById('searchInput'), hazardFilter: document.getElementById('hazard-filter'), hazardFilterContainer: document.getElementById('hazard-filter-container'),
160
- commodityFilter: document.getElementById('commodity-filter'), comparisonHazardFilter: document.getElementById('comparison-hazard-filter'),
 
 
 
 
 
161
  buttons: { ftl: document.getElementById('btn-ftl'), all: document.getElementById('btn-all'), pairs: document.getElementById('btn-pairs'), comparison: document.getElementById('btn-comparison') },
162
  metrics: { totalProducts: document.getElementById('total-products'), highRiskCount: document.getElementById('high-risk-count'), categoriesCount: document.getElementById('categories-count'), avgScore: document.getElementById('avg-score')},
163
  detailPanel: document.getElementById('detail-panel'),
164
- criteriaChartTitle: document.getElementById('criteria-chart-title'), criteriaCanvas: document.getElementById('criteriaChart'),
165
- takeawaysSection: document.getElementById('takeaways-section'), takeawaysList: document.getElementById('takeaways-list'),
166
- tableTitle: document.getElementById('table-title'), tableHead: document.getElementById('tableHead'), tableBody: document.getElementById('tableBody'), resultsCount: document.getElementById('results-count'),
167
- stackedBarChartCanvas: document.getElementById('stackedBarChart'), stackedChartTitle: document.getElementById('stacked-chart-title'),
168
- };
169
-
170
- const formatCell = (value, type) => {
171
- if (value === undefined || value === null) return '';
172
- const cleanValue = String(value);
173
- if (type === 'score') return `<span class="score-badge ${parseInt(cleanValue) >= 400 ? 'score-high' : parseInt(cleanValue) >= 200 ? 'score-medium' : 'score-low'}">${cleanValue}</span>`;
174
- if (type === 'boolean') return cleanValue === 'true' ? '<span style="color: var(--success-color); font-weight: 600;">✓</span>' : '<span style="color: var(--text-secondary);">-</span>';
175
- return cleanValue;
 
 
 
176
  };
177
 
178
  async function loadAllData() {
@@ -183,34 +269,39 @@
183
  };
184
  try {
185
  const responses = await Promise.all(Object.values(files).map(file => fetch(file)));
186
- for (const res of responses) { if (!res.ok) throw new Error(`Échec du chargement du fichier: ${res.url}`); }
187
  const [ftl, all, pairs] = await Promise.all(responses.map(res => res.json()));
188
  const clean = item => ({ ...item, commodity: String(item.commodity).replace(/<ast>\*<\/ast>/g, '').trim() });
189
  fdaData.aggregated_ftl = ftl.map(clean);
190
  fdaData.aggregated_all = all.map(clean);
191
  fdaData.pairs_all = pairs.map(item => ({ ...clean(item), C1w: (item.C1||0)*10, C2w: (item.C2||0)*10, C3w: (item.C3||0)*10, C4w: (item.C4||0)*10, C5w: (item.C5||0)*10, C6w: (item.C6||0)*10, C7w: (item.C7||0)*10 }));
192
 
193
- setupUI();
194
- el.loadingOverlay.style.display = 'none';
195
  } catch (error) { el.loadingOverlay.innerHTML = `<h2>Erreur</h2><p style="color:red;">${error.message}</p>`; }
196
  }
197
 
198
- function setupUI() {
199
- populateHazardFilter(); populateCommodityFilter();
200
- render(); setupEventListeners();
 
 
201
  }
202
-
203
- function render() {
204
- const isMainView = state.currentDataset !== 'comparison';
205
- el.mainViewContent.style.display = isMainView ? 'block' : 'none';
206
- el.comparisonViewContent.style.display = isMainView ? 'none' : 'block';
207
- el.mainControls.style.display = isMainView ? 'grid' : 'none';
208
- el.comparisonControls.style.display = isMainView ? 'none' : 'grid';
209
 
 
 
 
 
 
 
 
210
  Object.values(el.buttons).forEach(btn => btn.classList.remove('active'));
211
  el.buttons[state.currentDataset].classList.add('active');
212
 
213
- if(isMainView) renderMainView(); else renderComparisonView();
 
 
 
 
214
  }
215
 
216
  function renderMainView() {
@@ -222,27 +313,15 @@
222
  document.querySelectorAll('#tableHead th').forEach(th => { th.classList.remove('sort-asc', 'sort-desc'); if (th.dataset.column === state.sortColumn) th.classList.add(`sort-${state.sortDirection}`); });
223
  el.tableBody.innerHTML = data.length === 0 ? `<tr><td colspan="${config.columns.length}" style="text-align:center;">🚫 Aucun résultat</td></tr>` : data.map((row, index) => `<tr class="${state.currentDataset === 'pairs' ? 'clickable' : ''}" data-index="${index}">${config.columns.map(col => `<td>${formatCell(row[col.key], col.type)}</td>`).join('')}</tr>`).join('');
224
  updateDashboard(data);
225
- el.hazardFilterContainer.style.display = state.currentDataset === 'pairs' ? 'block' : 'none';
226
- }
227
-
228
- function updateDashboard(data) {
229
- const scoreCol = viewConfig[state.currentDataset].scoreColumn;
230
- const scores = data.map(i => i[scoreCol]).filter(s => typeof s === 'number');
231
- el.metrics.totalProducts.textContent = data.length;
232
- el.metrics.highRiskCount.textContent = scores.filter(s => s >= 400).length;
233
- el.metrics.categoriesCount.textContent = new Set(data.map(i => i.commodity_category).filter(Boolean)).size;
234
- el.metrics.avgScore.textContent = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 'N/A';
235
  }
236
 
237
- function showDetailPanel(rowIndex) {
238
- // ... (code de la fonction showDetailPanel de la version précédente)
239
- }
240
-
241
  function renderComparisonView() {
242
  let data = fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity);
243
  if (state.selectedHazard !== 'All') data = data.filter(p => p.hazardRef === state.selectedHazard);
244
  el.stackedChartTitle.textContent = `Analyse des Critères pour : ${state.selectedCommodity}`;
245
-
246
  if (state.charts.stackedBar) state.charts.stackedBar.destroy();
247
  const criteria = ['C1w', 'C2w', 'C3w', 'C4w', 'C5w', 'C6w', 'C7w'];
248
  const labels = ['C1: Éclosions', 'C2: Gravité', 'C3: Contamination', 'C4: Croissance', 'C5: Intervention', 'C6: Consommation', 'C7: Coût'];
@@ -251,46 +330,107 @@
251
  state.charts.stackedBar = new Chart(el.stackedBarChartCanvas, {
252
  type: 'bar',
253
  data: { labels: data.map(d => d.hazardRef), datasets: criteria.map((c, i) => ({ label: labels[i], data: data.map(d => d[c]), backgroundColor: colors[i] }))},
254
- options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true }, y: { stacked: true } }, plugins: { legend: { position: 'top' } } }
255
  });
256
  }
257
 
258
- function populateCommodityFilter() {
259
- const commodities = [...new Set(fdaData.pairs_all.map(item => item.commodity))].sort();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  el.commodityFilter.innerHTML = commodities.map(c => `<option value="${c}">${c}</option>`).join('');
261
- if(commodities.length > 0){
262
  state.selectedCommodity = commodities[0];
263
  el.commodityFilter.value = state.selectedCommodity;
 
264
  }
265
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
 
267
- function populateHazardFilter() {
268
- let hazards = [...new Set(fdaData.pairs_all.map(item => item.hazardRef))].filter(Boolean).sort();
269
- el.hazardFilter.innerHTML = '<option value="">Tous les Dangers</option>' + hazards.map(h => `<option value="${h}">${h}</option>`).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- if(state.selectedCommodity){
272
- hazards = [...new Set(fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity).map(p => p.hazardRef))].sort();
273
- el.comparisonHazardFilter.innerHTML = '<option value="All">Tous les Dangers</option>' + hazards.map(h => `<option value="${h}">${h}</option>`).join('');
274
- }
275
  }
276
 
277
  function setupEventListeners() {
278
- el.searchInput.addEventListener('input', () => { state.searchTerm = el.searchInput.value; render(); });
279
- el.hazardFilter.addEventListener('change', () => { state.hazardFilter = el.hazardFilter.value; render(); });
280
- el.tableHead.addEventListener('click', (e) => { const th = e.target.closest('th[data-column]'); if (th) { state.sortColumn = th.dataset.column; state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'; render(); }});
281
- el.tableBody.addEventListener('click', (e) => { if (state.currentDataset !== 'pairs') return; const row = e.target.closest('tr'); if (row) showDetailPanel(row.dataset.index); });
 
282
  Object.keys(el.buttons).forEach(key => {
283
  el.buttons[key].addEventListener('click', () => {
284
  state.currentDataset = key;
285
- state.sortColumn = viewConfig[key].scoreColumn;
286
- state.searchTerm = ''; el.searchInput.value = '';
 
287
  el.detailPanel.classList.remove('visible');
288
  render();
289
  });
290
  });
291
- el.commodityFilter.addEventListener('change', (e) => { state.selectedCommodity = e.target.value; state.selectedHazard = 'All'; populateHazardFilter(); renderComparisonView(); });
292
- el.comparisonHazardFilter.addEventListener('change', (e) => { state.selectedHazard = e.target.value; renderComparisonView(); });
293
- // etc.
 
 
 
 
 
 
294
  }
295
 
296
  loadAllData();
 
19
  .header-content { max-width: 1600px; margin: 0 auto; padding: 0 2rem; display: flex; align-items: center; justify-content: space-between; }
20
  .logo { display: flex; align-items: center; gap: 0.75rem; color: var(--primary-color); font-weight: 600; font-size: 1.25rem; }
21
  .logo-icon { width: 32px; height: 32px; background-color: var(--primary-color); border-radius: 6px; display: flex; align-items: center; justify-content: center; color: white; font-size: 1rem; }
22
+ .guide-btn { background-color: var(--primary-color); color: white; border: none; padding: 0.6rem 1.2rem; border-radius: var(--border-radius); cursor: pointer; font-size: 0.9rem; transition: var(--transition); }
23
+ .guide-btn:hover { background-color: var(--primary-light); }
24
  .container { max-width: 1600px; margin: 2rem auto; padding: 0 2rem; }
25
+
26
+ .welcome-guide { background-color: #e7f3ff; border: 1px solid #a8c7fa; border-radius: var(--border-radius); padding: 1.5rem; margin-bottom: 2rem; position: relative; animation: fadeIn 0.5s; }
27
+ .welcome-guide h3 { margin: 0 0 1rem 0; color: var(--primary-color); }
28
+ .guide-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; }
29
+ .guide-step { font-size: 0.9rem; }
30
+ .guide-step strong { color: var(--text-primary); }
31
+ .close-guide { position: absolute; top: 0.5rem; right: 0.5rem; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-secondary); }
32
 
33
  .controls-panel { background: var(--surface-color); border-radius: var(--border-radius); padding: 1.5rem; margin-bottom: 2rem; box-shadow: var(--shadow-md); border: 1px solid var(--border-color); }
34
  .view-selector { display: flex; flex-wrap: wrap; gap: 0.5rem; border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; margin-bottom: 1rem; }
35
  .view-btn { padding: 0.6rem 1.2rem; border: 1px solid transparent; background: none; color: var(--text-secondary); cursor: pointer; font-size: 0.95rem; font-weight: 500; border-bottom: 3px solid transparent; border-radius: 6px; }
36
  .view-btn.active { color: var(--primary-light); background-color: #e7f3ff; border-color: var(--primary-light); }
37
 
38
+ #main-view-controls { display: grid; grid-template-columns: 1fr auto; gap: 1rem; align-items: center; }
39
  #comparison-view-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: center; }
 
40
  .search-container { position: relative; }
41
  .search-input { width: 100%; padding: 0.75rem 1rem 0.75rem 2.5rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); font-size: 1rem; transition: var(--transition); }
42
  .search-input:focus { outline: none; border-color: var(--primary-light); box-shadow: 0 0 0 3px rgba(0, 85, 164, 0.1); }
43
  .search-icon { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: var(--text-secondary); }
44
  .filter-select { padding: 0.75rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); background: var(--surface-color); color: var(--text-primary); font-size: 0.9rem; width: 100%; }
45
 
 
 
 
46
  .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
47
  .metric-card { background: var(--surface-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--shadow-sm); border: 1px solid var(--border-color); }
48
  .metric-label { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem; }
 
54
  .detail-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: start; }
55
  .chart-title { font-weight: 600; color: var(--text-primary); margin: 0 0 1rem 0; font-size: 1.1rem; word-break: break-word; }
56
  .chart-canvas-container { position: relative; height: 320px; }
57
+ #comparison-chart-container { min-height: 600px; height: auto; }
 
58
  #takeaways-section h4 { margin: 0 0 1rem 0; color: var(--primary-color); }
59
  #takeaways-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.75rem; }
60
  #takeaways-list li { display: flex; align-items: flex-start; gap: 0.75rem; font-size: 0.9rem; }
 
83
  .score-low { background-color: #e6f4ea; color: #1b5e20; }
84
  #loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.9); z-index: 3000; display: flex; align-items: center; justify-content: center; text-align: center; font-size: 1.2rem; color: var(--primary-color); transition: opacity 0.3s; }
85
 
86
+ .modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); }
87
+ .modal-content { background-color: var(--surface-color); margin: 5% auto; padding: 2.5rem; border: 1px solid var(--border-color); width: 90%; max-width: 800px; border-radius: var(--border-radius); animation: fadeIn 0.3s; }
88
+ .modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 1rem; margin-bottom: 1.5rem; }
89
+ .modal-title { font-size: 1.5rem; color: var(--primary-color); margin: 0; }
90
+ .close-modal { font-size: 2rem; font-weight: bold; color: var(--text-secondary); cursor: pointer; }
91
+ .close-modal:hover { color: var(--text-primary); }
92
+ .criteria-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem; }
93
+ .criteria-card { background: var(--bg-color); padding: 1rem; border-radius: var(--border-radius); }
94
+ .criteria-title { font-weight: 600; display: block; margin-bottom: 0.25rem; }
95
+ .criteria-desc { font-size: 0.9rem; color: var(--text-secondary); }
96
+ .modal-footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border-color); text-align: center; font-size: 0.9rem; }
97
+ .modal-footer a { color: var(--primary-light); }
98
+
99
  @keyframes fadeIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
100
+ @media (max-width: 992px) { .controls-grid, .detail-layout { grid-template-columns: 1fr; } }
101
  @media (max-width: 768px) { .header-content, .container { padding: 0 1rem; } }
102
  </style>
103
  </head>
104
  <body>
105
  <div id="loading-overlay"><div><h2>Chargement des données...</h2><p>Veuillez patienter.</p></div></div>
106
+ <header class="header">
107
+ <div class="header-content">
108
+ <div class="logo"><div class="logo-icon">🛡️</div><span>FDA Risk Analyzer</span></div>
109
+ <button class="guide-btn" id="guide-btn">📚 Guide d'Utilisation</button>
110
+ </div>
111
+ </header>
112
 
113
  <div class="container">
114
+ <div class="welcome-guide" id="welcome-guide">
115
+ <button class="close-guide" id="close-guide">×</button>
116
+ <h3>Guide Rapide de l'Outil</h3>
117
+ <div class="guide-steps">
118
+ <div class="guide-step"><strong>1. Choisissez une Vue :</strong><br>Utilisez les boutons ci-dessous pour sélectionner votre niveau d'analyse. "Analyse Détaillée" est la plus puissante pour étudier les causes des risques.</div>
119
+ <div class="guide-step"><strong>2. Filtrez les Données :</strong><br>Utilisez la recherche et le filtre "Danger" (en mode détaillé) pour affiner les résultats.</div>
120
+ <div class="guide-step"><strong>3. Analysez en Profondeur :</strong><br>En mode "Analyse Détaillée", cliquez sur une ligne pour visualiser le détail des 7 critères de risque et les actions recommandées.</div>
121
+ </div>
122
+ </div>
123
+
124
  <div class="controls-panel">
125
  <div class="view-selector">
126
  <button class="view-btn active" id="btn-ftl">Vue FTL</button>
 
128
  <button class="view-btn" id="btn-pairs">Analyse Détaillée</button>
129
  <button class="view-btn" id="btn-comparison">Analyse Comparative</button>
130
  </div>
131
+ <div id="main-view-controls">
132
+ <div class="controls-grid">
133
+ <div class="search-container">
134
+ <span class="search-icon">🔍</span>
135
+ <input type="search" class="search-input" id="searchInput" placeholder="Rechercher...">
136
+ </div>
137
+ <div id="hazard-filter-container" style="display: none;">
138
+ <select class="filter-select" id="hazard-filter"><option value="">Tous les Dangers</option></select>
139
+ </div>
140
+ </div>
141
  </div>
142
+ <div id="comparison-view-controls" style="display: none;">
143
+ <div class="controls-grid" style="grid-template-columns: 1fr 1fr;">
144
+ <select class="filter-select" id="commodity-filter"></select>
145
+ <select class="filter-select" id="comparison-hazard-filter"><option value="All">Tous les Dangers</option></select>
146
+ </div>
147
  </div>
148
  </div>
149
 
 
154
  <div class="metric-card"><div class="metric-label">Catégories Uniques</div><div class="metric-value" id="categories-count">-</div></div>
155
  <div class="metric-card"><div class="metric-label">Score Moyen</div><div class="metric-value" id="avg-score">-</div></div>
156
  </div>
157
+ <div id="detail-panel">
158
+ <div class="detail-card">
159
+ <div class="detail-layout">
160
+ <div>
161
+ <h3 class="chart-title" id="criteria-chart-title"></h3>
162
+ <div class="chart-canvas-container"><canvas id="criteriaChart"></canvas></div>
163
+ </div>
164
+ <div id="takeaways-section">
165
+ <h4>Points de Vigilance & Actions</h4>
166
+ <ul id="takeaways-list"></ul>
167
+ </div>
168
+ </div>
169
+ </div>
170
  </div>
171
  <div class="table-container">
172
  <div class="table-header"><h3 class="table-title" id="table-title"></h3><div class="results-count" id="results-count"></div></div>
 
175
  </div>
176
 
177
  <div id="comparison-view-content">
178
+ <div class="chart-container">
179
  <h3 class="chart-title" id="stacked-chart-title">Analyse des Critères Pondérés</h3>
180
  <div class="chart-canvas-container" id="comparison-chart-container"><canvas id="stackedBarChart"></canvas></div>
181
  </div>
182
  </div>
183
  </div>
184
 
185
+ <div id="guide-modal" class="modal">
186
+ <div class="modal-content">
187
+ <div class="modal-header"><h2 class="modal-title">Guide d'Utilisation</h2><span class="close-modal" id="close-guide-btn">&times;</span></div>
188
+ <h3>Comment utiliser cet outil</h3>
189
+ <p>Cet outil vous aide à explorer le modèle de classement des risques de la FDA pour prioriser vos efforts en sécurité alimentaire.</p>
190
+ <h4>Les Vues d'Analyse</h4>
191
+ <ul>
192
+ <li><strong>Vue FTL :</strong> Se concentre sur les produits à plus haut risque identifiés par la FDA (Food Traceability List). Idéal pour les audits de conformité.</li>
193
+ <li><strong>Vue Globale :</strong> Affiche tous les produits de la base de données pour une vue d'ensemble complète du paysage des risques.</li>
194
+ <li><strong>Analyse Détaillée :</strong> Montre les couples produit/danger. Cliquez sur une ligne pour afficher une analyse détaillée (graphique radar et actions recommandées).</li>
195
+ <li><strong>Analyse Comparative :</strong> Permet de comparer visuellement les contributions des différents facteurs de risque pour un produit et ses dangers associés.</li>
196
+ </ul>
197
+ <h3>Les 7 Critères d'Évaluation de la FDA</h3>
198
+ <div class="criteria-grid">
199
+ <div class="criteria-card"><div class="criteria-title">C1: Gravité</div><div class="criteria-desc">Impact sur la santé.</div></div>
200
+ <div class="criteria-card"><div class="criteria-title">C2: Exposition</div><div class="criteria-desc">Fréquence de consommation.</div></div>
201
+ <div class="criteria-card"><div class="criteria-title">C3: Dose-Réponse</div><div class="criteria-desc">Quantité de pathogène infectieuse.</div></div>
202
+ <div class="criteria-card"><div class="criteria-title">C4: Croissance</div><div class="criteria-desc">Potentiel de multiplication.</div></div>
203
+ <div class="criteria-card"><div class="criteria-title">C5: Traitement</div><div class="criteria-desc">Impact des processus industriels.</div></div>
204
+ <div class="criteria-card"><div class="criteria-title">C6: Contamination</div><div class="criteria-desc">Probabilité de contamination à la source.</div></div>
205
+ <div class="criteria-card"><div class="criteria-title">C7: Consommation</div><div class="criteria-desc">Mode de consommation (cru/cuit).</div></div>
206
+ </div>
207
+ <div class="modal-footer">Source officielle : <a href="https://hfpappexternal.fda.gov/scripts/FDARiskRankingModelforFoodTracingfinalrule/" target="_blank" rel="noopener noreferrer">FDA Risk-Ranking Model</a>.</div>
208
+ </div>
209
+ </div>
210
+
211
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
212
+
213
  <script>
214
  document.addEventListener('DOMContentLoaded', function() {
215
  let state = {
216
+ currentDataset: 'ftl',
217
+ searchTerm: '',
218
+ hazardFilter: '',
219
+ sortColumn: 'agg_score',
220
+ sortDirection: 'desc',
221
+ selectedCommodity: '',
222
+ selectedHazard: 'All',
223
+ charts: {}
224
  };
225
  let fdaData = { aggregated_ftl: [], aggregated_all: [], pairs_all: [] };
226
+
227
  const viewConfig = {
228
  'ftl': { dataKey: 'aggregated_ftl', title: 'Vue FTL', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'agg_score' },
229
  'all': { dataKey: 'aggregated_all', title: 'Vue Globale', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'commodity_category', label: 'Catégorie', sortable: true }, { key: 'agg_score', label: 'Score', sortable: true, type: 'score' }, { key: 'ftl', label: 'FTL', sortable: true, type: 'boolean' }], scoreColumn: 'agg_score' },
230
+ 'pairs': { dataKey: 'pairs_all', title: 'Analyse Détaillée', columns: [{ key: 'commodity', label: 'Produit', sortable: true }, { key: 'hazardRef', label: 'Danger', sortable: true }, { key: 'risk_score', label: 'Score', sortable: true, type: 'score' }], scoreColumn: 'risk_score' }
 
231
  };
232
 
233
  const el = {
234
  loadingOverlay: document.getElementById('loading-overlay'),
235
+ mainViewContent: document.getElementById('main-view-content'),
236
+ comparisonViewContent: document.getElementById('comparison-view-content'),
237
+ mainControls: document.getElementById('main-view-controls'),
238
+ comparisonControls: document.getElementById('comparison-controls'),
239
+ searchInput: document.getElementById('searchInput'),
240
+ hazardFilter: document.getElementById('hazard-filter'),
241
+ hazardFilterContainer: document.getElementById('hazard-filter-container'),
242
+ commodityFilter: document.getElementById('commodity-filter'),
243
+ comparisonHazardFilter: document.getElementById('comparison-hazard-filter'),
244
  buttons: { ftl: document.getElementById('btn-ftl'), all: document.getElementById('btn-all'), pairs: document.getElementById('btn-pairs'), comparison: document.getElementById('btn-comparison') },
245
  metrics: { totalProducts: document.getElementById('total-products'), highRiskCount: document.getElementById('high-risk-count'), categoriesCount: document.getElementById('categories-count'), avgScore: document.getElementById('avg-score')},
246
  detailPanel: document.getElementById('detail-panel'),
247
+ criteriaChartTitle: document.getElementById('criteria-chart-title'),
248
+ criteriaCanvas: document.getElementById('criteriaChart'),
249
+ takeawaysSection: document.getElementById('takeaways-section'),
250
+ takeawaysList: document.getElementById('takeaways-list'),
251
+ tableTitle: document.getElementById('table-title'),
252
+ tableHead: document.getElementById('tableHead'),
253
+ tableBody: document.getElementById('tableBody'),
254
+ resultsCount: document.getElementById('results-count'),
255
+ stackedBarChartCanvas: document.getElementById('stackedBarChart'),
256
+ stackedChartTitle: document.getElementById('stacked-chart-title'),
257
+ welcomeGuide: document.getElementById('welcome-guide'),
258
+ closeGuide: document.getElementById('close-guide'),
259
+ guideModal: document.getElementById('guide-modal'),
260
+ guideBtn: document.getElementById('guide-btn'),
261
+ closeGuideBtn: document.getElementById('close-guide-btn')
262
  };
263
 
264
  async function loadAllData() {
 
269
  };
270
  try {
271
  const responses = await Promise.all(Object.values(files).map(file => fetch(file)));
272
+ for (const res of responses) { if (!res.ok) throw new Error(`Échec du chargement : ${res.url}`); }
273
  const [ftl, all, pairs] = await Promise.all(responses.map(res => res.json()));
274
  const clean = item => ({ ...item, commodity: String(item.commodity).replace(/<ast>\*<\/ast>/g, '').trim() });
275
  fdaData.aggregated_ftl = ftl.map(clean);
276
  fdaData.aggregated_all = all.map(clean);
277
  fdaData.pairs_all = pairs.map(item => ({ ...clean(item), C1w: (item.C1||0)*10, C2w: (item.C2||0)*10, C3w: (item.C3||0)*10, C4w: (item.C4||0)*10, C5w: (item.C5||0)*10, C6w: (item.C6||0)*10, C7w: (item.C7||0)*10 }));
278
 
279
+ initializeAppUI();
 
280
  } catch (error) { el.loadingOverlay.innerHTML = `<h2>Erreur</h2><p style="color:red;">${error.message}</p>`; }
281
  }
282
 
283
+ function initializeAppUI() {
284
+ populateFilters();
285
+ setupEventListeners();
286
+ render();
287
+ el.loadingOverlay.style.display = 'none';
288
  }
 
 
 
 
 
 
 
289
 
290
+ function render() {
291
+ const isComparisonView = state.currentDataset === 'comparison';
292
+ el.mainViewContent.style.display = isComparisonView ? 'none' : 'block';
293
+ el.comparisonViewContent.style.display = isComparisonView ? 'block' : 'none';
294
+ el.mainControls.style.display = isComparisonView ? 'none' : 'block';
295
+ el.comparisonControls.style.display = isComparisonView ? 'block' : 'none';
296
+
297
  Object.values(el.buttons).forEach(btn => btn.classList.remove('active'));
298
  el.buttons[state.currentDataset].classList.add('active');
299
 
300
+ if (isComparisonView) {
301
+ renderComparisonView();
302
+ } else {
303
+ renderMainView();
304
+ }
305
  }
306
 
307
  function renderMainView() {
 
313
  document.querySelectorAll('#tableHead th').forEach(th => { th.classList.remove('sort-asc', 'sort-desc'); if (th.dataset.column === state.sortColumn) th.classList.add(`sort-${state.sortDirection}`); });
314
  el.tableBody.innerHTML = data.length === 0 ? `<tr><td colspan="${config.columns.length}" style="text-align:center;">🚫 Aucun résultat</td></tr>` : data.map((row, index) => `<tr class="${state.currentDataset === 'pairs' ? 'clickable' : ''}" data-index="${index}">${config.columns.map(col => `<td>${formatCell(row[col.key], col.type)}</td>`).join('')}</tr>`).join('');
315
  updateDashboard(data);
316
+ el.hazardFilterContainer.style.display = state.currentDataset === 'pairs' ? 'flex' : 'none';
317
+ if (state.currentDataset !== 'pairs') el.detailPanel.classList.remove('visible');
 
 
 
 
 
 
 
 
318
  }
319
 
 
 
 
 
320
  function renderComparisonView() {
321
  let data = fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity);
322
  if (state.selectedHazard !== 'All') data = data.filter(p => p.hazardRef === state.selectedHazard);
323
  el.stackedChartTitle.textContent = `Analyse des Critères pour : ${state.selectedCommodity}`;
324
+
325
  if (state.charts.stackedBar) state.charts.stackedBar.destroy();
326
  const criteria = ['C1w', 'C2w', 'C3w', 'C4w', 'C5w', 'C6w', 'C7w'];
327
  const labels = ['C1: Éclosions', 'C2: Gravité', 'C3: Contamination', 'C4: Croissance', 'C5: Intervention', 'C6: Consommation', 'C7: Coût'];
 
330
  state.charts.stackedBar = new Chart(el.stackedBarChartCanvas, {
331
  type: 'bar',
332
  data: { labels: data.map(d => d.hazardRef), datasets: criteria.map((c, i) => ({ label: labels[i], data: data.map(d => d[c]), backgroundColor: colors[i] }))},
333
+ options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true, title: { display: true, text: 'Score Pondéré' } }, y: { stacked: true } }, plugins: { legend: { position: 'top' } } }
334
  });
335
  }
336
 
337
+ function filterAndSortData() {
338
+ const config = viewConfig[state.currentDataset];
339
+ let data = [...fdaData[config.dataKey]];
340
+ if (state.searchTerm) { const search = state.searchTerm.toLowerCase(); data = data.filter(row => Object.values(row).some(v => String(v).toLowerCase().includes(search))); }
341
+ if (state.currentDataset === 'pairs' && state.hazardFilter) { data = data.filter(row => row.hazardRef === state.hazardFilter); }
342
+ if (state.sortColumn) {
343
+ const isNumeric = config.columns.find(c => c.key === state.sortColumn)?.type === 'score';
344
+ data.sort((a, b) => { const valA = a[state.sortColumn], valB = b[state.sortColumn]; const comparison = isNumeric ? (valA || 0) - (valB || 0) : String(valA || '').localeCompare(String(valB || '')); return state.sortDirection === 'asc' ? comparison : -comparison; });
345
+ }
346
+ return data;
347
+ }
348
+
349
+ function populateFilters() {
350
+ // For main table view
351
+ const mainHazards = [...new Set(fdaData.pairs_all.map(item => item.hazardRef))].filter(Boolean).sort();
352
+ el.hazardFilter.innerHTML = '<option value="">Tous les Dangers</option>' + mainHazards.map(h => `<option value="${h}">${h}</option>`).join('');
353
+
354
+ // For comparison view
355
+ const commodities = [...new Set(fdaData.pairs_all.map(item => item.commodity))].sort();
356
  el.commodityFilter.innerHTML = commodities.map(c => `<option value="${c}">${c}</option>`).join('');
357
+ if (commodities.length > 0) {
358
  state.selectedCommodity = commodities[0];
359
  el.commodityFilter.value = state.selectedCommodity;
360
+ populateComparisonHazardFilter();
361
  }
362
  }
363
+
364
+ function populateComparisonHazardFilter(){
365
+ const hazards = [...new Set(fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity).map(p => p.hazardRef))].sort();
366
+ el.comparisonHazardFilter.innerHTML = '<option value="All">Tous les Dangers</option>' + hazards.map(h => `<option value="${h}">${h}</option>`).join('');
367
+ }
368
+
369
+ function updateDashboard(data) {
370
+ const scoreCol = viewConfig[state.currentDataset].scoreColumn;
371
+ const scores = data.map(i => i[scoreCol]).filter(s => typeof s === 'number');
372
+ el.metrics.totalProducts.textContent = data.length;
373
+ el.metrics.highRiskCount.textContent = scores.filter(s => s >= 400).length;
374
+ el.metrics.categoriesCount.textContent = new Set(data.map(i => i.commodity_category).filter(Boolean)).size;
375
+ el.metrics.avgScore.textContent = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 'N/A';
376
+ }
377
 
378
+ function showDetailPanel(rowIndex) {
379
+ const data = filterAndSortData()[rowIndex];
380
+ if (!data || !('C1' in data)) return;
381
+
382
+ el.detailPanel.classList.add('visible');
383
+ el.criteriaCanvas.style.display = 'block';
384
+ el.takeawaysSection.style.display = 'block';
385
+ el.criteriaChartTitle.textContent = `Analyse de "${data.commodity}"`;
386
+
387
+ if (state.charts.criteria) state.charts.criteria.destroy();
388
+ state.charts.criteria = new Chart(el.criteriaCanvas, {
389
+ type: 'radar',
390
+ data: { labels: ['C1-Gravité', 'C2-Expo.', 'C3-Dose', 'C4-Croissance', 'C5-Traitement', 'C6-Contam.', 'C7-Conso.'], datasets: [{ label: `Danger: ${data.hazardRef}`, data: [data.C1, data.C2, data.C3, data.C4, data.C5, data.C6, data.C7], backgroundColor: 'rgba(0, 51, 102, 0.2)', borderColor: 'rgba(0, 51, 102, 1)' }] },
391
+ options: { responsive: true, maintainAspectRatio: false, scales: { r: { beginAtZero: true, max: 9, ticks: { stepSize: 3 } } } }
392
+ });
393
+
394
+ const takeaways = [];
395
+ if (data.C1 > 6) takeaways.push("Le danger est <strong>sévère</strong>. La maîtrise du CCP est critique.");
396
+ if (data.C2 > 6) takeaways.push("Le produit est <strong>largement consommé</strong>. L'impact d'un rappel serait majeur.");
397
+ if (data.C4 > 6) takeaways.push("Le pathogène peut <strong>proliférer rapidement</strong>. <u>Action</u>: Valider la chaîne du froid et les barèmes de DLC/DDM.");
398
+ if (data.C5 < 3) takeaways.push("Pas d'<strong>étape d'assainissement</strong> efficace. <u>Action</u>: Maîtrise des matières premières essentielle.");
399
+ if (data.C6 > 6) takeaways.push("Risque de <strong>contamination à la source élevé</strong>. <u>Action</u>: Renforcer audits fournisseurs et contrôles à réception.");
400
+ if (data.C7 > 6) takeaways.push("Souvent <strong>consommé cru</strong>. Contrôles microbiologiques du produit fini à considérer.");
401
+ el.takeawaysList.innerHTML = takeaways.length ? takeaways.map(t => `<li><span class="icon">⚠️</span><span>${t}</span></li>`).join('') : "<li><span class='icon'>✓</span><span>Le risque est équilibré.</span></li>";
402
 
403
+ document.querySelectorAll('#tableBody tr').forEach(tr => tr.classList.remove('selected'));
404
+ const selectedRow = document.querySelector(`#tableBody tr[data-index="${rowIndex}"]`);
405
+ if(selectedRow) selectedRow.classList.add('selected');
406
+ el.detailPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
407
  }
408
 
409
  function setupEventListeners() {
410
+ el.searchInput.addEventListener('input', () => { state.searchTerm = el.searchInput.value; renderMainView(); });
411
+ el.hazardFilter.addEventListener('change', () => { state.hazardFilter = el.hazardFilter.value; renderMainView(); });
412
+ el.tableHead.addEventListener('click', (e) => { const th = e.target.closest('th[data-column]'); if (th) { state.sortColumn = th.dataset.column; state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'; renderMainView(); }});
413
+ el.tableBody.addEventListener('click', (e) => { if (state.currentDataset === 'pairs') { const row = e.target.closest('tr'); if (row) showDetailPanel(row.dataset.index); }});
414
+
415
  Object.keys(el.buttons).forEach(key => {
416
  el.buttons[key].addEventListener('click', () => {
417
  state.currentDataset = key;
418
+ if(key !== 'comparison') { state.sortColumn = viewConfig[key].scoreColumn; }
419
+ Object.assign(state, { searchTerm: '', hazardFilter: '', sortDirection: 'desc' });
420
+ el.searchInput.value = ''; el.hazardFilter.value = '';
421
  el.detailPanel.classList.remove('visible');
422
  render();
423
  });
424
  });
425
+
426
+ el.commodityFilter.addEventListener('change', (e) => { state.selectedCommodity = e.target.value; state.selectedHazard = 'All'; populateComparisonHazardFilter(); renderComparisonView(); });
427
+ el.comparisonHazardFilter.addEventListener('change', (e) => { state.selectedHazard = e.target.value; renderComparisonView(); });
428
+
429
+ el.guideBtn.addEventListener('click', () => { el.guideModal.style.display = 'block'; });
430
+ el.closeGuideBtn.addEventListener('click', () => { el.guideModal.style.display = 'none'; });
431
+ window.addEventListener('click', (event) => { if (event.target == el.guideModal) el.guideModal.style.display = 'none'; });
432
+ if (localStorage.getItem('hideWelcomeGuide')) el.welcomeGuide.style.display = 'none';
433
+ el.closeGuide.addEventListener('click', () => { el.welcomeGuide.style.display = 'none'; try { localStorage.setItem('hideWelcomeGuide', 'true'); } catch (e) {} });
434
  }
435
 
436
  loadAllData();