IFSUSPENSION / index.html
MMOON's picture
Update index.html
1ab61c4 verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IFS Certifications Dashboard</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.2/papaparse.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary: #667eea;
--primary-dark: #5a67d8; --secondary: #764ba2; --accent: #f093fb;
--success: #48bb78; --warning: #ed8936; --danger: #f56565;
--light: #f7fafc; --dark: #2d3748; --gray-50: #f9fafb;
--gray-100: #f3f4f6; --gray-200: #e5e7eb; --gray-800: #1f2937;
--shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05); --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1); --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1);
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
background:linear-gradient(135deg, var(--gray-50) 0%, var(--light) 100%);
color:var(--dark); line-height:1.6; min-height:100vh;
}
.header { background:linear-gradient(135deg,var(--primary)0%,var(--secondary)100%); color:white;padding:2rem 0;position:relative;overflow:hidden }
.header::before { content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');opacity:.3 }
.header-content {
max-width: none;
margin:0 auto;
padding:0 2rem;
position:relative;
z-index:1
}
.header h1 { font-size:2.5rem;font-weight:700;margin-bottom:.5rem;display:flex;align-items:center;gap:1rem }
.header-subtitle { font-size:1.1rem;opacity:.9;font-weight:400 }
.navbar { background:rgba(255,255,255,.95);backdrop-filter:blur(10px);border-bottom:1px solid var(--gray-200);padding:1rem 0;position:sticky;top:0;z-index:1000 }
.navbar-content {
max-width: none;
margin:0 auto;
padding:0 2rem;
display:flex;
justify-content:space-between;
align-items:center
}
.period-badge { background:var(--primary);color:white;padding:.5rem 1rem;border-radius:50px;font-size:.9rem;font-weight:500 }
.language-toggle { display:flex;gap:.5rem }
.lang-btn { padding:.5rem 1rem;border:2px solid var(--primary);border-radius:50px;background:transparent;color:var(--primary);cursor:pointer;transition:all .3s ease;font-weight:500 }
.lang-btn.active { background:var(--primary);color:white;transform:scale(1.05) }
.container {
max-width: none;
margin:0 auto;
padding:2rem
}
.upload-section { background:white;border-radius:16px;padding:2rem;margin-bottom:2rem;box-shadow:var(--shadow-md);border:2px dashed var(--gray-200);transition:all .3s ease;text-align:center }
.upload-section:hover { border-color:var(--primary);transform:translateY(-2px);box-shadow:var(--shadow-lg) }
.upload-icon { font-size:3rem;color:var(--primary);margin-bottom:1rem }
.upload-section h2 { font-size:1.5rem;font-weight:600;margin-bottom:.5rem;color:var(--dark) }
.upload-section p { color:var(--gray-800);margin-bottom:1.5rem }
.upload-btn,.btn-load-gsheet { background:linear-gradient(135deg,var(--primary)0%,var(--primary-dark)100%);color:white;border:none;padding:1rem 2rem;border-radius:50px;font-size:1.1rem;font-weight:600;cursor:pointer;transition:all .3s ease;display:inline-flex;align-items:center;gap:.5rem;margin:.5rem }
.upload-btn:hover,.btn-load-gsheet:hover { transform:translateY(-2px);box-shadow:var(--shadow-lg) }
.btn-load-gsheet { background:linear-gradient(135deg,var(--secondary)0%,var(--primary)100%) }
#fileInput { display:none }
.upload-section.shrunk { padding:0.5rem 1rem; border-style:solid; min-height: auto; max-height: 80px; overflow: hidden; }
.upload-section.shrunk h2 { font-size:1.2rem; margin-bottom:0.5rem; }
.upload-section.shrunk p#upload-description { display:none; }
.upload-section.shrunk .main-upload-options { display:none; }
.upload-section.shrunk p#upload-loaded-message { margin-bottom:.5rem; font-style:italic; color:var(--gray-800); }
.reload-options-trigger { display:inline-block!important; cursor:pointer; color:var(--primary); text-decoration:underline; font-size:0.9rem; }
.loading { display:none;text-align:center;padding:2rem;background:white;border-radius:16px;box-shadow:var(--shadow-md);margin:2rem 0 }
.spinner { width:50px;height:50px;margin:0 auto 1rem;border:4px solid var(--gray-200);border-top:4px solid var(--primary);border-radius:50%;animation:spin 1s linear infinite }
@keyframes spin { 0% { transform:rotate(0deg) } 100% { transform:rotate(360deg) } }
.filters { background:white;border-radius:16px;padding:1.5rem;margin-bottom:2rem;box-shadow:var(--shadow-md);display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem }
.filter-group { display:flex;flex-direction:column }
.filter-group label { font-weight:600;color:var(--dark);margin-bottom:.5rem;display:flex;align-items:center;gap:.5rem }
.filter-group select,.filter-group input { padding:.75rem;border:2px solid var(--gray-200);border-radius:8px;font-size:1rem;transition:all .3s ease;background:white }
.filter-group select:focus,.filter-group input:focus { outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(102,126,234,.1) }
.product-scopes-filter { margin-top:1rem;padding-top:1rem;border-top:1px solid var(--gray-200);grid-column:1/-1 }
.product-scopes-filter h3 { font-size:1.1rem;margin-bottom:.75rem;color:var(--dark) }
.scopes-checkbox-container { display:flex;flex-wrap:wrap;gap:.75rem;max-height:150px;overflow-y:auto;padding:.5rem;border:1px solid var(--gray-200);border-radius:8px }
.scopes-checkbox-container label { display:flex;align-items:center;gap:.3rem;font-weight:400;font-size:.9rem;cursor:pointer;padding:.3rem .5rem;border-radius:6px;transition:background-color .2s ease }
.scopes-checkbox-container label:hover { background-color:var(--gray-50) }
.scopes-checkbox-container input[type=checkbox] { margin-right:.3rem;accent-color:var(--primary) }
.search-bar-container { margin-top:1rem;padding-top:1rem;border-top:1px solid var(--gray-200);grid-column:1/-1 }
#searchInput { width:100%;padding:.75rem 1rem;font-size:1rem;border:2px solid var(--gray-200);border-radius:8px }
.stats-dashboard { display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1.5rem;margin-bottom:2rem }
.stat-card { background:white;border-radius:16px;padding:1.5rem;box-shadow:var(--shadow-md);transition:all .3s ease;position:relative;overflow:hidden }
.stat-card:hover { transform:translateY(-4px);box-shadow:var(--shadow-xl) }
.stat-card::before { content:'';position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,var(--primary),var(--accent)) }
.stat-header { display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem }
.stat-title { font-size:.9rem;color:var(--gray-800);font-weight:600;text-transform:uppercase;letter-spacing:.05em }
.stat-icon { width:40px;height:40px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:white }
.stat-value { font-size:2.5rem;font-weight:700;color:var(--dark);line-height:1 }
.stat-label { font-size:.9rem;color:var(--gray-800);margin-top:.5rem }
.charts-grid { display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1.5rem;margin-bottom:2rem }
.chart-card { background:white;border-radius:16px;padding:1.5rem;box-shadow:var(--shadow-md);transition:all .3s ease }
.chart-card:hover { transform:translateY(-2px);box-shadow:var(--shadow-lg) }
.chart-header { display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;padding-bottom:1rem;border-bottom:1px solid var(--gray-100) }
.chart-title { font-size:1.2rem;font-weight:600;color:var(--dark);display:flex;align-items:center;gap:.5rem }
.chart-container { position:relative;height:280px }
.table-card { background:white;border-radius:16px;box-shadow:var(--shadow-md);overflow:hidden;margin-bottom:2rem }
.table-header { padding:1.5rem;background:var(--gray-50);border-bottom:1px solid var(--gray-200) }
.table-title { font-size:1.3rem;font-weight:600;color:var(--dark);display:flex;align-items:center;gap:.5rem }
.table-wrapper { overflow-x:auto }
table { width:100%;border-collapse:collapse }
th,td { padding:1rem;text-align:left;border-bottom:1px solid var(--gray-100) }
td.col-supplier { min-width:180px } td.col-country { min-width:100px } td.col-date { min-width:130px }
td.col-requirements { min-width:220px;white-space:normal!important } td.col-types { min-width:180px;white-space:normal!important }
td.col-actions { min-width:110px;text-align:center }
th { background:var(--gray-50);font-weight:600;color:var(--dark);font-size:.9rem;text-transform:uppercase;letter-spacing:.05em;cursor:pointer;position:relative }
th .sort-icon { margin-left:8px;opacity:.5 } th.sorted .sort-icon { opacity:1 }
tbody tr:hover { background:var(--gray-100) }
.btn { padding:.75rem 1.5rem;border:none;border-radius:8px;font-weight:600;cursor:pointer;transition:all .3s ease;display:inline-flex;align-items:center;gap:.5rem;text-decoration:none;font-size:.9rem }
.btn-primary { background:linear-gradient(135deg,var(--primary),var(--primary-dark));color:white }
.btn-primary:hover { transform:translateY(-2px);box-shadow:var(--shadow-lg) }
.btn-secondary { background:var(--gray-100);color:var(--dark);border:2px solid var(--gray-200) }
.btn-secondary:hover { background:var(--gray-200) }
.btn-demo { background:linear-gradient(135deg,var(--success),#38a169);color:white }
.btn-demo:hover { transform:translateY(-2px);box-shadow:var(--shadow-lg) }
.btn-ai-translate { background:linear-gradient(135deg,#9f7aea,#667eea);color:white;border:none }
.btn-ai-translate:hover { transform:translateY(-2px);box-shadow:var(--shadow-lg) }
.btn-ai-translate:disabled { opacity:0.6;cursor:not-allowed;transform:none }
.requirement-highlight { background:rgba(102,126,234,.1);color:var(--primary);padding:.25rem .5rem;border-radius:6px;font-size:.8rem;font-weight:600;margin:.1rem;display:inline-block;white-space:normal }
.type-highlight { font-weight:600;padding:.25rem .5rem;border-radius:6px;font-size:.8rem;margin:.1rem;display:inline-block;white-space:normal }
.ko-highlight { background:rgba(245,101,101,.1);color:var(--danger) }
.major-highlight { background:rgba(237,137,54,.1);color:var(--warning) }
.minor-highlight { background:rgba(72,187,120,.1);color:var(--success) }
.nonpayment-highlight { background:rgba(159,122,234,.1);color:#9f7aea }
.termination-highlight { background:rgba(45,55,72,.1);color:var(--dark) }
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0,0,0,.6);
z-index: 1050;
justify-content: center;
align-items: flex-start;
padding: 5vh 20px 20px 20px;
overflow-y: auto;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 16px;
box-shadow: var(--shadow-xl);
width: 100%;
max-width: 800px;
max-height: calc(90vh - 5vh);
overflow-y: auto;
position: relative;
animation: modalFadeIn .3s ease-out;
}
@keyframes modalFadeIn { 0% { opacity:0;transform:translateY(-30px) scale(.95) } 100% { opacity:1;transform:translateY(0) scale(1) } }
.modal-header { display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--gray-200);padding-bottom:1rem;margin-bottom:1.5rem }
.modal-title { font-size:1.5rem;font-weight:600;color:var(--dark) }
.modal-close-btn { background:none;border:none;font-size:1.8rem;color:var(--gray-800);cursor:pointer;padding:.5rem;line-height:1 }
.modal-close-btn:hover { color:var(--danger) }
.modal-body p { margin-bottom:.8rem;line-height:1.7 } .modal-body strong { color:var(--primary-dark) }
.modal-footer { text-align:right;margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--gray-200) }
.translation-controls { margin:1rem 0;padding:1rem;background:var(--gray-50);border-radius:8px;display:flex;gap:1rem;align-items:center }
.lock-reason { background:var(--light);padding:1rem;border-radius:8px;border-left:4px solid var(--primary);margin-top:1rem;white-space:pre-wrap;word-break:break-word }
.tooltip { position:relative;cursor:help }
.tooltip .tooltiptext { visibility:hidden;width:250px;background-color:var(--dark);color:white;text-align:center;border-radius:8px;padding:.75rem;position:absolute;z-index:1060;bottom:125%;left:50%;margin-left:-125px;opacity:0;transition:opacity .3s;font-size:.8rem;box-shadow:var(--shadow-lg) }
.tooltip:hover .tooltiptext { visibility:visible;opacity:1 }
.no-data { text-align:center;padding:3rem;color:var(--gray-800);font-style:italic }
.export-section { text-align:center;margin:2rem 0 }
.export-btn { background:linear-gradient(135deg,var(--success),#38a169);color:white;border:none;padding:1rem 2rem;border-radius:50px;font-size:1rem;font-weight:600;cursor:pointer;transition:all .3s ease;display:inline-flex;align-items:center;gap:.5rem }
.export-btn:hover { transform:translateY(-2px);box-shadow:var(--shadow-lg) }
.error-message {
background: #fef2f2;
border: 1px solid #fecaca;
color: #dc2626;
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
text-align: center;
}
.translation-status {
font-size: 0.9rem;
color: var(--gray-800);
margin-left: 1rem;
font-style: italic;
}
.text-length-indicator {
font-size: 0.8rem;
color: var(--warning);
background: #fef3cd;
padding: 0.25rem 0.5rem;
border-radius: 4px;
margin-left: 0.5rem;
}
@media (max-width:768px) {
.header h1 { font-size:2rem }
.header-content,
.navbar-content {
padding-left: 1rem;
padding-right: 1rem;
}
.container { padding:1rem }
.stats-dashboard,.charts-grid,.filters { grid-template-columns:1fr }
.navbar-content { flex-direction:column;gap:1rem }
.table-wrapper { font-size:.8rem }
th,td { padding:.75rem .5rem;white-space:normal }
.modal-overlay {
padding: 20px 10px;
}
.modal-content {
padding: 20px;
max-width: 95%;
max-height: calc(100vh - 40px);
}
.modal-title { font-size:1.3rem }
.translation-controls { flex-direction:column;gap:0.5rem }
}
@keyframes fadeInUp { 0% { opacity:0;transform:translateY(30px) } 100% { opacity:1;transform:translateY(0) } }
.fade-in { animation:fadeInUp .6s ease-out }
</style>
</head>
<body>
<header class="header">
<div class="header-content"><h1><i class="fas fa-shield-alt"></i> <span id="page-main-title"></span></h1><p class="header-subtitle" id="page-subtitle"></p></div>
</header>
<nav class="navbar">
<div class="navbar-content">
<div class="period-badge"><i class="fas fa-calendar-alt"></i> <span id="period-text"></span></div>
<div class="language-toggle"><button id="lang-fr" class="lang-btn active">🇫🇷 FR</button><button id="lang-en" class="lang-btn">🇬🇧 EN</button></div>
</div>
</nav>
<div class="container">
<div class="upload-section" id="upload-section-container"></div>
<div class="loading" id="loading"><div class="spinner"></div><p id="loading-text"></p></div>
<div class="filters" id="filters" style="display: none;">
<div class="filter-group"><label for="countryFilter"><i class="fas fa-globe"></i> <span id="country-label"></span></label><select id="countryFilter"><option value="all" id="all-countries"></option></select></div>
<div class="filter-group"><label for="requirementFilter"><i class="fas fa-list-check"></i> <span id="requirement-label"></span></label><select id="requirementFilter"><option value="all" id="all-requirements"></option></select></div>
<div class="filter-group"><label for="typeFilter"><i class="fas fa-tags"></i> <span id="type-label"></span></label><select id="typeFilter"><option value="all" id="all-types"></option><option value="KO">KO</option><option value="Major">Major</option><option value="Minor">Minor</option><option value="Non-paiement" id="non-payment-option"></option><option value="Arrêt de certification" id="termination-option"></option></select></div>
<div class="filter-group">
<label><i class="fas fa-calendar"></i> <span id="date-period-label"></span></label>
<div style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem;">
<input type="date" id="dateFromFilter" style="flex: 1;">
<span style="color: var(--gray-800); font-weight: 500;" id="date-to-text"></span>
<input type="date" id="dateToFilter" style="flex: 1;">
</div>
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap;">
<button type="button" class="btn btn-secondary date-period-btn" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" data-period="thisMonth" onclick="setDatePeriod('thisMonth')">Ce mois</button>
<button type="button" class="btn btn-secondary date-period-btn" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" data-period="last3Months" onclick="setDatePeriod('last3Months')">3 derniers mois</button>
<button type="button" class="btn btn-secondary date-period-btn" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" data-period="thisYear" onclick="setDatePeriod('thisYear')">Cette année</button>
<button type="button" class="btn btn-secondary date-period-btn" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;" data-period="clear" onclick="setDatePeriod('clear')">Effacer</button>
</div>
</div>
<div class="filter-group product-scopes-filter">
<h3 id="product-scopes-title"></h3>
<div id="scopesCheckboxContainer" class="scopes-checkbox-container"></div>
</div>
<div class="filter-group search-bar-container">
<label for="searchInput"><i class="fas fa-search"></i> <span id="search-label"></span></label>
<input type="text" id="searchInput" placeholder="">
</div>
</div>
<div class="stats-dashboard" id="stats-dashboard" style="display: none;">
<div class="stat-card"><div class="stat-header"><div class="stat-title" id="total-title"></div><div class="stat-icon" style="background:linear-gradient(135deg, var(--primary), var(--accent));"><i class="fas fa-ban"></i></div></div><div class="stat-value" id="totalLocks">0</div><div class="stat-label" id="total-label"></div></div>
<div class="stat-card"><div class="stat-header"><div class="stat-title" id="ko-title-text"></div><div class="stat-icon" style="background:linear-gradient(135deg, var(--danger), #ff6b6b);"><i class="fas fa-exclamation-triangle"></i></div></div><div class="stat-value" id="koCount">0</div><div class="stat-label" id="ko-label"></div></div>
<div class="stat-card"><div class="stat-header"><div class="stat-title" id="major-title-text"></div><div class="stat-icon" style="background:linear-gradient(135deg, var(--warning), #ffa726);"><i class="fas fa-exclamation-circle"></i></div></div><div class="stat-value" id="majorCount">0</div><div class="stat-label" id="major-label"></div></div>
<div class="stat-card"><div class="stat-header"><div class="stat-title" id="countries-title"></div><div class="stat-icon" style="background:linear-gradient(135deg, var(--success), #66bb6a);"><i class="fas fa-globe-americas"></i></div></div><div class="stat-value" id="countriesCount">0</div><div class="stat-label" id="countries-label"></div></div>
</div>
<div class="charts-grid" id="charts-grid" style="display: none;">
<div class="chart-card"><div class="chart-header"><div class="chart-title"><i class="fas fa-chart-pie"></i> <span id="types-chart-title"></span></div></div><div class="chart-container"><canvas id="typesChart"></canvas></div></div>
<div class="chart-card"><div class="chart-header"><div class="chart-title"><i class="fas fa-chart-bar"></i> <span id="requirements-chart-title"></span></div></div><div class="chart-container"><canvas id="requirementsChart"></canvas></div></div>
<div class="chart-card"><div class="chart-header"><div class="chart-title"><i class="fas fa-chart-doughnut"></i> <span id="chapters-chart-title"></span></div></div><div class="chart-container"><canvas id="chaptersChart"></canvas></div></div>
<div class="chart-card"><div class="chart-header"><div class="chart-title"><i class="fas fa-globe-americas"></i><span id="countries-distrib-chart-title"></span></div></div><div class="chart-container"><canvas id="countriesDistribChart"></canvas></div></div>
<div class="chart-card"><div class="chart-header"><div class="chart-title"><i class="fas fa-tags"></i><span id="scopes-distrib-chart-title"></span></div></div><div class="chart-container"><canvas id="scopesDistribChart"></canvas></div></div>
</div>
<div class="table-card" id="table-card" style="display:none;"><div class="table-header"><div class="table-title"><i class="fas fa-table"></i> <span id="table-main-title"></span></div></div><div class="table-wrapper"><table id="auditsTable"><thead><tr>
<th data-sort-key="Supplier" id="supplier-header"><span class="sort-icon"></span></th>
<th data-sort-key="Country/Region" id="country-header"><span class="sort-icon"></span></th>
<th data-sort-key="Certificate/Assessment lock date" data-sort-type="date" id="date-header"><span class="sort-icon"></span></th>
<th id="requirements-header"></th><th id="types-header"></th><th id="actions-header"></th>
</tr></thead><tbody id="auditsTableBody"></tbody></table></div></div>
<div class="export-section" id="export-section" style="display:none;"><button class="export-btn" id="export-btn"><i class="fas fa-download"></i> <span id="export-btn-text"></span></button></div>
</div>
<div class="modal-overlay" id="detailsModalOverlay">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="modalTitle"></h2>
<button class="modal-close-btn" id="modalCloseBtn">×</button>
</div>
<div class="modal-body" id="modalBodyContent"></div>
<div class="modal-footer">
<button class="btn btn-secondary" id="modalFooterCloseBtn"></button>
</div>
</div>
</div>
<script>
// Variables globales
let allAuditData = [], displayedAuditData = [], currentDetailedAudit = null;
let requirementStats = {}, chapterStats = {}, typeStats = {'KO':0,'Major':0,'Minor':0,'Non-paiement':0,'Arrêt de certification':0};
let countries = [], requirements = [], currentLanguage = 'fr', currentSortColumn = null, currentSortDirection = 'asc';
let typesChart, requirementsChart, chaptersChart, countriesDistribChart, scopesDistribChart;
// 🤖 SYSTÈME DE TRADUCTION IA MyMemory (VERSION AVEC DÉCOUPAGE)
class MyMemoryTranslateService {
constructor() {
this.baseUrl = 'https://api.mymemory.translated.net/get';
this.cache = new Map();
this.maxChars = 450; // Limite de sécurité (MyMemory limite à 500)
}
async translateText(text, targetLanguage = 'fr', sourceLanguage = 'en') {
console.log(`🔄 Tentative de traduction: "${text.substring(0, 50)}..." (${text.length} chars) de ${sourceLanguage} vers ${targetLanguage}`);
const cacheKey = `${text}_${sourceLanguage}_${targetLanguage}`;
if (this.cache.has(cacheKey)) {
console.log('✅ Traduction trouvée dans le cache');
return this.cache.get(cacheKey);
}
// Si le texte est trop long, le découper
if (text.length > this.maxChars) {
console.log(`📏 Texte trop long (${text.length} chars), découpage en morceaux...`);
return await this.translateLongText(text, targetLanguage, sourceLanguage);
}
return await this.translateSingleChunk(text, targetLanguage, sourceLanguage);
}
async translateLongText(text, targetLanguage, sourceLanguage) {
// Découper le texte en phrases ou par points
const chunks = this.splitTextIntoChunks(text);
console.log(`📦 Texte découpé en ${chunks.length} morceaux:`, chunks.map(c => `"${c.substring(0, 30)}..." (${c.length})`));
const translatedChunks = [];
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
console.log(`🔄 Traduction du morceau ${i + 1}/${chunks.length}: "${chunk.substring(0, 30)}..."`);
try {
const translatedChunk = await this.translateSingleChunk(chunk, targetLanguage, sourceLanguage);
translatedChunks.push(translatedChunk);
// Petite pause entre les requêtes pour éviter la limite de taux
if (i < chunks.length - 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}
} catch (error) {
console.error(`❌ Erreur sur le morceau ${i + 1}:`, error);
// En cas d'erreur sur un morceau, garder l'original
translatedChunks.push(chunk);
}
}
const result = translatedChunks.join(' ');
console.log(`✅ Traduction complète réussie: "${result.substring(0, 50)}..." (${result.length} chars)`);
// Mettre en cache le résultat complet
const cacheKey = `${text}_${sourceLanguage}_${targetLanguage}`;
this.cache.set(cacheKey, result);
return result;
}
splitTextIntoChunks(text) {
const chunks = [];
// Découper par phrases (points + espace + majuscule)
const sentences = text.split(/(?<=\.)\s+(?=[A-Z])/);
let currentChunk = '';
for (const sentence of sentences) {
// Si ajouter cette phrase dépasse la limite
if ((currentChunk + ' ' + sentence).length > this.maxChars) {
// Sauvegarder le chunk actuel s'il n'est pas vide
if (currentChunk.trim()) {
chunks.push(currentChunk.trim());
currentChunk = '';
}
// Si la phrase elle-même est trop longue, la découper par virgules ou parenthèses
if (sentence.length > this.maxChars) {
const subChunks = this.splitLongSentence(sentence);
chunks.push(...subChunks);
} else {
currentChunk = sentence;
}
} else {
currentChunk = currentChunk ? currentChunk + ' ' + sentence : sentence;
}
}
// Ajouter le dernier chunk
if (currentChunk.trim()) {
chunks.push(currentChunk.trim());
}
return chunks.filter(chunk => chunk.length > 0);
}
splitLongSentence(sentence) {
const chunks = [];
// Découper par virgules, parenthèses, ou deux-points
const parts = sentence.split(/([,;:()\[\]])/);
let currentChunk = '';
for (const part of parts) {
if ((currentChunk + part).length > this.maxChars) {
if (currentChunk.trim()) {
chunks.push(currentChunk.trim());
currentChunk = '';
}
// Si même un morceau simple est trop long, découper brutalement
if (part.length > this.maxChars) {
const bruteChunks = this.bruteSplit(part);
chunks.push(...bruteChunks);
} else {
currentChunk = part;
}
} else {
currentChunk += part;
}
}
if (currentChunk.trim()) {
chunks.push(currentChunk.trim());
}
return chunks.filter(chunk => chunk.length > 0);
}
bruteSplit(text) {
const chunks = [];
for (let i = 0; i < text.length; i += this.maxChars) {
chunks.push(text.substring(i, i + this.maxChars));
}
return chunks;
}
async translateSingleChunk(text, targetLanguage, sourceLanguage) {
try {
const langPair = `${sourceLanguage}|${targetLanguage}`;
const url = `${this.baseUrl}?q=${encodeURIComponent(text)}&langpair=${langPair}`;
console.log(`🌐 Appel API pour ${text.length} chars`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`MyMemory API error: ${response.status}`);
}
const data = await response.json();
if (data.responseStatus === 200) {
const translatedText = data.responseData.translatedText;
console.log(`✅ Chunk traduit: "${text.substring(0, 30)}..." → "${translatedText.substring(0, 30)}..."`);
return translatedText;
} else {
throw new Error(`MyMemory translation failed: ${data.responseDetails || 'Unknown error'}`);
}
} catch (error) {
console.error('❌ MyMemory error:', error);
throw error;
}
}
}
class IFSTranslationManager {
constructor() {
this.translator = new MyMemoryTranslateService();
this.isTranslating = false;
}
async translateLockReason(lockReason, targetLanguage = 'fr') {
console.log(`🎯 translateLockReason appelée avec: langue=${targetLanguage}, texte="${lockReason.substring(0, 50)}..."`);
if (!lockReason || lockReason.trim() === '') {
console.log('⚠️ Texte vide, retour direct');
return lockReason;
}
const isAlreadyFrench = this.isLikelyFrench(lockReason);
const isAlreadyEnglish = this.isLikelyEnglish(lockReason);
console.log(`🔍 Détection langue: français=${isAlreadyFrench}, anglais=${isAlreadyEnglish}`);
// Simplifier la logique : toujours traduire si demandé
try {
const sourceLanguage = targetLanguage === 'fr' ? 'en' : 'fr';
console.log(`🔄 Traduction de ${sourceLanguage} vers ${targetLanguage}`);
return await this.translator.translateText(lockReason, targetLanguage, sourceLanguage);
} catch (error) {
console.error('❌ Translation failed:', error);
throw error; // Re-throw pour afficher l'erreur à l'utilisateur
}
}
isLikelyFrench(text) {
const frenchIndicators = [
'non-conformité', 'évaluation', 'vérification', 'surveillance',
'nettoyage', 'maintenance', 'procédure', 'exigence',
'certification', 'arrêt', 'frais', 'paiement', 'système', 'entreprise'
];
const lowerText = text.toLowerCase();
const found = frenchIndicators.filter(indicator => lowerText.includes(indicator));
console.log(`🇫🇷 Indicateurs français trouvés: ${found.join(', ')}`);
return found.length > 0;
}
isLikelyEnglish(text) {
const englishIndicators = [
'non-conformity', 'assessment', 'verification', 'monitoring',
'cleaning', 'maintenance', 'procedure', 'requirement',
'certification', 'termination', 'fees', 'payment', 'system', 'company', 'based', 'reviewed'
];
const lowerText = text.toLowerCase();
const found = englishIndicators.filter(indicator => lowerText.includes(indicator));
console.log(`🇬🇧 Indicateurs anglais trouvés: ${found.join(', ')}`);
return found.length > 0;
}
}
// Initialiser le gestionnaire de traduction
const translationManager = new IFSTranslationManager();
// 🧪 FONCTION DE TEST GLOBALE
window.testTranslation = async function(text = "Hello world, this is a test") {
console.log('🧪 Test de traduction global');
try {
const result = await translationManager.translateLockReason(text, 'fr');
console.log('✅ Résultat:', result);
alert(`Test réussi !\nOriginal (${text.length} chars): ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}\n\nTraduit (${result.length} chars): ${result.substring(0, 100)}${result.length > 100 ? '...' : ''}`);
return result;
} catch (error) {
console.error('❌ Erreur:', error);
alert(`Erreur: ${error.message}`);
return null;
}
};
// Test avec texte long
window.testLongTranslation = async function() {
const longText = "KO in 1.2.1 Based on the samples reviewed during the audit, the food safety and product quality management system is not fully implemented, maintained and reviewed. Employees could not demonstrate in all areas that they are aware of their responsibilities. The following examples: 1) Receipt of frozen commercial goods: The unloading of the delivering truck was carried out on the first day of the audit without incoming goods inspection / checking the incoming goods temperature (CP 1). The responsible employee was on vacation and the production management was not informed by the staff.";
return await testTranslation(longText);
};
console.log('🔧 Fonctions de test disponibles:');
console.log('- testTranslation("votre texte")');
console.log('- testLongTranslation() // Test avec le texte d\'exemple long');
const validIFSRequirements = ["1.1.1","1.1.2","1.2.1","1.2.2","1.2.3","1.2.4","1.2.5","1.2.6","1.3.1","1.3.2","1.3.3","2.1.1.1","2.1.1.2","2.1.1.3","2.1.2.1","2.1.2.2","2.1.2.3","2.2.1.1","2.2.1.2","2.2.1.3","2.2.1.4","2.3.1.1","2.3.1.2","2.3.2.1","2.3.3.1","2.3.4.1","2.3.5.1","2.3.6.1","2.3.7.1","2.3.8.1","2.3.9.1","2.3.9.2","2.3.9.3","2.3.9.4","2.3.10.1","2.3.11.1","2.3.11.2","2.3.12.1","3.1.1","3.1.2","3.2.1","3.2.2","3.2.3","3.2.4","3.2.5","3.2.6","3.2.7","3.2.8","3.2.9","3.2.10","3.2.11","3.3.1","3.3.2","3.3.3","3.3.4","3.4.1","3.4.2","3.4.3","3.4.4","3.4.5","3.4.6","3.4.7","3.4.8","4.1.1","4.1.2","4.1.3","4.1.4","4.2.1.1","4.2.1.2","4.2.1.3","4.2.1.4","4.2.1.5","4.3.1","4.3.2","4.3.3","4.3.4","4.3.5","4.3.6","4.4.1","4.4.2","4.4.3","4.4.4","4.4.5","4.4.6","4.4.7","4.5.1","4.5.2","4.5.3","4.6.1","4.7.1","4.7.2","4.8.1","4.8.2","4.8.3","4.8.4","4.9.1.1","4.9.2.1","4.9.2.2","4.9.2.3","4.9.3.1","4.9.3.2","4.9.3.3","4.9.4.1","4.9.4.2","4.9.5.1","4.9.5.2","4.9.5.3","4.9.5.4","4.9.6.1","4.9.6.2","4.9.6.3","4.9.7.1","4.9.8.1","4.9.8.2","4.9.8.3","4.9.8.4","4.9.9.1","4.9.9.2","4.9.9.3","4.9.9.4","4.9.10.1","4.9.10.2","4.10.1","4.10.2","4.10.3","4.10.4","4.10.5","4.10.6","4.10.7","4.10.8","4.10.9","4.11.1","4.11.2","4.11.3","4.11.4","4.11.5","4.11.6","4.12.1","4.12.2","4.12.3","4.12.4","4.12.5","4.12.6","4.12.7","4.12.8","4.12.9","4.12.10","4.12.11","4.12.12","4.13.1","4.13.2","4.13.3","4.13.4","4.13.5","4.13.6","4.13.7","4.14.1","4.14.2","4.14.3","4.14.4","4.14.5","4.14.6","4.15.1","4.15.2","4.15.3","4.15.4","4.15.5","4.15.6","4.15.7","4.16.1","4.16.2","4.16.3","4.16.4","4.16.5","4.16.6","4.17.1","4.17.2","4.17.3","4.17.4","4.17.5","4.18.1","4.18.2","4.18.3","4.18.4","4.18.5","4.19.1","4.19.2","4.19.3","4.20.1","4.20.2","4.20.3","4.20.4","4.21.1","4.21.2","4.21.3","5.1.1","5.1.2","5.1.3","5.2.1","5.3.1","5.3.2","5.3.3","5.3.4","5.3.5","5.4.1","5.4.2","5.4.3","5.5.1","5.5.2","5.6.1","5.6.2","5.6.3","5.6.4","5.6.5","5.6.6","5.6.7","5.6.8","5.7.1","5.7.2","5.8.1","5.8.2","5.8.3","5.8.4","5.9.1","5.9.2","5.10.1","5.10.2","5.10.3","5.10.4","5.11.1","5.11.2","5.11.3","5.11.4"];
const koRequirements=["1.2.1","2.3.9.1","3.2.2","4.1.3","4.2.1.3","4.12.1","4.18.1","5.1.1","5.7.2","5.9.1","5.11.3"];
const requirementTranslations={"1.1.1":{fr:"Politique qualité/sécurité",en:"FS & quality policy"},"1.2.1":{fr:"Structure (KO)",en:"Organizational (KO)"},"1.3.1":{fr:"Revue direction",en:"Mgmt review"},"2.1.1.1":{fr:"Manuel QS",en:"FSQ manual"},"2.2.1.1":{fr:"HACCP",en:"HACCP"},"2.3.1.1":{fr:"Équipe HACCP",en:"HACCP team"},"2.3.9.1":{fr:"Plan HACCP (KO)",en:"HACCP plan (KO)"},"3.1.1":{fr:"Gestion RH",en:"HR mgmt"},"3.2.1":{fr:"Hygiène perso.",en:"Pers. hygiene"},"3.2.2":{fr:"Hygiène perso. (KO)",en:"Pers. hygiene (KO)"},"3.2.10":{fr:"Tenue travail",en:"Workwear"},"3.3.1":{fr:"Formation",en:"Training"},"4.1.1":{fr:"Revue contrat",en:"Contract review"},"4.1.3":{fr:"Exig. client (KO)",en:"Cust. req. (KO)"},"4.2.1.1":{fr:"Spécifications",en:"Specifications"},"4.2.1.3":{fr:"Conf. specs (KO)",en:"Spec. compl. (KO)"},"4.3.1":{fr:"Dév. produit",en:"Prod. dev."},"4.4.1":{fr:"Achats",en:"Purchasing"},"4.5.1":{fr:"Emballage",en:"Packaging"},"4.6.1":{fr:"Site usine",en:"Factory loc."},"4.7.1":{fr:"Extérieurs",en:"Exterior"},"4.8.1":{fr:"Plan usine",en:"Factory layout"},"4.9.1.1":{fr:"Net. équip.",en:"Equip. clean."},"4.10.1":{fr:"Net./Désinf.",en:"Clean./Disinf."},"4.11.1":{fr:"Gestion déchets",en:"Waste mgmt"},"4.12.1":{fr:"Risque CE (KO)",en:"For. mat. (KO)"},"4.13.1":{fr:"Surv. nuisibles",en:"Pest monit."},"4.14.1":{fr:"Récep./Stock.",en:"Recep./Stor."},"4.15.1":{fr:"Transport",en:"Transport"},"4.16.1":{fr:"Maintenance",en:"Maintenance"},"4.17.1":{fr:"Équipement",en:"Equipment"},"4.18.1":{fr:"Traçabilité (KO)",en:"Traceab. (KO)"},"4.19.1":{fr:"Allergènes",en:"Allergens"},"4.20.1":{fr:"Fraude alim.",en:"Food fraud"},"5.1.1":{fr:"Audits int. (KO)",en:"Int. audits (KO)"},"5.1.2":{fr:"Fréq. audits",en:"Audit freq."},"5.2.1":{fr:"Insp. site",en:"Site insp."},"5.3.1":{fr:"Valid. process",en:"Proc. valid."},"5.4.1":{fr:"Étalonnage",en:"Calibration"},"5.5.1":{fr:"Vérif. quantité",en:"Qty verif."},"5.5.2":{fr:"Ctrl quantité",en:"Qty ctrl"},"5.6.1":{fr:"Anal. produits",en:"Prod. anal."},"5.7.1":{fr:"Bloc./Lib. prod.",en:"Prod. block/rel."},"5.7.2":{fr:"NC produits (KO)",en:"NC prod. (KO)"},"5.8.1":{fr:"Gest. réclam.",en:"Compl. mgmt"},"5.9.1":{fr:"Incid./Rappels (KO)",en:"Incid./Recalls (KO)"},"5.10.1":{fr:"Gest. NC",en:"NC mgmt"},"5.11.1":{fr:"Actions corr.",en:"Corr. actions"},"5.11.3":{fr:"Impl. AC (KO)",en:"CA impl. (KO)"}};
const ifsChaptersV8={'1':{fr:'Gouvernance',en:'Governance'},'2':{fr:'Syst. MQ/SA',en:'FSQMS'},'3':{fr:'Ressources',en:'Resources'},'4':{fr:'Opérations',en:'Operations'},'5':{fr:'Mesures/Anal./Amél.',en:'Meas./Anal./Impr.'}};
const productScopesData = {
1: { fr: "Viandes et prép.", en: "Meat & prep." }, 2: { fr: "Poissons et prép.", en: "Fish & prep." },
3: { fr: "Œufs et ovoproduits", en: "Eggs & egg prod." }, 4: { fr: "Produits laitiers", en: "Dairy products" },
5: { fr: "Fruits et légumes", en: "Fruits & vegetables" }, 6: { fr: "Céréales, boulangerie, snacks", en: "Grains, bakery, snacks" },
7: { fr: "Produits combinés", en: "Combined products" }, 8: { fr: "Boissons", en: "Beverages" },
9: { fr: "Huiles et graisses", en: "Oils & fats" }, 10: { fr: "Alim. déshydr., ingrédients", en: "Dry goods, ingredients" },
11: { fr: "Aliments pour animaux", en: "Pet food" }
};
const uiTexts={pageMainTitle:{fr:'IFS Dashboard',en:'IFS Dashboard'},pageSubtitle:{fr:'Analyse certifications suspendues',en:'Suspended certifications analysis'},uploadSectionTitle:{fr:'Charger Données',en:'Load Data'},uploadSectionDescription:{fr:'Via Google Sheet (Apps Script) ou fichier CSV.',en:'Via Google Sheet (Apps Script) or CSV file.'},uploadSectionDemoLoadedTitle:{fr:'Données Chargées',en:'Data Loaded'},uploadSectionDemoLoadedDescription:{fr:'certifications analysées.',en:'certifications analyzed.'},uploadSectionDemoLoadedPrompt:{fr:'Recharger ou importer nouvelles données:',en:'Reload or import new data:'},uploadBtnText:{fr:'Fichier CSV',en:'CSV File'},demoBtnText:{fr:'Démo',en:'Demo'},reloadDemoBtnText:{fr:'Re-Démo',en:'Re-Demo'},loadGoogleSheetBtnText:{fr:'Charger BASE',en:'Load Database'},errorLoadingGoogleSheet:{fr:'Erreur GSheet. Vérif. script & partage.',en:'GSheet Error. Check script & sharing.'},loadingText:{fr:'Analyse en cours...',en:'Analyzing...'},autoLoadText:{fr:'Chargement automatique depuis Google Apps Script...',en:'Auto-loading from Google Apps Script...'},countryLabel:{fr:'Pays',en:'Country'},allCountries:{fr:'Tous pays',en:'All countries'},requirementLabel:{fr:'Exigence',en:'Requirement'},allRequirements:{fr:'Toutes exig.',en:'All reqs.'},typeLabel:{fr:'Type susp.',en:'Susp. type'},allTypes:{fr:'Tous types',en:'All types'},datePeriodLabel:{fr:'Période',en:'Period'},dateToText:{fr:'au',en:'to'},totalTitle:{fr:'Total Susp.',en:'Total Susp.'},totalLabel:{fr:'suspensions',en:'suspensions'},koCount:{fr:'KO',en:'KO'},koLabel:{fr:'NC KO',en:'KO NCs'},majorCount:{fr:'Major',en:'Major'},majorLabel:{fr:'NC Majeures',en:'Major NCs'},countriesTitle:{fr:'Pays Impactés',en:'Impacted Countries'},countriesLabel:{fr:'pays',en:'countries'},typesChartTitle:{fr:'Types Susp.',en:'Susp. Types'},requirementsChartTitle:{fr:'Exig. Impliquées',en:'Involved Reqs.'},chaptersChartTitle:{fr:'Distr. Chapitre',en:'Chapter Distr.'},countriesDistribChartTitle:{fr:"Répartition par Pays",en:"Distribution by Country"},scopesDistribChartTitle:{fr:"Répartition par Secteur Produit",en:"Distribution by Product Scope"},tableMainTitle:{fr:'Liste Suspensions',en:'Suspensions List'},supplierHeader:{fr:'Fournisseur',en:'Supplier'},countryHeader:{fr:'Pays',en:'Country'},dateHeader:{fr:'Date Susp.',en:'Susp. Date'},requirementsHeader:{fr:'Exigence(s)',en:'Requirement(s)'},typesHeader:{fr:'Type(s)',en:'Type(s)'},actionsHeader:{fr:'Actions',en:'Actions'},detailsButton:{fr:'Détails',en:'Details'},noDataMessage:{fr:'Chargez données pour analyse.',en:'Load data for analysis.'},noFilterResults:{fr:'Aucun résultat pour filtres.',en:'No results for filters.'},detailsPanelTitle:{fr:'Détails Suspension',en:'Suspension Details'},exportBtnText:{fr:'Exporter (CSV)',en:'Export (CSV)'},nonPaymentOption:{fr:'Non-paiement',en:'Non-payment'},terminationOption:{fr:'Arrêt certif.',en:'Cert. term.'},translateButton:{fr:'Traduire avec IA',en:'Translate with AI'},originalTextButton:{fr:'Texte original',en:'Original text'},reasonLabel:{fr:'Raison:',en:'Reason:'},supplierLabel:{fr:'Fourn.:',en:'Supplier:'},addressLabel:{fr:'Adresse:',en:'Address:'},suspDateLabel:{fr:'Date susp.:',en:'Susp. date:'},issueDateLabel:{fr:'Date émission:',en:'Issue date:'},productScopeLabel:{fr:'Scope(s) Produit:',en:'Product Scope(s):'},lockHistoryLabel:{fr:'Historique:',en:'History:'},nextAuditLabel:{fr:'Proch. audit:',en:'Next audit:'},noInfoAvailable:{fr:'N/A',en:'N/A'},periodText:{fr:"Données non temps réel",en:"NOT Real-time data"},modalCloseButtonText:{fr:"Fermer", en:"Close"},productScopesTitle:{fr:"Secteurs de produits IFS",en:"IFS Product Scopes"},searchLabel:{fr:"Rechercher dans la raison",en:"Search in reason"},searchPlaceholder:{fr:"Entrez un mot-clé...",en:"Enter keyword..."},uploadLoadedMessage:{fr:"Données chargées. ",en:"Data loaded. "},reloadOptionsTriggerText:{fr:"Afficher les options de chargement.",en:"Show loading options."},errorAppsScriptConfig:{fr:"URL Google Apps Script non configurée. Veuillez vérifier la configuration.",en:"Google Apps Script URL not configured. Please check configuration."},autoLoadFailed:{fr:"Échec du chargement automatique. Utilisez les boutons manuels ci-dessous.",en:"Auto-load failed. Use manual buttons below."},gsheetConnectionError:{fr:"Impossible de se connecter à Google Apps Script",en:"Unable to connect to Google Apps Script"},retryText:{fr:"Réessayer",en:"Retry"},translatingText:{fr:"Traduction en cours...",en:"Translating..."},translationError:{fr:"Erreur de traduction",en:"Translation error"}};
function initCharts() {
Chart.defaults.font.family = "'Inter', sans-serif"; Chart.defaults.font.size = 12; Chart.defaults.color = '#4a5568';
const commonDoughnutOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } } }, cutout: '60%' };
typesChart = new Chart(document.getElementById('typesChart').getContext('2d'), { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: ['#f56565', '#ed8936', '#48bb78', '#667eea', '#9f7aea'], borderWidth: 0, hoverOffset: 10 }] }, options: commonDoughnutOptions });
requirementsChart = new Chart(document.getElementById('requirementsChart').getContext('2d'), { type: 'bar', data: { labels: [], datasets: [{ label: '', data: [], backgroundColor: '#667eea', borderRadius: 6, borderSkipped: false }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } }, x: { grid: { display: false } } } } });
chaptersChart = new Chart(document.getElementById('chaptersChart').getContext('2d'), { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: ['#667eea', '#f56565', '#48bb78', '#ed8936', '#9f7aea', '#38b2ac', '#ecc94b'], borderWidth: 0, hoverOffset: 10 }] }, options: commonDoughnutOptions });
countriesDistribChart = new Chart(document.getElementById('countriesDistribChart').getContext('2d'), { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: ['#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40','#C9CBCF','#2ECC71','#E74C3C','#F1C40F','#8E44AD','#3498DB'] }] }, options: commonDoughnutOptions });
scopesDistribChart = new Chart(document.getElementById('scopesDistribChart').getContext('2d'), { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: ['#FFB1C1','#AED6F1','#F9E79F','#A2D9CE','#D7BDE2','#FAD7A0','#E5E7E9','#A9DFBF','#F5B7B1','#F7DC6F','#D2B4DE','#A3E4D7'] }] }, options: commonDoughnutOptions });
}
function parseDateString(dateStr) {
if (!dateStr || typeof dateStr !== 'string') return null;
// Format DD.MM.YYYY
if (dateStr.includes('.')) {
const parts = dateStr.split('.');
return parts.length === 3 ? new Date(parts[2], parts[1] - 1, parts[0]) : null;
}
// Format DD/MM/YYYY
if (dateStr.includes('/')) {
const parts = dateStr.split('/');
return parts.length === 3 ? new Date(parts[2], parts[1] - 1, parts[0]) : null;
}
// Essayer de parser comme date standard
const parsed = new Date(dateStr);
return isNaN(parsed.getTime()) ? null : parsed;
}
// 📅 FONCTION DE FORMATAGE DES DATES
function formatDateForDisplay(dateValue) {
if (!dateValue) return 'N/A';
let date;
// Si c'est déjà une chaîne au format DD.MM.YYYY, la convertir
if (typeof dateValue === 'string') {
if (dateValue.includes('.')) {
// Format DD.MM.YYYY
const parts = dateValue.split('.');
if (parts.length === 3) {
return `${parts[0]}/${parts[1]}/${parts[2]}`;
}
}
// Essayer de parser comme date JavaScript
date = new Date(dateValue);
} else if (dateValue instanceof Date) {
date = dateValue;
} else {
return dateValue; // Retourner tel quel si format inconnu
}
// Vérifier si la date est valide
if (isNaN(date.getTime())) {
return dateValue; // Retourner la valeur originale si pas une date valide
}
// Formater au format DD/MM/YYYY
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
// 📅 FONCTION POUR CONVERTIR UNE DATE D'AFFICHAGE EN OBJET DATE
function parseDisplayDate(displayDate) {
if (!displayDate || displayDate === 'N/A') return null;
// Format DD/MM/YYYY
if (displayDate.includes('/')) {
const parts = displayDate.split('/');
if (parts.length === 3) {
return new Date(parts[2], parts[1] - 1, parts[0]);
}
}
return null;
}
// 📅 FONCTION POUR DÉFINIR DES PÉRIODES PRÉDÉFINIES
window.setDatePeriod = function(period) {
const dateFromInput = document.getElementById('dateFromFilter');
const dateToInput = document.getElementById('dateToFilter');
const today = new Date();
switch(period) {
case 'thisMonth':
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);
dateFromInput.value = formatDateForInput(startOfMonth.toDateString());
dateToInput.value = formatDateForInput(endOfMonth.toDateString());
break;
case 'last3Months':
const threeMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 3, 1);
dateFromInput.value = formatDateForInput(threeMonthsAgo.toDateString());
dateToInput.value = formatDateForInput(today.toDateString());
break;
case 'thisYear':
const startOfYear = new Date(today.getFullYear(), 0, 1);
const endOfYear = new Date(today.getFullYear(), 11, 31);
dateFromInput.value = formatDateForInput(startOfYear.toDateString());
dateToInput.value = formatDateForInput(endOfYear.toDateString());
break;
case 'clear':
dateFromInput.value = '';
dateToInput.value = '';
break;
}
// Appliquer les filtres après avoir défini la période
applyFilters();
};
function updateDatePeriodButtons() {
const buttons = document.querySelectorAll('.date-period-btn');
const texts = {
fr: {
thisMonth: 'Ce mois',
last3Months: '3 derniers mois',
thisYear: 'Cette année',
clear: 'Effacer'
},
en: {
thisMonth: 'This month',
last3Months: 'Last 3 months',
thisYear: 'This year',
clear: 'Clear'
}
};
buttons.forEach(btn => {
const period = btn.getAttribute('data-period');
if (texts[currentLanguage] && texts[currentLanguage][period]) {
btn.textContent = texts[currentLanguage][period];
}
});
}
// 📅 FONCTION POUR OBTENIR LA DATE AU FORMAT YYYY-MM-DD (pour les inputs date)
function formatDateForInput(dateStr) {
let date;
if (dateStr instanceof Date) {
date = dateStr;
} else {
date = parseDateString(dateStr) || new Date(dateStr);
}
if (!date || isNaN(date.getTime())) return '';
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
function processData(data) {
if (data.length > 0) { console.log("Clés du premier audit:", Object.keys(data[0])); }
showElements();
allAuditData = data.map(audit => ({...audit}));
allAuditData.sort((a, b) => (a["Country/Region"] || '').localeCompare(b["Country/Region"] || '') || (a.Supplier || '').localeCompare(b.Supplier || ''));
updateGlobalStatsAndFilters(allAuditData);
applyFilters();
showElements();
updateUploadSectionUI(false, allAuditData.length);
currentDetailedAudit = null;
}
function updateGlobalStatsAndFilters(dataToAnalyze) {
requirementStats = {}; chapterStats = {}; typeStats = {'KO':0,'Major':0,'Minor':0,'Non-paiement':0,'Arrêt de certification':0};
countries = []; requirements = [];
const uniqueProductScopes = new Set();
dataToAnalyze.forEach(audit => {
const country = audit["Country/Region"];
if (country && !countries.includes(country)) countries.push(country);
const scopesString = audit["Product scopes"] || "";
scopesString.split(',').forEach(scopeNumStr => {
const scopeNum = scopeNumStr.trim();
if (scopeNum && productScopesData[scopeNum]) { uniqueProductScopes.add(scopeNum); }
});
const lockReason = audit["Lock reason"] || "";
const extractedReqs = getRequirementsFromReason(lockReason);
extractedReqs.forEach(req => {
requirementStats[req] = (requirementStats[req] || 0) + 1;
if (!requirements.includes(req)) requirements.push(req);
chapterStats[req.split('.')[0]] = (chapterStats[req.split('.')[0]] || 0) + 1;
});
getTypesFromReason(lockReason, extractedReqs).forEach(type => { if (typeStats.hasOwnProperty(type)) typeStats[type]++; });
});
updateFilterDropdowns();
populateProductScopesFilter(Array.from(uniqueProductScopes).sort((a,b) => parseInt(a) - parseInt(b)));
updateStatsCards(dataToAnalyze);
}
function populateProductScopesFilter(scopes) {
const container = document.getElementById('scopesCheckboxContainer');
container.innerHTML = '';
document.getElementById('product-scopes-title').textContent = uiTexts.productScopesTitle[currentLanguage];
scopes.forEach(scopeKey => {
const scopeInfo = productScopesData[scopeKey];
if (scopeInfo) {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox'; checkbox.value = scopeKey; checkbox.id = `scope-${scopeKey}`;
checkbox.name = "productScope"; checkbox.addEventListener('change', applyFilters);
label.htmlFor = `scope-${scopeKey}`; label.appendChild(checkbox);
label.appendChild(document.createTextNode(`${scopeKey}: ${scopeInfo[currentLanguage]}`));
container.appendChild(label);
}
});
}
function updateFilterDropdowns() {
const countryFilterEl = document.getElementById('countryFilter');
const requirementFilterEl = document.getElementById('requirementFilter');
countryFilterEl.innerHTML = `<option value="all">${uiTexts.allCountries[currentLanguage]}</option>`;
countries.sort().forEach(c => countryFilterEl.add(new Option(c, c)));
requirementFilterEl.innerHTML = `<option value="all">${uiTexts.allRequirements[currentLanguage]}</option>`;
requirements.sort((a, b) => {
const partsA = a.split('.').map(Number); const partsB = b.split('.').map(Number);
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
if ((partsA[i] || 0) !== (partsB[i] || 0)) return (partsA[i] || 0) - (partsB[i] || 0);
} return 0;
}).forEach(req => {
const translation = requirementTranslations[req]?.[currentLanguage] || req;
requirementFilterEl.add(new Option(`${req} - ${translation}`, req));
});
}
function updateCharts(dataForCharts) {
const localTS = {'KO':0,'Major':0,'Minor':0,'Non-paiement':0,'Arrêt de certification':0}, localRS = {}, localCS = {}, localCountriesStats = {}, localScopesStats = {};
dataForCharts.forEach(audit => {
const lockReason = audit["Lock reason"] || "", extractedReqs = getRequirementsFromReason(lockReason);
extractedReqs.forEach(req => { localRS[req]=(localRS[req]||0)+1; localCS[req.split('.')[0]]=(localCS[req.split('.')[0]]||0)+1; });
getTypesFromReason(lockReason, extractedReqs).forEach(type => { if (localTS.hasOwnProperty(type)) localTS[type]++; });
const country = audit["Country/Region"]; if (country) localCountriesStats[country] = (localCountriesStats[country] || 0) + 1;
const scopesString = audit["Product scopes"] || "";
scopesString.split(',').forEach(scopeNumStr => {
const scopeNum = scopeNumStr.trim();
if (scopeNum && productScopesData[scopeNum]) {
const scopeDesc = productScopesData[scopeNum][currentLanguage];
localScopesStats[scopeDesc] = (localScopesStats[scopeDesc] || 0) + 1;
}
});
});
typesChart.data.labels = [uiTexts.koCount[currentLanguage],uiTexts.majorCount[currentLanguage],'Minor',uiTexts.nonPaymentOption[currentLanguage],uiTexts.terminationOption[currentLanguage]];
typesChart.data.datasets[0].data = [localTS.KO,localTS.Major,localTS.Minor,localTS['Non-paiement'],localTS['Arrêt de certification']]; typesChart.update();
const sortedReqs = Object.entries(localRS).sort((a,b)=>b[1]-a[1]).slice(0,10);
requirementsChart.data.labels = sortedReqs.map(r => `${r[0]} (${(requirementTranslations[r[0]]?.[currentLanguage]||r[0]).substring(0,15)}...)`);
requirementsChart.data.datasets[0].data = sortedReqs.map(r=>r[1]); requirementsChart.data.datasets[0].label=uiTexts.requirementsChartTitle[currentLanguage]; requirementsChart.update();
const chapterEntries = Object.entries(localCS);
chaptersChart.data.labels = chapterEntries.map(c=>`${c[0]}. ${ifsChaptersV8[c[0]]?.[currentLanguage]||`Chap. ${c[0]}`}`);
chaptersChart.data.datasets[0].data = chapterEntries.map(c=>c[1]); chaptersChart.update();
const countryLabels = Object.keys(localCountriesStats), countryData = Object.values(localCountriesStats);
countriesDistribChart.data.labels = countryLabels; countriesDistribChart.data.datasets[0].data = countryData;
if(countryLabels.length > countriesDistribChart.data.datasets[0].backgroundColor.length) countriesDistribChart.data.datasets[0].backgroundColor = countryLabels.map(()=>`hsl(${Math.random()*360},70%,60%)`);
countriesDistribChart.update();
const scopeLabels = Object.keys(localScopesStats), scopeData = Object.values(localScopesStats);
scopesDistribChart.data.labels = scopeLabels; scopesDistribChart.data.datasets[0].data = scopeData;
if(scopeLabels.length > scopesDistribChart.data.datasets[0].backgroundColor.length) scopesDistribChart.data.datasets[0].backgroundColor = scopeLabels.map(()=>`hsl(${Math.random()*360},65%,70%)`);
scopesDistribChart.update();
}
function updateStatsCards(dataForStats) {
const localTS = {'KO':0,'Major':0,'Minor':0,'Non-paiement':0,'Arrêt de certification':0}, uC = new Set();
dataForStats.forEach(audit => {
if(audit["Country/Region"]) uC.add(audit["Country/Region"]);
const lockReason = audit["Lock reason"] || "", extractedReqs = getRequirementsFromReason(lockReason);
getTypesFromReason(lockReason, extractedReqs).forEach(type => { if (localTS.hasOwnProperty(type)) localTS[type]++; });
});
document.getElementById('totalLocks').textContent=dataForStats.length; document.getElementById('koCount').textContent=localTS.KO;
document.getElementById('majorCount').textContent=localTS.Major; document.getElementById('countriesCount').textContent=uC.size;
}
function updateTable(data) {
const tableBody = document.getElementById('auditsTableBody'); tableBody.innerHTML = '';
if (data.length === 0) {
const r=tableBody.insertRow(),c=r.insertCell();c.colSpan=6;c.className='no-data';
c.textContent = allAuditData.length > 0 ? uiTexts.noFilterResults[currentLanguage] : uiTexts.noDataMessage[currentLanguage]; return;
}
data.forEach(audit => {
const r = tableBody.insertRow();
const cellSupplier = r.insertCell(); cellSupplier.textContent = audit.Supplier||'N/A'; cellSupplier.className = 'col-supplier';
const cellCountry = r.insertCell(); cellCountry.textContent = audit["Country/Region"]||'N/A'; cellCountry.className = 'col-country';
const cellDate = r.insertCell();
cellDate.textContent = formatDateForDisplay(audit["Certificate/Assessment lock date"]);
cellDate.className = 'col-date';
const reqCell = r.insertCell(); reqCell.className = 'col-requirements'; reqCell.innerHTML = formatRequirementsForDisplay(audit["Lock reason"]||"");
const typeCell = r.insertCell(); typeCell.className = 'col-types'; typeCell.innerHTML = formatTypesForDisplay(audit["Lock reason"]||"");
const actionCell = r.insertCell(); actionCell.className = 'col-actions';
const btn = document.createElement('button'); btn.className='btn btn-primary';
btn.innerHTML = `<i class="fas fa-eye"></i> ${uiTexts.detailsButton[currentLanguage]}`;
btn.onclick = () => showDetails(audit); actionCell.appendChild(btn);
});
}
function getRequirementsFromReason(reason) {
if (!reason) return []; const pM = reason.match(/\b(\d+\.\d+\.\d+|\d+\.\d+)\b/g)||[];
return pM.map(req => { if(req.split('.').length===2 && !validIFSRequirements.includes(req)) return validIFSRequirements.find(v=>v.startsWith(req+'.'))||req; return req; }).filter(req => validIFSRequirements.includes(req));
}
function formatRequirementsForDisplay(reason) {
const reqs=getRequirementsFromReason(reason); if(reqs.length===0)return 'N/A';
return reqs.map(req=>{let kI=koRequirements.includes(req)?' <span class="ko-highlight">(KO)</span>':'';const t=requirementTranslations[req]?.[currentLanguage]||req;let dT=t.replace(/\(KO\)/gi,'<span class="ko-highlight">(KO)</span>');if(koRequirements.includes(req)&&!t.toUpperCase().includes("(KO)"))kI=' <span class="ko-highlight">(KO)</span>';return`<span class="requirement-highlight tooltip">${req}${kI}<span class="tooltiptext">${dT}</span></span>`}).join(' ');
}
function getTypesFromReason(reason,rIR=null){if(!reason)return[];const ts=new Set(),rqs=rIR||getRequirementsFromReason(reason);if(/\bKO\b/i.test(reason))ts.add('KO');if(/\bMajor\b/i.test(reason))ts.add('Major');if(/\bMinor\b/i.test(reason))ts.add('Minor');rqs.forEach(req=>{if(koRequirements.includes(req))ts.add('KO')});if(/((non[-\s]?paiement)|(frais)|(paie)|(paye)|(cotisation)|(facture))/i.test(reason))ts.add('Non-paiement');if(/arrêt de certification/i.test(reason))ts.add('Arrêt de certification');return Array.from(ts)}
function formatTypesForDisplay(reason){const ts=getTypesFromReason(reason);if(ts.length===0)return 'N/A';return ts.map(t=>{let cN='',txt=t;switch(t){case'KO':cN='ko-highlight';txt=uiTexts.koCount[currentLanguage];break;case'Major':cN='major-highlight';txt=uiTexts.majorCount[currentLanguage];break;case'Minor':cN='minor-highlight';break;case'Non-paiement':cN='nonpayment-highlight';txt=uiTexts.nonPaymentOption[currentLanguage];break;case'Arrêt de certification':cN='termination-highlight';txt=uiTexts.terminationOption[currentLanguage];break}return`<span class="type-highlight ${cN}">${txt}</span>`}).join(' ')}
// 🎯 FONCTION SHOWDETAILS AVEC TRADUCTION IA
function showDetails(audit) {
currentDetailedAudit = audit;
const modalOverlay = document.getElementById('detailsModalOverlay');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBodyContent');
const modalFooterClose = document.getElementById('modalFooterCloseBtn');
const lockReason = audit["Lock reason"] || uiTexts.noInfoAvailable[currentLanguage];
modalTitle.textContent = uiTexts.detailsPanelTitle[currentLanguage] +
(audit.Supplier ? ` - ${audit.Supplier}` : '');
// Génération du contenu modal avec traduction IA
modalBody.innerHTML = generateModalContent(audit, lockReason);
modalFooterClose.textContent = uiTexts.modalCloseButtonText[currentLanguage];
modalOverlay.style.display = 'flex';
// Gestion des boutons de traduction IA
const translateBtn = document.getElementById('translateBtnDetailsModal');
const originalBtn = document.getElementById('originalBtnDetailsModal');
const testBtn = document.getElementById('testTranslateBtn');
const reasonElement = document.getElementById('reasonTextDetailsModal');
// Bouton de test API
if (testBtn) {
testBtn.onclick = async () => {
console.log('🧪 Test API MyMemory lancé');
testBtn.disabled = true;
testBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Test...';
try {
const testText = "Hello, this is a test.";
const result = await translationManager.translator.translateText(testText, 'fr', 'en');
console.log('✅ Test réussi:', result);
alert(`Test API réussi ! 🎉\n\nOriginal: "${testText}"\nTraduit: "${result}"\n\nVous pouvez maintenant essayer la traduction complète.`);
} catch (error) {
console.error('❌ Test échoué:', error);
alert(`Test API échoué: ${error.message}\n\nL'API MyMemory pourrait être temporairement indisponible.`);
} finally {
testBtn.disabled = false;
testBtn.innerHTML = '<i class="fas fa-flask"></i> Test API';
}
};
}
if (translateBtn && originalBtn && reasonElement) {
translateBtn.onclick = async () => {
console.log('🎬 Bouton traduction cliqué !');
// Désactiver le bouton et afficher l'indicateur de chargement
translateBtn.disabled = true;
const originalBtnText = translateBtn.innerHTML;
// Déterminer le message de chargement selon la longueur du texte
const loadingMsg = lockReason.length > 450 ?
(currentLanguage === 'fr' ? 'Traduction en cours (texte long)...' : 'Translating (long text)...') :
uiTexts.translatingText[currentLanguage];
translateBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${loadingMsg}`;
try {
const targetLang = currentLanguage === 'fr' ? 'fr' : 'en';
console.log(`🎯 Langue actuelle: ${currentLanguage}, Langue cible: ${targetLang}`);
console.log(`📝 Texte à traduire: "${lockReason.substring(0, 100)}..." (${lockReason.length} caractères)`);
const translatedReason = await translationManager.translateLockReason(lockReason, targetLang);
console.log(`✅ Traduction reçue: "${translatedReason.substring(0, 100)}..." (${translatedReason.length} caractères)`);
reasonElement.innerHTML = formatTextWithHighlights(translatedReason, currentLanguage);
translateBtn.style.display = 'none';
originalBtn.style.display = 'inline-block';
console.log('🎉 Traduction affichée avec succès !');
} catch (error) {
console.error('❌ Erreur de traduction:', error);
reasonElement.innerHTML = formatTextWithHighlights(lockReason, currentLanguage);
// Afficher une alerte plus détaillée selon le type d'erreur
let errorMsg;
if (error.message.includes('QUERY LENGTH LIMIT')) {
errorMsg = currentLanguage === 'fr' ?
`Le texte est trop long pour être traduit en une fois (${lockReason.length} caractères). La traduction par morceaux a échoué.` :
`Text is too long to translate at once (${lockReason.length} characters). Chunked translation failed.`;
} else {
errorMsg = currentLanguage === 'fr' ?
`Erreur de traduction: ${error.message}. Le texte original est affiché.` :
`Translation error: ${error.message}. Original text is displayed.`;
}
alert(errorMsg);
} finally {
translateBtn.disabled = false;
translateBtn.innerHTML = originalBtnText;
}
};
originalBtn.onclick = () => {
console.log('🔄 Retour au texte original');
reasonElement.innerHTML = formatTextWithHighlights(lockReason, currentLanguage);
translateBtn.style.display = 'inline-block';
originalBtn.style.display = 'none';
};
}
}
function generateModalContent(audit, lockReason) {
const isLongText = lockReason.length > 450;
const lengthIndicator = isLongText ?
`<span class="text-length-indicator">📏 ${lockReason.length} caractères - Traduction en plusieurs étapes</span>` : '';
return `
<p><strong>${uiTexts.supplierLabel[currentLanguage]}</strong> ${audit.Supplier || 'N/A'}</p>
<p><strong>${uiTexts.addressLabel[currentLanguage]}</strong> ${audit.Address || 'N/A'}, ${audit.City || ''}, ${audit.ZIP || ''}, ${audit["Country/Region"] || ''}</p>
<p><strong>${uiTexts.suspDateLabel[currentLanguage]}</strong> ${formatDateForDisplay(audit["Certificate/Assessment lock date"])}</p>
<p><strong>${uiTexts.issueDateLabel[currentLanguage]}</strong> ${formatDateForDisplay(audit["Certificate/Assessment issue date"])}</p>
<p><strong>${uiTexts.productScopeLabel[currentLanguage]}</strong> ${audit["Product scopes"] || 'N/A'}</p>
<p><strong>${uiTexts.lockHistoryLabel[currentLanguage]}</strong> ${audit["Lock history"] || 'N/A'}</p>
<p><strong>${uiTexts.nextAuditLabel[currentLanguage]}</strong> ${audit["Next audit to be performed"] || 'N/A'}</p>
<hr>
<div class="translation-controls">
<button id="translateBtnDetailsModal" class="btn btn-ai-translate">
<i class="fas fa-brain"></i>
<span>${uiTexts.translateButton[currentLanguage]}</span>
</button>
<button id="originalBtnDetailsModal" class="btn btn-secondary" style="display:none;">
<i class="fas fa-undo"></i>
<span>${uiTexts.originalTextButton[currentLanguage]}</span>
</button>
<button id="testTranslateBtn" class="btn btn-secondary" style="margin-left: 10px;">
<i class="fas fa-flask"></i>
Test API
</button>
<span class="translation-status">
<i class="fas fa-info-circle"></i>
${currentLanguage === 'fr' ? 'Traduction automatique via MyMemory API' : 'Automatic translation via MyMemory API'}
${lengthIndicator}
</span>
</div>
<p><strong>${uiTexts.reasonLabel[currentLanguage]}</strong></p>
<div class="lock-reason" id="reasonTextDetailsModal">
${formatTextWithHighlights(lockReason, currentLanguage)}
</div>
`;
}
function closeDetailsModal(){document.getElementById('detailsModalOverlay').style.display='none'}
function formatTextWithHighlights(text,lang){let fT=text;getTypesFromReason(text).forEach(ty=>{let tRP=ty.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),dT=ty,cN='';if(ty==='KO'){cN='ko-highlight';dT=uiTexts.koCount[lang]}else if(ty==='Major'){cN='major-highlight';dT=uiTexts.majorCount[lang]}else if(ty==='Minor'){cN='minor-highlight'}else if(ty==='Non-paiement'){cN='nonpayment-highlight';dT=uiTexts.nonPaymentOption[lang];tRP="(non[-\\s]?paiement|non-payment|frais|paie|paye|cotisation|facture)"}else if(ty==='Arrêt de certification'){cN='termination-highlight';dT=uiTexts.terminationOption[lang];tRP="(arrêt de certification|certification termination)"}fT=fT.replace(new RegExp(`\\b${tRP}\\b`,'gi'),`<span class="type-highlight ${cN}">${dT}</span>`)});fT=fT.replace(/\b(\d+\.\d+\.\d+|\d+\.\d+)\b/g,m=>{const r=getRequirementsFromReason(m);if(r.length>0){const R=r[0],tr=requirementTranslations[R]?.[lang]||R;let kI='';if(koRequirements.includes(R)&&!tr.toUpperCase().includes("(KO)"))kI=' <span class="ko-highlight">(KO)</span>';let dT=tr.replace(/\(KO\)/gi,'<span class="ko-highlight">(KO)</span>');return`<span class="requirement-highlight tooltip">${R}${kI}<span class="tooltiptext">${dT}</span></span>`}return m});return fT}
function showElements(){['filters','stats-dashboard','charts-grid','table-card','export-section'].forEach(id=>document.getElementById(id).style.display=id.includes('filter')||id.includes('stats')||id.includes('chart')?'grid':'block')}
function switchLanguage(lang){
currentLanguage = lang;
const t = uiTexts;
document.title = t.pageMainTitle[lang];
document.getElementById('page-main-title').textContent = t.pageMainTitle[lang];
document.getElementById('page-subtitle').textContent = t.pageSubtitle[lang];
document.getElementById('period-text').textContent = t.periodText[lang];
updateUploadSectionUI(allAuditData.length === 0, allAuditData.length);
document.getElementById('loading-text').textContent = t.loadingText[lang];
document.getElementById('product-scopes-title').textContent = t.productScopesTitle[lang];
document.getElementById('search-label').textContent = t.searchLabel[lang];
document.getElementById('searchInput').placeholder = t.searchPlaceholder[lang];
const idsToUpdate = {
'country-label':'countryLabel', 'all-countries':'allCountries',
'requirement-label':'requirementLabel', 'all-requirements':'allRequirements',
'type-label':'typeLabel', 'all-types':'allTypes',
'date-period-label':'datePeriodLabel', 'date-to-text':'dateToText',
'non-payment-option':'nonPaymentOption', 'termination-option':'terminationOption'
};
Object.keys(idsToUpdate).forEach(id => {
const el = document.getElementById(id);
const key = idsToUpdate[id];
if (el && t[key] && t[key][lang] !== undefined) {
el.textContent = t[key][lang];
}
});
document.getElementById('total-title').textContent = t.totalTitle[lang];
document.getElementById('total-label').textContent = t.totalLabel[lang];
document.getElementById('ko-title-text').textContent = t.koCount[lang];
document.getElementById('ko-label').textContent = t.koLabel[lang];
document.getElementById('major-title-text').textContent = t.majorCount[lang];
document.getElementById('major-label').textContent = t.majorLabel[lang];
document.getElementById('countries-title').textContent = t.countriesTitle[lang];
document.getElementById('countries-label').textContent = t.countriesLabel[lang];
const generalTxtEl = {
'types-chart-title':'typesChartTitle', 'requirements-chart-title':'requirementsChartTitle',
'chapters-chart-title':'chaptersChartTitle', 'countries-distrib-chart-title':'countriesDistribChartTitle',
'scopes-distrib-chart-title':'scopesDistribChartTitle', 'table-main-title':'tableMainTitle',
'actions-header':'actionsHeader', 'export-btn-text':'exportBtnText'
};
Object.keys(generalTxtEl).forEach(id => {
const el = document.getElementById(id);
const key = generalTxtEl[id];
if (el && t[key] && t[key][lang] !== undefined) {
el.textContent = t[key][lang];
}
});
const tableHdrs = {
'supplier-header':'supplierHeader', 'country-header':'countryHeader',
'date-header':'dateHeader', 'requirements-header':'requirementsHeader',
'types-header':'typesHeader'
};
Object.keys(tableHdrs).forEach(id => {
const headerElement = document.getElementById(id);
const textKey = tableHdrs[id];
if (headerElement && t[textKey] && t[textKey][lang] !== undefined) {
if (id === 'requirements-header' || id === 'types-header') {
headerElement.textContent = t[textKey][lang];
} else {
const sortIconSpan = headerElement.querySelector('.sort-icon');
while (headerElement.firstChild) {
headerElement.removeChild(headerElement.firstChild);
}
headerElement.appendChild(document.createTextNode(t[textKey][lang] + ' '));
if (sortIconSpan) {
headerElement.appendChild(sortIconSpan);
}
}
}
});
const actionsHeaderEl = document.getElementById('actions-header');
if(actionsHeaderEl && t.actionsHeader && t.actionsHeader[lang]) {
actionsHeaderEl.textContent = t.actionsHeader[lang];
}
document.getElementById('lang-fr').classList.toggle('active', lang === 'fr');
document.getElementById('lang-en').classList.toggle('active', lang === 'en');
if (allAuditData.length > 0) {
updateFilterDropdowns();
populateProductScopesFilter(Array.from(new Set(allAuditData.flatMap(a => (a["Product scopes"] || "").split(',').map(s => s.trim()).filter(s => s)))).sort((a,b) => parseInt(a) - parseInt(b)));
applyFilters();
} else {
updateTable([]);
}
// Mettre à jour les boutons de période de dates
updateDatePeriodButtons();
if (document.getElementById('detailsModalOverlay').style.display === 'flex' && currentDetailedAudit) {
showDetails(currentDetailedAudit);
}
}
function updateUploadSectionUI(isInitial = true, dataLength = 0) {
const uploadContainer = document.getElementById('upload-section-container');
const t = uiTexts;
if (!isInitial && dataLength > 0) {
uploadContainer.classList.add('shrunk');
uploadContainer.innerHTML = `
<h2 id="upload-title">${t.uploadSectionDemoLoadedTitle[currentLanguage]}</h2>
<p id="upload-loaded-message">${dataLength} ${t.uploadSectionDemoLoadedDescription[currentLanguage]}
<span class="reload-options-trigger" id="reloadOptionsTrigger">${t.reloadOptionsTriggerText[currentLanguage]}</span>
</p>
<div class="main-upload-options" style="display:none;">
<button class="btn-load-gsheet" id="loadGoogleSheetBtn"><i class="fab fa-google-drive"></i> <span id="loadGoogleSheetBtnText">${t.loadGoogleSheetBtnText[currentLanguage]}</span></button>
<label for="fileInput" class="upload-btn"><i class="fas fa-file-csv"></i> <span id="upload-btn-text">${t.uploadBtnText[currentLanguage]}</span></label>
<input type="file" id="fileInput" accept=".csv" style="display:none;">
<button class="upload-btn btn-demo" id="demo-btn"><i class="fas fa-redo"></i> <span id="demo-btn-text">${t.reloadDemoBtnText[currentLanguage]}</span></button>
</div>`;
const reloadTrigger = document.getElementById('reloadOptionsTrigger');
if (reloadTrigger) {
reloadTrigger.addEventListener('click', () => {
uploadContainer.classList.remove('shrunk');
const mainOptions = uploadContainer.querySelector('.main-upload-options');
if (mainOptions) mainOptions.style.display = 'block';
reloadTrigger.style.display = 'none';
if (document.getElementById('fileInput')) document.getElementById('fileInput').addEventListener('change', handleFileUpload);
if (document.getElementById('demo-btn')) document.getElementById('demo-btn').addEventListener('click', loadDemoData);
if (document.getElementById('loadGoogleSheetBtn')) document.getElementById('loadGoogleSheetBtn').addEventListener('click', handleLoadFromGoogleSheet);
});
}
} else {
uploadContainer.classList.remove('shrunk');
const gSheetButtonHTML = `<button class="btn-load-gsheet" id="loadGoogleSheetBtn"><i class="fab fa-google-drive"></i> <span id="loadGoogleSheetBtnText">${t.loadGoogleSheetBtnText[currentLanguage]}</span></button>`;
const fileInputButtonHTML = `<label for="fileInput" class="upload-btn"><i class="fas fa-file-csv"></i> <span id="upload-btn-text">${t.uploadBtnText[currentLanguage]}</span></label><input type="file" id="fileInput" accept=".csv" style="display:none;">`;
const demoButtonHTML = `<button class="upload-btn btn-demo" id="demo-btn"><i class="fas fa-play"></i> <span id="demo-btn-text">${t.demoBtnText[currentLanguage]}</span></button>`;
uploadContainer.innerHTML = `
<div class="upload-icon"><i class="fas fa-cloud-upload-alt"></i></div>
<h2 id="upload-title">${t.uploadSectionTitle[currentLanguage]}</h2>
<p id="upload-description">${t.uploadSectionDescription[currentLanguage]}</p>
<div class="main-upload-options">${gSheetButtonHTML}${fileInputButtonHTML}${demoButtonHTML}</div>`;
}
if (document.getElementById('fileInput')) document.getElementById('fileInput').addEventListener('change', handleFileUpload);
if (document.getElementById('demo-btn')) document.getElementById('demo-btn').addEventListener('click', loadDemoData);
if (document.getElementById('loadGoogleSheetBtn')) document.getElementById('loadGoogleSheetBtn').addEventListener('click', handleLoadFromGoogleSheet);
}
function generateDemoData() {
return [{"Supplier":"Seawell Fresh ApS","Country/Region":"Denmark", "Product scopes":"2, 7","Certificate/Assessment lock date":"23.04.2025","Lock reason":"5.1.1 KO: Internal audit not done. 5.1.2 Major: Review missing. 5.7.2 KO: Old NC persist.","Address":"Fiskerivej 12","City":"Copenhagen","ZIP":"2100","Certificate/Assessment issue date":"15.01.2023"},
{"Supplier":"FoodCorp Industries","Country/Region":"Germany", "Product scopes":"4, 1","Certificate/Assessment lock date":"18.04.2025","Lock reason":"4.12.1 KO: foreign material risk. 4.10.1 Major: cleaning. 3.2.2 KO: hygiene.","Address":"Industriestr. 45","City":"Munich","ZIP":"80331","Certificate/Assessment issue date":"22.03.2023"},
{"Supplier":"Organic Harvest","Country/Region":"United Kingdom", "Product scopes":"5","Certificate/Assessment lock date":"25.04.2025","Lock reason":"Arrêt de certification. Contract terminated.","Address":"Green Valley","City":"Manchester","ZIP":"M1 1AA","Certificate/Assessment issue date":"08.06.2022"},
{"Supplier":"Mediterranean Oils","Country/Region":"Spain", "Product scopes":"9","Certificate/Assessment lock date":"20.04.2025","Lock reason":"Non-paiement of fees.","Address":"Calle Oliva 23","City":"Seville","ZIP":"41001","Certificate/Assessment issue date":"12.12.2022"},
{"Supplier":"Sweet Treats Bakery","Country/Region":"France", "Product scopes":"6, 7","Certificate/Assessment lock date":"15.05.2025","Lock reason":"Major non-conformity on traceability 4.18.1. Pest control issues.","Address":"Rue du Four","City":"Paris","ZIP":"75001","Certificate/Assessment issue date":"30.09.2023"},
{"Supplier":"Alpine Dairy Co","Country/Region":"Switzerland", "Product scopes":"4","Certificate/Assessment lock date":"10.03.2025","Lock reason":"2.3.9.1 KO: HACCP plan incomplete. Temperature control issues.","Address":"Bergstraße 45","City":"Zurich","ZIP":"8001","Certificate/Assessment issue date":"18.07.2022"},
{"Supplier":"Nordic Fish Ltd","Country/Region":"Norway", "Product scopes":"2","Certificate/Assessment lock date":"08.06.2025","Lock reason":"4.18.1 KO: Traceability system failure. Product recall required.","Address":"Havnegata 12","City":"Bergen","ZIP":"5003","Certificate/Assessment issue date":"25.11.2023"}];
}
function loadDemoData() { document.getElementById('loading').style.display='block'; closeDetailsModal(); currentDetailedAudit=null; setTimeout(()=>{processData(generateDemoData()); document.getElementById('loading').style.display='none'},500); }
function handleFileUpload(e){const f=e.target.files[0];if(!f)return;document.getElementById('loading').style.display='block';closeDetailsModal();currentDetailedAudit=null;Papa.parse(f,{header:true,delimiter:';',skipEmptyLines:true,complete:r=>{if(r.errors.length>0){console.error('CSV Parse Errors:',r.errors);alert('Error reading CSV lines.')}processData(r.data);document.getElementById('loading').style.display='none'},error:er=>{console.error('CSV Parse Fail:',er);alert('Error parsing CSV.');document.getElementById('loading').style.display='none'}})}
function applyFilters() {
const selectedCountry = document.getElementById('countryFilter').value;
const selectedReq = document.getElementById('requirementFilter').value;
const selectedType = document.getElementById('typeFilter').value;
const dateFrom = document.getElementById('dateFromFilter').value;
const dateTo = document.getElementById('dateToFilter').value;
const selectedScopes = [];
document.querySelectorAll('#scopesCheckboxContainer input[type="checkbox"]:checked').forEach(cb => selectedScopes.push(cb.value));
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
displayedAuditData = allAuditData.filter(audit => {
// Filtre par pays
if(selectedCountry !== 'all' && audit["Country/Region"] !== selectedCountry) return false;
// Filtre par exigence
const lockReason = audit["Lock reason"] || "";
const requirementsInReason = getRequirementsFromReason(lockReason);
if(selectedReq !== 'all' && !requirementsInReason.includes(selectedReq)) return false;
// Filtre par type
if(selectedType !== 'all' && !getTypesFromReason(lockReason, requirementsInReason).includes(selectedType)) return false;
// Filtre par période de dates
if(dateFrom || dateTo) {
const auditDateStr = audit["Certificate/Assessment lock date"];
if(!auditDateStr) return false;
const auditDate = parseDateString(auditDateStr);
if(!auditDate) return false;
// Vérifier la date de début
if(dateFrom) {
const fromDate = new Date(dateFrom);
if(auditDate < fromDate) return false;
}
// Vérifier la date de fin
if(dateTo) {
const toDate = new Date(dateTo);
// Ajouter 23:59:59 à la date de fin pour inclure toute la journée
toDate.setHours(23, 59, 59, 999);
if(auditDate > toDate) return false;
}
}
// Filtre par secteurs produits
if(selectedScopes.length > 0) {
const auditScopesString = audit["Product scopes"] || "";
const auditScopesArray = auditScopesString.split(',').map(s => s.trim()).filter(s => s);
if(!selectedScopes.some(selectedScope => auditScopesArray.includes(selectedScope))) return false;
}
// Filtre par recherche textuelle
if(searchTerm) {
const reasonLower = lockReason.toLowerCase();
if(!reasonLower.includes(searchTerm)) return false;
}
return true;
});
// Appliquer le tri si une colonne est sélectionnée
if(currentSortColumn) {
const sortType = document.querySelector(`th[data-sort-key="${currentSortColumn}"]`)?.dataset.sortType || 'string';
sortTableByColumn(currentSortColumn, sortType, false);
} else {
updateTable(displayedAuditData);
}
updateStatsCards(displayedAuditData);
updateCharts(displayedAuditData);
}
function sortTableByColumn(k,t='string',tg=true){
if(tg) currentSortDirection = currentSortColumn === k && currentSortDirection === 'asc' ? 'desc' : 'asc';
currentSortColumn = k;
displayedAuditData.sort((a,b) => {
let vA = a[k] || '';
let vB = b[k] || '';
if(t === 'date'){
// Pour les dates, utiliser parseDateString qui gère les différents formats
vA = parseDateString(vA);
vB = parseDateString(vB);
// Gérer les dates nulles
if(!vA && vB) return currentSortDirection === 'asc' ? 1 : -1;
if(vA && !vB) return currentSortDirection === 'asc' ? -1 : 1;
if(!vA && !vB) return 0;
// Comparer les timestamps
const timeA = vA.getTime();
const timeB = vB.getTime();
return currentSortDirection === 'asc' ? timeA - timeB : timeB - timeA;
} else if(t === 'number'){
vA = parseFloat(vA);
vB = parseFloat(vB);
if(isNaN(vA) && !isNaN(vB)) return currentSortDirection === 'asc' ? 1 : -1;
if(!isNaN(vA) && isNaN(vB)) return currentSortDirection === 'asc' ? -1 : 1;
if(isNaN(vA) && isNaN(vB)) return 0;
return currentSortDirection === 'asc' ? vA - vB : vB - vA;
} else {
// Tri alphabétique
const comparison = vA.toString().localeCompare(vB.toString());
return currentSortDirection === 'asc' ? comparison : -comparison;
}
});
updateTable(displayedAuditData);
updateSortIcons();
}
function updateSortIcons(){document.querySelectorAll('#auditsTable th[data-sort-key]').forEach(th=>{const i=th.querySelector('.sort-icon');if(i){if(th.dataset.sortKey===currentSortColumn){th.classList.add('sorted');i.innerHTML=currentSortDirection==='asc'?'<i class="fas fa-arrow-up"></i>':'<i class="fas fa-arrow-down"></i>';}else{th.classList.remove('sorted');i.innerHTML='<i class="fas fa-sort"></i>';}}})}
async function handleLoadFromGoogleSheet(){
// ⚠️ IMPORTANT: Remplacez cette URL par votre propre URL de déploiement Google Apps Script
const appsScriptWebAppUrl = "https://script.google.com/macros/s/AKfycbz7Ed56rzWkiuZTNHpCFxo-ugdf2eekyQd3FU-4ektPQAeBpRlNG4M3pBgI3ajWWYk8/exec";
if(!appsScriptWebAppUrl||appsScriptWebAppUrl.includes("VOTRE_URL")||appsScriptWebAppUrl.includes("YOUR_URL")){
showErrorMessage(uiTexts.errorAppsScriptConfig[currentLanguage]);
console.error("URL Apps Script non configurée:", appsScriptWebAppUrl);
return;
}
document.getElementById('loading').style.display='block';
document.getElementById('loading-text').textContent = uiTexts.autoLoadText[currentLanguage];
closeDetailsModal();currentDetailedAudit=null;
try{
const r=await fetch(appsScriptWebAppUrl);
if(!r.ok){
const eT=await r.text();
throw new Error(`HTTP ${r.status} from Apps Script: ${eT}`);
}
const cT=await r.text();
if(cT.toLowerCase().includes("<html")||cT.toLowerCase().startsWith("error:")){
console.error('Apps Script error/HTML:',cT);
throw new Error(currentLanguage==='fr'?"Apps Script a retourné une erreur. Vérif. logs & déploiement.":"Apps Script error. Check logs & deployment.");
}
Papa.parse(cT,{header:true,delimiter:',',skipEmptyLines:true,complete:res=>{
if(res.errors.length>0){
console.error('GSheet CSV Parse Errors:',res.errors,res.data);
alert(currentLanguage==='fr'?'Erreur lecture CSV (Apps Script).':'Error reading CSV (Apps Script).');
}
if(res.data.length>0&&Object.keys(res.data[0]).length>1){
processData(res.data);
}else{
showErrorMessage(uiTexts.errorLoadingGoogleSheet[currentLanguage]+(currentLanguage==='fr'?" Feuille via Apps Script vide/mal formatée.":" Sheet via Apps Script empty/badly formatted."));
}
document.getElementById('loading').style.display='none';
},error:er=>{
console.error('GSheet PapaParse Fail:',er);
showErrorMessage(uiTexts.errorLoadingGoogleSheet[currentLanguage]);
document.getElementById('loading').style.display='none';
}});
}catch(err){
console.error('GSheet Fetch/Process Fail:',err);
showErrorMessage(uiTexts.gsheetConnectionError[currentLanguage]+`: ${err.message}`);
document.getElementById('loading').style.display='none';
}
}
function showErrorMessage(message) {
const uploadContainer = document.getElementById('upload-section-container');
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.innerHTML = `
<i class="fas fa-exclamation-triangle"></i> ${message}
<br><br>
<button class="btn btn-secondary" onclick="location.reload()">
<i class="fas fa-redo"></i> ${uiTexts.retryText[currentLanguage]}
</button>
`;
uploadContainer.appendChild(errorDiv);
}
function tryAutoLoadFromGoogleSheet() {
console.log("Tentative de chargement automatique depuis Google Apps Script...");
document.getElementById('loading').style.display = 'block';
document.getElementById('loading-text').textContent = uiTexts.autoLoadText[currentLanguage];
// Tentative de chargement automatique
handleLoadFromGoogleSheet().catch(error => {
console.log("Échec du chargement automatique:", error);
document.getElementById('loading').style.display = 'none';
// Afficher un message informatif au lieu d'une erreur
const uploadContainer = document.getElementById('upload-section-container');
const infoDiv = document.createElement('div');
infoDiv.className = 'error-message';
infoDiv.style.background = '#fef3cd';
infoDiv.style.color = '#856404';
infoDiv.style.borderColor = '#ffeaa7';
infoDiv.innerHTML = `
<i class="fas fa-info-circle"></i> ${uiTexts.autoLoadFailed[currentLanguage]}
`;
uploadContainer.appendChild(infoDiv);
// Supprimer le message après 5 secondes
setTimeout(() => {
if (infoDiv.parentNode) {
infoDiv.parentNode.removeChild(infoDiv);
}
}, 5000);
});
}
document.addEventListener('DOMContentLoaded',()=>{
console.log('🚀 IFS Dashboard avec Traduction IA MyMemory initialisé');
initCharts();
switchLanguage(currentLanguage);
document.getElementById('searchInput').addEventListener('input',()=>{applyFilters();});
['countryFilter','requirementFilter','typeFilter','dateFromFilter','dateToFilter'].forEach(id=>document.getElementById(id).addEventListener('change',applyFilters));
document.getElementById('lang-fr').addEventListener('click',()=>switchLanguage('fr'));
document.getElementById('lang-en').addEventListener('click',()=>switchLanguage('en'));
document.querySelectorAll('#auditsTable th[data-sort-key]').forEach(th=>{
th.addEventListener('click',()=>sortTableByColumn(th.dataset.sortKey, (th.dataset.sortType || 'string')));
const sortIcon = th.querySelector('.sort-icon');
if(sortIcon) sortIcon.innerHTML='<i class="fas fa-sort"></i>';
});
document.getElementById('export-btn').addEventListener('click',()=>{
const dE=displayedAuditData.length>0?displayedAuditData:allAuditData;
if(dE.length===0){alert(currentLanguage==='fr'?"Pas de données à exporter.":"No data to export.");return}
const h=[uiTexts.supplierHeader[currentLanguage],uiTexts.countryHeader[currentLanguage],uiTexts.dateHeader[currentLanguage],uiTexts.requirementsHeader[currentLanguage],uiTexts.typesHeader[currentLanguage],uiTexts.reasonLabel[currentLanguage].replace(':','')];
const cR=[h.join(';')];
dE.forEach(a=>{const rS=getRequirementsFromReason(a["Lock reason"]||"").join(', ');
const tS=getTypesFromReason(a["Lock reason"]||"").map(t=>{switch(t){case'KO':return uiTexts.koCount[currentLanguage];case'Major':return uiTexts.majorCount[currentLanguage];case'Non-paiement':return uiTexts.nonPaymentOption[currentLanguage];case'Arrêt de certification':return uiTexts.terminationOption[currentLanguage];default:return t;}}).join(', ');
cR.push([a.Supplier||'',a["Country/Region"]||'',a["Certificate/Assessment lock date"]||'',rS,tS,(a["Lock reason"]||"").replace(/\r?\n|\r/g," ").replace(/"/g,'""')].map(f=>`"${f}"`).join(';'));});
const l=document.createElement('a');l.href=encodeURI("data:text/csv;charset=utf-8,\uFEFF"+cR.join('\r\n'));l.download=`analyse_ifs_${new Date().toISOString().slice(0,10)}.csv`;document.body.appendChild(l);l.click();document.body.removeChild(l);
});
const obs=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting){e.target.classList.add('fade-in');obs.unobserve(e.target);}})},{threshold:0.1,rootMargin:'0px 0px -50px 0px'});
document.querySelectorAll('.stat-card,.chart-card,.table-card,.filters,.upload-section').forEach(el=>obs.observe(el));
document.getElementById('modalCloseBtn').addEventListener('click',closeDetailsModal);
document.getElementById('modalFooterCloseBtn').addEventListener('click',closeDetailsModal);
document.getElementById('detailsModalOverlay').addEventListener('click',function(e){if(e.target===this)closeDetailsModal();});
updateUploadSectionUI(true, 0);
// 🎯 CHARGEMENT AUTOMATIQUE AU DÉMARRAGE
setTimeout(()=>{
if(allAuditData.length===0) {
tryAutoLoadFromGoogleSheet();
}
}, 1000); // Délai de 1 seconde pour laisser l'interface se charger
});
</script>
</body>
</html>