| | <!DOCTYPE html> |
| | <html lang="fr"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>FDA Risk Analyzer - Outil d'Analyse des Risques Alimentaires</title> |
| | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| | <style> |
| | :root { |
| | --primary-color: #003366; |
| | --primary-light: #0055a4; |
| | --accent-color: #fdb81e; |
| | --danger-color: #d93025; |
| | --warning-color: #f59e0b; |
| | --success-color: #1e8e3e; |
| | --bg-color: #f8f9fa; |
| | --surface-color: #ffffff; |
| | --text-primary: #202124; |
| | --text-secondary: #5f6368; |
| | --border-color: #dee2e6; |
| | --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); |
| | --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); |
| | --border-radius: 8px; |
| | --transition: all 0.3s ease-in-out; |
| | } |
| | |
| | * { |
| | box-sizing: border-box; |
| | margin: 0; |
| | padding: 0; |
| | } |
| | |
| | body { |
| | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
| | background-color: var(--bg-color); |
| | color: var(--text-primary); |
| | line-height: 1.5; |
| | } |
| | |
| | .header { |
| | background: var(--surface-color); |
| | border-bottom: 1px solid var(--border-color); |
| | padding: 1rem 0; |
| | position: sticky; |
| | top: 0; |
| | z-index: 1000; |
| | box-shadow: var(--shadow-sm); |
| | } |
| | |
| | .header-content { |
| | max-width: 1600px; |
| | margin: 0 auto; |
| | padding: 0 2rem; |
| | display: flex; |
| | align-items: center; |
| | justify-content: space-between; |
| | } |
| | |
| | .logo { |
| | display: flex; |
| | align-items: center; |
| | gap: 0.75rem; |
| | color: var(--primary-color); |
| | font-weight: 600; |
| | font-size: 1.25rem; |
| | } |
| | |
| | .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; |
| | } |
| | |
| | .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); |
| | display: flex; |
| | align-items: center; |
| | gap: 0.5rem; |
| | } |
| | |
| | .guide-btn:hover { |
| | background-color: var(--primary-light); |
| | } |
| | |
| | .container { |
| | max-width: 1600px; |
| | margin: 2rem auto; |
| | padding: 0 2rem; |
| | } |
| | |
| | .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; |
| | } |
| | |
| | .methodology-info { |
| | background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); |
| | border: 1px solid #0ea5e9; |
| | border-radius: var(--border-radius); |
| | margin-bottom: 2rem; |
| | overflow: hidden; |
| | box-shadow: var(--shadow-sm); |
| | } |
| | |
| | .methodology-header { |
| | background: var(--primary-color); |
| | color: white; |
| | padding: 1rem 1.5rem; |
| | cursor: pointer; |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | transition: var(--transition); |
| | } |
| | |
| | .methodology-header:hover { |
| | background-color: var(--primary-light); |
| | } |
| | |
| | .methodology-title { |
| | font-size: 1.1rem; |
| | font-weight: 600; |
| | margin: 0; |
| | display: flex; |
| | align-items: center; |
| | gap: 0.5rem; |
| | } |
| | |
| | .methodology-toggle { |
| | font-size: 1.2rem; |
| | transition: transform 0.3s ease; |
| | } |
| | |
| | .methodology-toggle.expanded { |
| | transform: rotate(180deg); |
| | } |
| | |
| | .methodology-content { |
| | max-height: 0; |
| | overflow: hidden; |
| | transition: max-height 0.5s ease-in-out, padding 0.5s ease-in-out; |
| | padding: 0 2rem; |
| | } |
| | |
| | .methodology-content.expanded { |
| | max-height: 3000px; |
| | padding: 2rem; |
| | } |
| | |
| | .methodology-inner { |
| | |
| | } |
| | |
| | .methodology-section { |
| | margin-bottom: 2rem; |
| | } |
| | |
| | .methodology-section:last-child { |
| | margin-bottom: 0; |
| | } |
| | |
| | .methodology-section h4 { |
| | color: var(--primary-color); |
| | font-size: 1.1rem; |
| | margin: 0 0 1rem 0; |
| | display: flex; |
| | align-items: center; |
| | gap: 0.5rem; |
| | } |
| | |
| | .methodology-section p { |
| | margin-bottom: 1rem; |
| | line-height: 1.6; |
| | } |
| | |
| | .methodology-section ul { |
| | padding-left: 1.5rem; |
| | margin-bottom: 1rem; |
| | } |
| | |
| | .methodology-section li { |
| | margin-bottom: 0.5rem; |
| | line-height: 1.5; |
| | } |
| | |
| | .highlight-box { |
| | background: rgba(251, 191, 36, 0.1); |
| | border-left: 4px solid #f59e0b; |
| | padding: 1rem; |
| | margin: 1rem 0; |
| | border-radius: 0 var(--border-radius) var(--border-radius) 0; |
| | } |
| | |
| | .reference-section { |
| | background: var(--bg-color); |
| | border-radius: var(--border-radius); |
| | padding: 1.5rem; |
| | margin-top: 1.5rem; |
| | } |
| | |
| | .reference-section h5 { |
| | color: var(--primary-color); |
| | margin: 0 0 1rem 0; |
| | font-size: 1rem; |
| | } |
| | |
| | .reference-list { |
| | list-style: none; |
| | padding: 0; |
| | margin: 0; |
| | } |
| | |
| | .reference-list li { |
| | margin-bottom: 0.75rem; |
| | padding-left: 1.5rem; |
| | position: relative; |
| | font-size: 0.9rem; |
| | } |
| | |
| | .reference-list li:before { |
| | content: "📄"; |
| | position: absolute; |
| | left: 0; |
| | } |
| | |
| | .reference-list a { |
| | color: var(--primary-light); |
| | text-decoration: none; |
| | } |
| | |
| | .reference-list a:hover { |
| | text-decoration: underline; |
| | } |
| | |
| | .haccp-integration { |
| | background: rgba(30, 142, 62, 0.1); |
| | border: 2px solid var(--success-color); |
| | border-radius: var(--border-radius); |
| | padding: 1.5rem; |
| | margin: 1.5rem 0; |
| | } |
| | |
| | .haccp-integration h5 { |
| | color: var(--success-color); |
| | margin: 0 0 1rem 0; |
| | display: flex; |
| | align-items: center; |
| | gap: 0.5rem; |
| | } |
| | |
| | .welcome-guide h3 { |
| | margin: 0 0 1rem 0; |
| | color: var(--primary-color); |
| | } |
| | |
| | .guide-steps { |
| | display: grid; |
| | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| | gap: 1.5rem; |
| | } |
| | |
| | .guide-step { |
| | font-size: 0.9rem; |
| | } |
| | |
| | .guide-step strong { |
| | color: var(--text-primary); |
| | } |
| | |
| | .close-guide { |
| | position: absolute; |
| | top: 0.5rem; |
| | right: 0.5rem; |
| | background: none; |
| | border: none; |
| | font-size: 1.5rem; |
| | cursor: pointer; |
| | color: var(--text-secondary); |
| | } |
| | |
| | .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); |
| | } |
| | |
| | .view-selector { |
| | display: flex; |
| | flex-wrap: wrap; |
| | gap: 0.5rem; |
| | border-bottom: 1px solid var(--border-color); |
| | padding-bottom: 1rem; |
| | margin-bottom: 1rem; |
| | } |
| | |
| | .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; |
| | transition: var(--transition); |
| | } |
| | |
| | .view-btn.active { |
| | color: var(--primary-light); |
| | background-color: #e7f3ff; |
| | border-color: var(--primary-light); |
| | } |
| | |
| | .view-btn:hover:not(.active) { |
| | background-color: #f0f7ff; |
| | } |
| | |
| | #main-view-controls { |
| | display: grid; |
| | grid-template-columns: 1fr auto; |
| | gap: 1rem; |
| | align-items: center; |
| | } |
| | |
| | #comparison-view-controls { |
| | display: grid; |
| | grid-template-columns: 1fr 1fr; |
| | gap: 1rem; |
| | align-items: center; |
| | } |
| | |
| | .search-container { |
| | position: relative; |
| | } |
| | |
| | .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); |
| | } |
| | |
| | .search-input:focus { |
| | outline: none; |
| | border-color: var(--primary-light); |
| | box-shadow: 0 0 0 3px rgba(0, 85, 164, 0.1); |
| | } |
| | |
| | .search-icon { |
| | position: absolute; |
| | left: 0.75rem; |
| | top: 50%; |
| | transform: translateY(-50%); |
| | color: var(--text-secondary); |
| | } |
| | |
| | .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%; |
| | } |
| | |
| | .dashboard { |
| | display: grid; |
| | grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
| | gap: 1.5rem; |
| | margin-bottom: 2rem; |
| | } |
| | |
| | .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); |
| | text-align: center; |
| | } |
| | |
| | .metric-label { |
| | font-size: 0.85rem; |
| | color: var(--text-secondary); |
| | margin-bottom: 0.5rem; |
| | } |
| | |
| | .metric-value { |
| | font-size: 1.75rem; |
| | font-weight: 600; |
| | color: var(--text-primary); |
| | } |
| | |
| | .metric-card.high-risk { |
| | border-top: 4px solid var(--danger-color); |
| | } |
| | |
| | .metric-card.medium-risk { |
| | border-top: 4px solid var(--warning-color); |
| | } |
| | |
| | .metric-card.low-risk { |
| | border-top: 4px solid var(--success-color); |
| | } |
| | |
| | #detail-panel { |
| | max-height: 0; |
| | overflow: hidden; |
| | transition: max-height 0.5s ease-in-out, margin 0.5s ease-in-out; |
| | margin-bottom: 0; |
| | } |
| | |
| | #detail-panel.visible { |
| | max-height: 1000px; |
| | margin-bottom: 2rem; |
| | } |
| | |
| | .detail-card { |
| | background-color: var(--surface-color); |
| | border-radius: var(--border-radius); |
| | border: 1px solid var(--border-color); |
| | box-shadow: var(--shadow-md); |
| | padding: 1.5rem; |
| | } |
| | |
| | .detail-layout { |
| | display: grid; |
| | grid-template-columns: 1fr 1fr; |
| | gap: 2rem; |
| | align-items: start; |
| | } |
| | |
| | .chart-title { |
| | font-weight: 600; |
| | color: var(--text-primary); |
| | margin: 0 0 1rem 0; |
| | font-size: 1.1rem; |
| | word-break: break-word; |
| | } |
| | |
| | .chart-canvas-container { |
| | position: relative; |
| | height: 320px; |
| | } |
| | |
| | #comparison-chart-container { |
| | min-height: 600px; |
| | height: auto; |
| | } |
| | |
| | #takeaways-section h4 { |
| | margin: 0 0 1rem 0; |
| | color: var(--primary-color); |
| | } |
| | |
| | #takeaways-list { |
| | list-style: none; |
| | padding: 0; |
| | margin: 0; |
| | display: flex; |
| | flex-direction: column; |
| | gap: 0.75rem; |
| | } |
| | |
| | #takeaways-list li { |
| | display: flex; |
| | align-items: flex-start; |
| | gap: 0.75rem; |
| | font-size: 0.9rem; |
| | } |
| | |
| | #takeaways-list li .icon { |
| | color: var(--warning-color); |
| | font-weight: bold; |
| | margin-top: 2px; |
| | } |
| | |
| | .table-container { |
| | background: var(--surface-color); |
| | border-radius: var(--border-radius); |
| | box-shadow: var(--shadow-md); |
| | border: 1px solid var(--border-color); |
| | overflow: hidden; |
| | } |
| | |
| | .table-header { |
| | padding: 1rem 1.5rem; |
| | border-bottom: 1px solid var(--border-color); |
| | display: flex; |
| | justify-content: space-between; |
| | align-items: center; |
| | } |
| | |
| | .table-title { |
| | font-weight: 600; |
| | margin: 0; |
| | } |
| | |
| | .results-count { |
| | color: var(--text-secondary); |
| | font-size: 0.9rem; |
| | } |
| | |
| | .table-wrapper { |
| | overflow-x: auto; |
| | } |
| | |
| | table { |
| | width: 100%; |
| | border-collapse: collapse; |
| | } |
| | |
| | th, td { |
| | padding: 1rem 1.5rem; |
| | text-align: left; |
| | border-bottom: 1px solid var(--border-color); |
| | white-space: nowrap; |
| | } |
| | |
| | tbody tr:last-child td { |
| | border-bottom: none; |
| | } |
| | |
| | th { |
| | background: var(--bg-color); |
| | font-weight: 600; |
| | color: var(--text-secondary); |
| | font-size: 0.85rem; |
| | text-transform: uppercase; |
| | } |
| | |
| | th.sortable { |
| | cursor: pointer; |
| | position: relative; |
| | } |
| | |
| | th.sortable:hover { |
| | color: var(--text-primary); |
| | } |
| | |
| | th.sortable::after { |
| | content: '↕'; |
| | position: absolute; |
| | right: 0.75rem; |
| | opacity: 0.3; |
| | } |
| | |
| | th.sort-asc::after { |
| | content: '↑'; |
| | opacity: 1; |
| | } |
| | |
| | th.sort-desc::after { |
| | content: '↓'; |
| | opacity: 1; |
| | } |
| | |
| | tbody tr.clickable { |
| | cursor: pointer; |
| | } |
| | |
| | tbody tr:hover { |
| | background-color: #e9ecef; |
| | } |
| | |
| | tbody tr.selected { |
| | background-color: #dbeafe !important; |
| | font-weight: 500; |
| | } |
| | |
| | .score-badge { |
| | display: inline-block; |
| | padding: 0.2rem 0.6rem; |
| | border-radius: 1rem; |
| | font-size: 0.8rem; |
| | font-weight: 700; |
| | } |
| | |
| | .score-high { |
| | background-color: #fdeded; |
| | color: #a50e0e; |
| | } |
| | |
| | .score-medium { |
| | background-color: #fff8e1; |
| | color: #6f4f12; |
| | } |
| | |
| | .score-low { |
| | background-color: #e6f4ea; |
| | color: #1b5e20; |
| | } |
| | |
| | #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; |
| | } |
| | |
| | .spinner { |
| | width: 50px; |
| | height: 50px; |
| | border: 5px solid rgba(0, 51, 102, 0.2); |
| | border-top: 5px solid var(--primary-color); |
| | border-radius: 50%; |
| | animation: spin 1s linear infinite; |
| | margin: 0 auto 1rem; |
| | } |
| | |
| | @keyframes spin { |
| | 0% { transform: rotate(0deg); } |
| | 100% { transform: rotate(360deg); } |
| | } |
| | |
| | .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); |
| | } |
| | |
| | .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; |
| | } |
| | |
| | .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; |
| | } |
| | |
| | .modal-title { |
| | font-size: 1.5rem; |
| | color: var(--primary-color); |
| | margin: 0; |
| | } |
| | |
| | .close-modal { |
| | font-size: 2rem; |
| | font-weight: bold; |
| | color: var(--text-secondary); |
| | cursor: pointer; |
| | } |
| | |
| | .close-modal:hover { |
| | color: var(--text-primary); |
| | } |
| | |
| | .criteria-grid { |
| | display: grid; |
| | grid-template-columns: 1fr 1fr; |
| | gap: 1rem; |
| | margin-top: 1rem; |
| | } |
| | |
| | .criteria-card { |
| | background: var(--bg-color); |
| | padding: 1rem; |
| | border-radius: var(--border-radius); |
| | } |
| | |
| | .criteria-title { |
| | font-weight: 600; |
| | display: block; |
| | margin-bottom: 0.25rem; |
| | } |
| | |
| | .criteria-desc { |
| | font-size: 0.9rem; |
| | color: var(--text-secondary); |
| | } |
| | |
| | .modal-footer { |
| | margin-top: 2rem; |
| | padding-top: 1rem; |
| | border-top: 1px solid var(--border-color); |
| | text-align: center; |
| | font-size: 0.9rem; |
| | } |
| | |
| | .modal-footer a { |
| | color: var(--primary-light); |
| | } |
| | |
| | @keyframes fadeIn { |
| | from { opacity: 0; transform: translateY(-20px); } |
| | to { opacity: 1; transform: translateY(0); } |
| | } |
| | |
| | @media (max-width: 1200px) { |
| | #main-view-controls, #comparison-view-controls { |
| | grid-template-columns: 1fr; |
| | } |
| | .detail-layout { |
| | grid-template-columns: 1fr; |
| | } |
| | } |
| | |
| | @media (max-width: 768px) { |
| | .header-content, .container { |
| | padding: 0 1rem; |
| | } |
| | .view-selector { |
| | flex-direction: column; |
| | } |
| | .dashboard { |
| | grid-template-columns: 1fr; |
| | } |
| | } |
| | |
| | .risk-indicator { |
| | display: inline-block; |
| | width: 12px; |
| | height: 12px; |
| | border-radius: 50%; |
| | margin-right: 8px; |
| | } |
| | |
| | .risk-high { |
| | background-color: var(--danger-color); |
| | } |
| | |
| | .risk-medium { |
| | background-color: var(--warning-color); |
| | } |
| | |
| | .risk-low { |
| | background-color: var(--success-color); |
| | } |
| | |
| | .footer { |
| | text-align: center; |
| | padding: 2rem; |
| | color: var(--text-secondary); |
| | font-size: 0.9rem; |
| | border-top: 1px solid var(--border-color); |
| | margin-top: 2rem; |
| | } |
| | |
| | .error-message { |
| | color: var(--danger-color); |
| | text-align: center; |
| | padding: 2rem; |
| | font-weight: 500; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <div id="loading-overlay"> |
| | <div> |
| | <div class="spinner"></div> |
| | <h2>Chargement des données...</h2> |
| | <p>Veuillez patienter.</p> |
| | </div> |
| | </div> |
| | |
| | <header class="header"> |
| | <div class="header-content"> |
| | <div class="logo"> |
| | <div class="logo-icon">🛡️</div> |
| | <span>FDA Risk Analyzer</span> |
| | </div> |
| | <button class="guide-btn" id="guide-btn"> |
| | <i class="fas fa-book"></i> |
| | Guide d'Utilisation |
| | </button> |
| | </div> |
| | </header> |
| |
|
| | <div class="container"> |
| | <div class="welcome-guide" id="welcome-guide"> |
| | <button class="close-guide" id="close-guide">×</button> |
| | <h3>Guide Rapide de l'Outil</h3> |
| | <div class="guide-steps"> |
| | <div class="guide-step"> |
| | <strong>1. Choisissez une Vue</strong><br> |
| | Utilisez les boutons ci-dessous pour sélectionner votre niveau d'analyse. |
| | </div> |
| | <div class="guide-step"> |
| | <strong>2. Filtrez les Données</strong><br> |
| | Utilisez la recherche et les filtres pour affiner les résultats. |
| | </div> |
| | <div class="guide-step"> |
| | <strong>3. Analysez en Profondeur</strong><br> |
| | Cliquez sur les lignes du tableau ou utilisez la vue comparative. |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="methodology-info" id="methodology-info"> |
| | <div class="methodology-header" id="methodology-header"> |
| | <h3 class="methodology-title"> |
| | 🧬 Comprendre la Méthodologie FDA de Classement des Risques |
| | </h3> |
| | <span class="methodology-toggle" id="methodology-toggle">▼</span> |
| | </div> |
| | <div class="methodology-content" id="methodology-content"> |
| | <div class="methodology-inner"> |
| | <div class="methodology-section"> |
| | <h4>🎯 Qu'est-ce que le Modèle de Classement des Risques de la FDA ?</h4> |
| | <p> |
| | Le <strong>FDA Risk-Ranking Model</strong> est un outil scientifique développé par l'administration américaine (FDA) pour <strong>prioriser les efforts de surveillance</strong> et de contrôle dans l'industrie alimentaire. Il permet d'identifier quels couples produit/danger représentent les risques les plus élevés pour la santé publique. |
| | </p> |
| | <div class="highlight-box"> |
| | <strong>🔍 Objectif principal :</strong> Aider les autorités sanitaires et les professionnels de l'alimentaire à concentrer leurs ressources limitées sur les risques qui comptent vraiment. |
| | </div> |
| | </div> |
| |
|
| | <div class="methodology-section"> |
| | <h4>⚙️ Comment ça fonctionne : Les 7 Critères d'Évaluation</h4> |
| | <p>Chaque couple produit/pathogène est évalué selon 7 critères scientifiques, notés de 1 à 9 :</p> |
| | <ul> |
| | <li><strong>C1 - Gravité :</strong> Sévérité des effets sur la santé (mortalité, hospitalisation)</li> |
| | <li><strong>C2 - Exposition :</strong> Fréquence et quantité de consommation du produit</li> |
| | <li><strong>C3 - Dose-Réponse :</strong> Quantité minimale de pathogène nécessaire pour déclencher la maladie</li> |
| | <li><strong>C4 - Croissance :</strong> Capacité du pathogène à survivre/proliférer dans l'aliment</li> |
| | <li><strong>C5 - Traitement :</strong> Efficacité des procédés de décontamination</li> |
| | <li><strong>C6 - Contamination :</strong> Probabilité de contamination à la production</li> |
| | <li><strong>C7 - Consommation :</strong> Mode de préparation (cru, cuit, transformé)</li> |
| | </ul> |
| | <p>Le <strong>score final</strong> est calculé en multipliant ces 7 critères, créant un classement objectif des risques.</p> |
| | </div> |
| |
|
| | <div class="methodology-section"> |
| | <h4>📊 Pourquoi c'est révolutionnaire ?</h4> |
| | <p>Avant ce modèle, les décisions étaient souvent basées sur :</p> |
| | <ul> |
| | <li>❌ L'intuition ou l'expérience personnelle</li> |
| | <li>❌ Les crises médiatiques du moment</li> |
| | <li>❌ Des approches fragmentées par produit OU par pathogène</li> |
| | </ul> |
| | <p>Le modèle FDA apporte :</p> |
| | <ul> |
| | <li>✅ Une approche <strong>scientifique et quantitative</strong></li> |
| | <li>✅ Une vision <strong>globale</strong> produit × pathogène</li> |
| | <li>✅ Une <strong>transparence</strong> dans les décisions de priorisation</li> |
| | <li>✅ Une <strong>comparabilité</strong> entre différents risques</li> |
| | </ul> |
| | </div> |
| |
|
| | <div class="haccp-integration"> |
| | <h5>🛡️ Intégration avec votre Plan HACCP</h5> |
| | <p><strong>Ce modèle ne remplace pas HACCP, il le renforce !</strong></p> |
| | <ul> |
| | <li><strong>Analyse des Dangers :</strong> Utilisez les scores pour prioriser vos dangers significatifs (étape 1 HACCP)</li> |
| | <li><strong>Points Critiques :</strong> Concentrez vos CCP sur les couples à haut score</li> |
| | <li><strong>Surveillance :</strong> Adaptez la fréquence de monitoring selon les scores de risque</li> |
| | <li><strong>Validation :</strong> Justifiez scientifiquement vos choix de maîtrise</li> |
| | <li><strong>Audits :</strong> Orientez vos vérifications sur les zones à plus haut risque</li> |
| | </ul> |
| | </div> |
| |
|
| | <div class="methodology-section"> |
| | <h4>💼 Applications Pratiques en Industrie</h4> |
| | <ul> |
| | <li><strong>Responsables Qualité :</strong> Priorisation des plans de surveillance et contrôles</li> |
| | <li><strong>Acheteurs :</strong> Évaluation et sélection des fournisseurs selon les risques</li> |
| | <li><strong>R&D :</strong> Conception de produits avec maîtrise des risques intégrée</li> |
| | <li><strong>Direction :</strong> Allocation des budgets sécurité alimentaire basée sur les risques réels</li> |
| | <li><strong>Auditeurs :</strong> Focus des audits sur les couples produit/danger critiques</li> |
| | </ul> |
| | </div> |
| |
|
| | <div class="methodology-section"> |
| | <h4>🌍 Food Traceability List (FTL)</h4> |
| | <p> |
| | La <strong>Food Traceability List</strong> est une liste des aliments à plus haut risque, identifiés grâce à ce modèle. |
| | Ces produits sont soumis à des <strong>exigences de traçabilité renforcée</strong> aux États-Unis depuis janvier 2026. |
| | </p> |
| | <div class="highlight-box"> |
| | <strong>💡 Conseil :</strong> Même en Europe, surveiller ces produits FTL peut vous donner un avantage concurrentiel et anticiper les évolutions réglementaires. |
| | </div> |
| | </div> |
| |
|
| | <div class="reference-section"> |
| | <h5>📚 Sources et Références Officielles</h5> |
| | <ul class="reference-list"> |
| | <li> |
| | <a href="https://www.fda.gov/food/cfsan-constituent-updates/fda-releases-risk-ranking-model-food-tracing-final-rule" target="_blank"> |
| | FDA Risk-Ranking Model for Food Tracing Final Rule (2022) |
| | </a> |
| | </li> |
| | <li> |
| | <a href="https://hfpappexternal.fda.gov/scripts/FDARiskRankingModelforFoodTracingfinalrule/" target="_blank"> |
| | Outil Officiel FDA Risk-Ranking Model (Interface Interactive) |
| | </a> |
| | </li> |
| | <li> |
| | <a href="https://www.federalregister.gov/documents/2022/11/21/2022-24417/requirements-for-additional-traceability-records-for-certain-foods" target="_blank"> |
| | Federal Register - Règlement Traçabilité Alimentaire (21 CFR 204) |
| | </a> |
| | </li> |
| | <li> |
| | <a href="https://www.fda.gov/media/161908/download" target="_blank"> |
| | Document Technique FDA : "Risk-Ranking Model for Food Tracing" (PDF) |
| | </a> |
| | </li> |
| | <li> |
| | <strong>Publications Scientifiques :</strong> Journal of Food Protection, Risk Analysis, Food Control - rechercher "FDA risk-ranking model" |
| | </li> |
| | </ul> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="controls-panel"> |
| | <div class="view-selector"> |
| | <button class="view-btn active" id="btn-ftl">Vue FTL</button> |
| | <button class="view-btn" id="btn-all">Vue Globale</button> |
| | <button class="view-btn" id="btn-pairs">Analyse Détaillée</button> |
| | <button class="view-btn" id="btn-comparison">Analyse Comparative</button> |
| | </div> |
| | |
| | <div id="main-controls"> |
| | <div class="controls-grid"> |
| | <div class="search-container"> |
| | <span class="search-icon">🔍</span> |
| | <input type="search" class="search-input" id="searchInput" placeholder="Rechercher..."> |
| | </div> |
| | <div id="hazard-filter-container" style="display: none;"> |
| | <select class="filter-select" id="hazard-filter"> |
| | <option value="">Tous les Dangers</option> |
| | </select> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div id="comparison-controls" style="display: none;"> |
| | <div class="controls-grid" style="grid-template-columns: 1fr 1fr;"> |
| | <select class="filter-select" id="commodity-filter"></select> |
| | <select class="filter-select" id="comparison-hazard-filter"> |
| | <option value="All">Tous les Dangers</option> |
| | </select> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div id="main-view-content" class="active"> |
| | <div class="dashboard"> |
| | <div class="metric-card"> |
| | <div class="metric-label">Produits Affichés</div> |
| | <div class="metric-value" id="total-products">-</div> |
| | </div> |
| | <div class="metric-card high-risk"> |
| | <div class="metric-label">Haut Risque (≥400)</div> |
| | <div class="metric-value" id="high-risk-count">-</div> |
| | </div> |
| | <div class="metric-card"> |
| | <div class="metric-label">Catégories Uniques</div> |
| | <div class="metric-value" id="categories-count">-</div> |
| | </div> |
| | <div class="metric-card"> |
| | <div class="metric-label">Score Moyen</div> |
| | <div class="metric-value" id="avg-score">-</div> |
| | </div> |
| | </div> |
| | |
| | <div id="detail-panel"> |
| | <div class="detail-card"> |
| | <div class="detail-layout"> |
| | <div> |
| | <h3 class="chart-title" id="criteria-chart-title"></h3> |
| | <div class="chart-canvas-container"> |
| | <canvas id="criteriaChart"></canvas> |
| | </div> |
| | </div> |
| | <div id="takeaways-section"> |
| | <h4>Points de Vigilance & Actions</h4> |
| | <ul id="takeaways-list"></ul> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="table-container"> |
| | <div class="table-header"> |
| | <h3 class="table-title" id="table-title"></h3> |
| | <div class="results-count" id="results-count"></div> |
| | </div> |
| | <div class="table-wrapper"> |
| | <table id="dataTable"> |
| | <thead id="tableHead"></thead> |
| | <tbody id="tableBody"></tbody> |
| | </table> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div id="comparison-view-content" style="display:none;"> |
| | <div class="chart-container"> |
| | <h3 class="chart-title" id="stacked-chart-title">Analyse des Critères Pondérés</h3> |
| | <div class="chart-canvas-container" id="comparison-chart-container"> |
| | <canvas id="stackedBarChart"></canvas> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div class="footer"> |
| | <p>FDA Risk Analyzer - Outil d'Analyse des Risques Alimentaires basé sur le modèle de la FDA</p> |
| | <p>© 2023 Tous droits réservés</p> |
| | </div> |
| | |
| | <div id="guide-modal" class="modal"> |
| | <div class="modal-content"> |
| | <div class="modal-header"> |
| | <h2 class="modal-title">Guide d'Utilisation</h2> |
| | <span class="close-modal" id="close-guide-btn">×</span> |
| | </div> |
| | |
| | <h3>Comment utiliser cet outil</h3> |
| | <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> |
| | |
| | <h4>Les Vues d'Analyse</h4> |
| | <ul> |
| | <li><strong>Vue FTL</strong> Se concentre sur les produits à plus haut risque de la Food Traceability List. Idéal pour les audits.</li> |
| | <li><strong>Vue Globale</strong> Affiche tous les produits de la base de données pour une vue d'ensemble.</li> |
| | <li><strong>Analyse Détaillée</strong> Montre les couples produit/danger. Cliquez sur une ligne pour une analyse approfondie (graphique radar et actions recommandées).</li> |
| | <li><strong>Analyse Comparative</strong> Permet de comparer visuellement les contributions des différents facteurs de risque pour un produit et ses dangers.</li> |
| | </ul> |
| | |
| | <h3>Les 7 Critères d'Évaluation de la FDA</h3> |
| | <div class="criteria-grid"> |
| | <div class="criteria-card"> |
| | <div class="criteria-title">C1: Gravité</div> |
| | <div class="criteria-desc">Gravité des effets sur la santé (mortalité, morbidité, hospitalisation).</div> |
| | </div> |
| | <div class="criteria-card"> |
| | <div class="criteria-title">C2: Exposition</div> |
| | <div class="criteria-desc">Probabilité d'exposition au danger (fréquence de consommation, quantité).</div> |
| | </div> |
| | <div class="criteria-card"> |
| | <div class="criteria-title">C3: Dose-Réponse</div> |
| | <div class="criteria-desc">Dose infectieuse minimale requise pour provoquer la maladie.</div> |
| | </div> |
| | <div class="criteria-card"> |
| | <div class="criteria-title">C4: Croissance</div> |
| | <div class="criteria-desc">Capacité du pathogène à croître/survivre dans l'aliment.</div> |
| | </div> |
| | <div class="criteria-card"> |
| | <div class="criteria-title">C5: Traitement</div> |
| | <div class="criteria-desc">Efficacité des traitements de décontamination appliqués.</div> |
| | </div> |
| | <div class="criteria-card"> |
| | <div class="criteria-title">C6: Contamination</div> |
| | <div class="criteria-desc">Probabilité de contamination à la production (sources, environnement).</div> |
| | </div> |
| | <div class="criteria-card"> |
| | <div class="criteria-title">C7: Consommation</div> |
| | <div class="criteria-desc">Mode de préparation/consommation (cru, cuit, transformé).</div> |
| | </div> |
| | </div> |
| | |
| | <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> |
| | </div> |
| | </div> |
| |
|
| | <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| | <script> |
| | document.addEventListener('DOMContentLoaded', function() { |
| | |
| | const state = { |
| | currentDataset: 'ftl', |
| | searchTerm: '', |
| | hazardFilter: '', |
| | sortColumn: 'agg_score', |
| | sortDirection: 'desc', |
| | selectedCommodity: '', |
| | selectedHazard: 'All', |
| | charts: {} |
| | }; |
| | |
| | |
| | let fdaData = { |
| | aggregated_ftl: [], |
| | aggregated_all: [], |
| | pairs_all: [] |
| | }; |
| | |
| | const viewConfig = { |
| | 'ftl': { |
| | dataKey: 'aggregated_ftl', |
| | title: 'Vue FTL (Produits à Traçabilité Renforcée)', |
| | 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' |
| | }, |
| | 'all': { |
| | dataKey: 'aggregated_all', |
| | title: 'Vue Globale (Tous les Produits)', |
| | 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' |
| | }, |
| | 'pairs': { |
| | dataKey: 'pairs_all', |
| | title: 'Analyse Détaillée (Produit x Danger)', |
| | 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' |
| | } |
| | }; |
| | |
| | const el = { |
| | loadingOverlay: document.getElementById('loading-overlay'), |
| | mainViewContent: document.getElementById('main-view-content'), |
| | comparisonViewContent: document.getElementById('comparison-view-content'), |
| | mainControls: document.getElementById('main-controls'), |
| | comparisonControls: document.getElementById('comparison-controls'), |
| | searchInput: document.getElementById('searchInput'), |
| | hazardFilter: document.getElementById('hazard-filter'), |
| | hazardFilterContainer: document.getElementById('hazard-filter-container'), |
| | commodityFilter: document.getElementById('commodity-filter'), |
| | comparisonHazardFilter: document.getElementById('comparison-hazard-filter'), |
| | buttons: { |
| | ftl: document.getElementById('btn-ftl'), |
| | all: document.getElementById('btn-all'), |
| | pairs: document.getElementById('btn-pairs'), |
| | comparison: document.getElementById('btn-comparison') |
| | }, |
| | metrics: { |
| | totalProducts: document.getElementById('total-products'), |
| | highRiskCount: document.getElementById('high-risk-count'), |
| | categoriesCount: document.getElementById('categories-count'), |
| | avgScore: document.getElementById('avg-score') |
| | }, |
| | detailPanel: document.getElementById('detail-panel'), |
| | criteriaChartTitle: document.getElementById('criteria-chart-title'), |
| | criteriaCanvas: document.getElementById('criteriaChart'), |
| | takeawaysSection: document.getElementById('takeaways-section'), |
| | takeawaysList: document.getElementById('takeaways-list'), |
| | tableTitle: document.getElementById('table-title'), |
| | tableHead: document.getElementById('tableHead'), |
| | tableBody: document.getElementById('tableBody'), |
| | resultsCount: document.getElementById('results-count'), |
| | stackedBarChartCanvas: document.getElementById('stackedBarChart'), |
| | stackedChartTitle: document.getElementById('stacked-chart-title'), |
| | welcomeGuide: document.getElementById('welcome-guide'), |
| | closeGuide: document.getElementById('close-guide'), |
| | guideModal: document.getElementById('guide-modal'), |
| | guideBtn: document.getElementById('guide-btn'), |
| | closeGuideBtn: document.getElementById('close-guide-btn') |
| | }; |
| | |
| | const formatCell = (value, type) => { |
| | if (value === undefined || value === null) return ''; |
| | const cleanValue = String(value); |
| | if (type === 'score') return `<span class="score-badge ${parseInt(cleanValue) >= 400 ? 'score-high' : parseInt(cleanValue) >= 200 ? 'score-medium' : 'score-low'}">${cleanValue}</span>`; |
| | if (type === 'boolean') return cleanValue === 'true' ? '<span style="color: var(--success-color); font-weight: 600;">✓</span>' : '<span style="color: var(--text-secondary);">-</span>'; |
| | return cleanValue; |
| | }; |
| | |
| | const filterAndSortData = () => { |
| | const config = viewConfig[state.currentDataset]; |
| | if (!config) return []; |
| | let data = [...fdaData[config.dataKey]]; |
| | |
| | if (state.searchTerm) { |
| | const search = state.searchTerm.toLowerCase(); |
| | data = data.filter(row => |
| | Object.values(row).some(v => |
| | String(v).toLowerCase().includes(search) |
| | ) |
| | ); |
| | } |
| | |
| | if (state.currentDataset === 'pairs' && state.hazardFilter) { |
| | data = data.filter(row => row.hazardRef === state.hazardFilter); |
| | } |
| | |
| | if (state.sortColumn) { |
| | const isNumeric = config.columns.find(c => c.key === state.sortColumn)?.type === 'score'; |
| | data.sort((a, b) => { |
| | const valA = a[state.sortColumn]; |
| | const valB = b[state.sortColumn]; |
| | |
| | if (isNumeric) { |
| | return state.sortDirection === 'asc' ? |
| | (valA || 0) - (valB || 0) : |
| | (valB || 0) - (valA || 0); |
| | } else { |
| | const strA = String(valA || '').toLowerCase(); |
| | const strB = String(valB || '').toLowerCase(); |
| | return state.sortDirection === 'asc' ? |
| | strA.localeCompare(strB) : |
| | strB.localeCompare(strA); |
| | } |
| | }); |
| | } |
| | |
| | return data; |
| | }; |
| | |
| | const render = () => { |
| | const isComparisonView = state.currentDataset === 'comparison'; |
| | el.mainViewContent.style.display = isComparisonView ? 'none' : 'block'; |
| | el.comparisonViewContent.style.display = isComparisonView ? 'block' : 'none'; |
| | el.mainControls.style.display = isComparisonView ? 'none' : 'block'; |
| | el.comparisonControls.style.display = isComparisonView ? 'block' : 'none'; |
| | |
| | Object.values(el.buttons).forEach(btn => btn.classList.remove('active')); |
| | el.buttons[state.currentDataset].classList.add('active'); |
| | |
| | if (isComparisonView) { |
| | renderComparisonView(); |
| | } else { |
| | renderMainView(); |
| | } |
| | }; |
| | |
| | const renderMainView = () => { |
| | const config = viewConfig[state.currentDataset]; |
| | const data = filterAndSortData(); |
| | |
| | el.tableTitle.textContent = config.title; |
| | el.resultsCount.textContent = `${data.length} résultat(s)`; |
| | |
| | |
| | el.tableHead.innerHTML = `<tr>${config.columns.map(c => |
| | `<th class="${c.sortable ? 'sortable' : ''}" data-column="${c.key}">${c.label}</th>` |
| | ).join('')}</tr>`; |
| | |
| | |
| | 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}`); |
| | } |
| | }); |
| | |
| | |
| | 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(''); |
| | |
| | updateDashboard(data); |
| | el.hazardFilterContainer.style.display = state.currentDataset === 'pairs' ? 'flex' : 'none'; |
| | |
| | if (state.currentDataset !== 'pairs') { |
| | el.detailPanel.classList.remove('visible'); |
| | } |
| | }; |
| | |
| | const renderComparisonView = () => { |
| | let data = fdaData.pairs_all.filter(p => p.commodity === state.selectedCommodity); |
| | if (state.selectedHazard !== 'All') { |
| | data = data.filter(p => p.hazardRef === state.selectedHazard); |
| | } |
| | |
| | el.stackedChartTitle.textContent = `Analyse des Critères Pondérés pour : ${state.selectedCommodity}`; |
| | |
| | if (state.charts.stackedBar) { |
| | state.charts.stackedBar.destroy(); |
| | } |
| | |
| | const criteria = ['C1w', 'C2w', 'C3w', 'C4w', 'C5w', 'C6w', 'C7w']; |
| | const labels = [ |
| | 'C1: Gravité', |
| | 'C2: Exposition', |
| | 'C3: Dose-Réponse', |
| | 'C4: Croissance', |
| | 'C5: Traitement', |
| | 'C6: Contamination', |
| | 'C7: Consommation' |
| | ]; |
| | const colors = [ |
| | '#C62828', '#AD1457', '#6A1B9A', |
| | '#4527A0', '#283593', '#1565C0', '#0277BD' |
| | ]; |
| | |
| | if (data.length === 0) { |
| | document.getElementById('comparison-chart-container').innerHTML = '<p style="text-align:center; padding:2rem;">Aucune donnée disponible pour cette sélection</p>'; |
| | return; |
| | } |
| | |
| | state.charts.stackedBar = new Chart(el.stackedBarChartCanvas, { |
| | type: 'bar', |
| | data: { |
| | labels: data.map(d => d.hazardRef), |
| | datasets: criteria.map((c, i) => ({ |
| | label: labels[i], |
| | data: data.map(d => d[c]), |
| | backgroundColor: colors[i] |
| | })) |
| | }, |
| | 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' |
| | } |
| | } |
| | } |
| | }); |
| | }; |
| | |
| | const updateDashboard = (data) => { |
| | const scoreCol = viewConfig[state.currentDataset].scoreColumn; |
| | const scores = data.map(i => i[scoreCol]).filter(s => typeof s === 'number'); |
| | |
| | el.metrics.totalProducts.textContent = data.length; |
| | el.metrics.highRiskCount.textContent = scores.filter(s => s >= 400).length; |
| | el.metrics.categoriesCount.textContent = new Set(data.map(i => i.commodity_category).filter(Boolean)).size; |
| | el.metrics.avgScore.textContent = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 'N/A'; |
| | }; |
| | |
| | const showDetailPanel = (rowIndex) => { |
| | const data = filterAndSortData()[rowIndex]; |
| | if (!data || !('C1' in data)) return; |
| | |
| | el.detailPanel.classList.add('visible'); |
| | el.criteriaCanvas.style.display = 'block'; |
| | el.takeawaysSection.style.display = 'block'; |
| | el.criteriaChartTitle.textContent = `Analyse de "${data.commodity}"`; |
| | |
| | if (state.charts.criteria) { |
| | state.charts.criteria.destroy(); |
| | } |
| | |
| | |
| | const cleanedProduct = data.commodity.replace(/<[^>]+>/g, ''); |
| | const cleanedHazard = data.hazardRef.replace(/<[^>]+>/g, ''); |
| | el.criteriaChartTitle.textContent = `Analyse "${cleanedProduct}" × Danger "${cleanedHazard}"`; |
| | |
| | state.charts.criteria = new Chart(el.criteriaCanvas, { |
| | type: 'radar', |
| | data: { |
| | labels: [ |
| | 'C1-Gravité', |
| | 'C2-Exposition', |
| | 'C3-Dose-Réponse', |
| | 'C4-Croissance', |
| | 'C5-Traitement', |
| | 'C6-Contamination', |
| | 'C7-Consommation' |
| | ], |
| | datasets: [{ |
| | label: `${cleanedHazard}`, |
| | 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)', |
| | pointBackgroundColor: 'rgba(0, 51, 102, 1)' |
| | }] |
| | }, |
| | options: { |
| | responsive: true, |
| | maintainAspectRatio: false, |
| | scales: { |
| | r: { |
| | beginAtZero: true, |
| | max: 9, |
| | ticks: { |
| | stepSize: 3 |
| | } |
| | } |
| | }, |
| | plugins: { |
| | legend: { |
| | display: false |
| | } |
| | } |
| | } |
| | }); |
| | |
| | const takeaways = []; |
| | if (data.C1 > 6) takeaways.push("Le danger associé est <strong>sévère</strong>. La maîtrise du CCP est critique."); |
| | if (data.C2 > 6) takeaways.push("Le produit est <strong>largement consommé</strong>. L'impact d'un rappel serait majeur."); |
| | 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."); |
| | 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."); |
| | 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."); |
| | if (data.C7 > 6) takeaways.push("Souvent <strong>consommé cru</strong>. Contrôles microbiologiques du produit fini à considérer."); |
| | |
| | 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>"; |
| | |
| | document.querySelectorAll('#tableBody tr').forEach(tr => tr.classList.remove('selected')); |
| | document.querySelector(`#tableBody tr[data-index="${rowIndex}"]`)?.classList.add('selected'); |
| | el.detailPanel.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| | }; |
| | |
| | function populateFilters() { |
| | const mainHazards = [...new Set(fdaData.pairs_all.map(item => item.hazardRef))].filter(Boolean).sort(); |
| | el.hazardFilter.innerHTML = '<option value="">Tous les Dangers</option>' + |
| | mainHazards.map(h => `<option value="${h}">${h}</option>`).join(''); |
| | |
| | const commodities = [...new Set(fdaData.pairs_all.map(item => item.commodity))].sort(); |
| | el.commodityFilter.innerHTML = commodities.map(c => `<option value="${c}">${c}</option>`).join(''); |
| | |
| | if (commodities.length > 0) { |
| | state.selectedCommodity = commodities[0]; |
| | el.commodityFilter.value = state.selectedCommodity; |
| | populateComparisonHazardFilter(); |
| | } |
| | } |
| | |
| | function populateComparisonHazardFilter(){ |
| | const hazards = [...new Set( |
| | fdaData.pairs_all |
| | .filter(p => p.commodity === state.selectedCommodity) |
| | .map(p => p.hazardRef) |
| | )].sort(); |
| | |
| | el.comparisonHazardFilter.innerHTML = '<option value="All">Tous les Dangers</option>' + |
| | hazards.map(h => `<option value="${h}">${h}</option>`).join(''); |
| | } |
| | |
| | function setupEventListeners() { |
| | |
| | if (!el.searchInput || !el.hazardFilter) { |
| | console.error('Certains éléments DOM ne sont pas trouvés'); |
| | return; |
| | } |
| | |
| | |
| | console.log('Methodology elements check:'); |
| | console.log('Header:', el.methodologyHeader); |
| | console.log('Content:', el.methodologyContent); |
| | console.log('Toggle:', el.methodologyToggle); |
| | |
| | el.searchInput.addEventListener('input', () => { |
| | state.searchTerm = el.searchInput.value; |
| | render(); |
| | }); |
| | |
| | el.hazardFilter.addEventListener('change', () => { |
| | state.hazardFilter = el.hazardFilter.value; |
| | render(); |
| | }); |
| | |
| | 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(); |
| | } |
| | }); |
| | |
| | el.tableBody.addEventListener('click', (e) => { |
| | if (state.currentDataset === 'pairs') { |
| | const row = e.target.closest('tr'); |
| | if (row) showDetailPanel(row.dataset.index); |
| | } |
| | }); |
| | |
| | Object.keys(el.buttons).forEach(key => { |
| | el.buttons[key].addEventListener('click', () => { |
| | state.currentDataset = key; |
| | if (key !== 'comparison') { |
| | state.sortColumn = viewConfig[key].scoreColumn; |
| | } |
| | Object.assign(state, { |
| | searchTerm: '', |
| | hazardFilter: '', |
| | sortDirection: 'desc' |
| | }); |
| | el.searchInput.value = ''; |
| | el.hazardFilter.value = ''; |
| | el.detailPanel.classList.remove('visible'); |
| | render(); |
| | }); |
| | }); |
| | |
| | el.commodityFilter.addEventListener('change', (e) => { |
| | state.selectedCommodity = e.target.value; |
| | state.selectedHazard = 'All'; |
| | el.comparisonHazardFilter.value = 'All'; |
| | populateComparisonHazardFilter(); |
| | renderComparisonView(); |
| | }); |
| | |
| | el.comparisonHazardFilter.addEventListener('change', (e) => { |
| | state.selectedHazard = e.target.value; |
| | renderComparisonView(); |
| | }); |
| | |
| | el.guideBtn.addEventListener('click', () => { |
| | el.guideModal.style.display = 'block'; |
| | }); |
| | |
| | el.closeGuideBtn.addEventListener('click', () => { |
| | el.guideModal.style.display = 'none'; |
| | }); |
| | |
| | window.addEventListener('click', (event) => { |
| | if (event.target == el.guideModal) { |
| | el.guideModal.style.display = 'none'; |
| | } |
| | }); |
| | |
| | if (localStorage.getItem('hideWelcomeGuide')) { |
| | el.welcomeGuide.style.display = 'none'; |
| | } |
| | |
| | if (el.closeGuide) { |
| | el.closeGuide.addEventListener('click', () => { |
| | el.welcomeGuide.style.display = 'none'; |
| | try { |
| | localStorage.setItem('hideWelcomeGuide', 'true'); |
| | } catch (e) {} |
| | }); |
| | } |
| | |
| | |
| | if (el.methodologyHeader && el.methodologyContent && el.methodologyToggle) { |
| | console.log('Setting up methodology toggle'); |
| | el.methodologyHeader.addEventListener('click', () => { |
| | console.log('Methodology header clicked'); |
| | const isExpanded = el.methodologyContent.classList.contains('expanded'); |
| | console.log('Is expanded:', isExpanded); |
| | |
| | if (isExpanded) { |
| | el.methodologyContent.classList.remove('expanded'); |
| | el.methodologyToggle.classList.remove('expanded'); |
| | console.log('Collapsed'); |
| | } else { |
| | el.methodologyContent.classList.add('expanded'); |
| | el.methodologyToggle.classList.add('expanded'); |
| | console.log('Expanded'); |
| | } |
| | }); |
| | } else { |
| | console.error('Methodology elements missing:', { |
| | header: !!el.methodologyHeader, |
| | content: !!el.methodologyContent, |
| | toggle: !!el.methodologyToggle |
| | }); |
| | |
| | |
| | setTimeout(() => { |
| | const header = document.getElementById('methodology-header'); |
| | const content = document.getElementById('methodology-content'); |
| | const toggle = document.getElementById('methodology-toggle'); |
| | |
| | if (header && content && toggle) { |
| | console.log('Retry: Setting up methodology toggle'); |
| | header.addEventListener('click', () => { |
| | console.log('Methodology header clicked (retry)'); |
| | const isExpanded = content.classList.contains('expanded'); |
| | |
| | if (isExpanded) { |
| | content.classList.remove('expanded'); |
| | toggle.classList.remove('expanded'); |
| | } else { |
| | content.classList.add('expanded'); |
| | toggle.classList.add('expanded'); |
| | } |
| | }); |
| | } else { |
| | console.error('Methodology elements still not found after retry'); |
| | } |
| | }, 100); |
| | } |
| | } |
| | |
| | |
| | async function loadAllData() { |
| | try { |
| | |
| | const [ftlResponse, allResponse, pairsResponse] = await Promise.all([ |
| | fetch('fda_aggregated_ftl_20250813_164803.json'), |
| | fetch('fda_aggregated_all_20250813_164803.json'), |
| | fetch('fda_pairs_all_20250813_164803.json') |
| | ]); |
| | |
| | if (!ftlResponse.ok || !allResponse.ok || !pairsResponse.ok) { |
| | throw new Error('Erreur lors du chargement des fichiers JSON'); |
| | } |
| | |
| | fdaData.aggregated_ftl = await ftlResponse.json(); |
| | fdaData.aggregated_all = await allResponse.json(); |
| | fdaData.pairs_all = await pairsResponse.json(); |
| | |
| | |
| | populateFilters(); |
| | render(); |
| | |
| | |
| | el.loadingOverlay.style.display = 'none'; |
| | } catch (error) { |
| | console.error('Erreur lors du chargement des données:', error); |
| | el.loadingOverlay.innerHTML = ` |
| | <div class="error-message"> |
| | <h2>Erreur de chargement</h2> |
| | <p>Impossible de charger les fichiers de données.</p> |
| | <p>Vérifiez que les fichiers suivants sont présents dans le répertoire:</p> |
| | <ul style="text-align: left; margin: 1rem auto; max-width: 400px;"> |
| | <li>fda_aggregated_ftl_20250813_164803.json</li> |
| | <li>fda_aggregated_all_20250813_164803.json</li> |
| | <li>fda_pairs_all_20250813_164803.json</li> |
| | </ul> |
| | <p>Message d'erreur : ${error.message}</p> |
| | </div> |
| | `; |
| | } |
| | } |
| | |
| | setupEventListeners(); |
| | loadAllData(); |
| | }); |
| | </script> |
| | </body> |
| | </html> |