document.addEventListener('DOMContentLoaded', () => { const ifsScrollable = document.getElementById('ifsList'); const brcScrollable = document.getElementById('brcList'); const searchInput = document.getElementById('searchInput'); let appData = null; let currentLang = 'fr'; let comparisonMode = false; let selectedIfsIds = new Set(); // ─── Translations ──────────────────────────────────────────────────────────── const translations = { fr: { mainTitle: 'Correspondance IFS v8 ↔ BRC v9', subTitle: "Outil d'analyse comparative des référentiels de sécurité des aliments", searchPh: 'Rechercher...', guideBtn: '? Guide', guideBtnClose: '✕ Guide', help1Title: 'Cliquez sur une exigence', help1Text: 'pour voir sa correspondance', help2Title: 'Correspondance approximative', help2Text: 'pas de correspondance directe', help3Title: 'Pas de correspondance', help3Text: 'exigence non cartographiée', help4Text: 'Exigence éliminatoire', help5Title: 'Mode comparaison', help5Text: 'multi-sélection IFS', ifsTitle: 'IFS Food v8', brcTitle: 'BRCGS Food v9', expandTip: 'Tout déplier', collapseTip:'Tout réduire', compareModeOn: 'Mode comparaison multi-sélection', compareModeOff: 'Quitter le mode comparaison', selected: 'sélectionné(s)', clearSel: 'Effacer la sélection', noMatchTitle: 'Aucune correspondance dans la norme liée', approxTitle: 'Correspondance approximative – pas de correspondance directe', }, en: { mainTitle: 'IFS v8 ↔ BRC v9 Mapping', subTitle: 'Comparative analysis tool for food safety standards', searchPh: 'Search...', guideBtn: '? Guide', guideBtnClose: '✕ Guide', help1Title: 'Click on a requirement', help1Text: 'to see its correspondence', help2Title: 'Approximate correspondence', help2Text: 'no direct correspondence', help3Title: 'No correspondence', help3Text: 'unmapped requirement', help4Text: 'Knock-out requirement', help5Title: 'Comparison mode', help5Text: 'multi-select IFS items', ifsTitle: 'IFS Food v8', brcTitle: 'BRCGS Food v9', expandTip: 'Expand all', collapseTip:'Collapse all', compareModeOn: 'Multi-select comparison mode', compareModeOff: 'Exit comparison mode', selected: 'selected', clearSel: 'Clear selection', noMatchTitle: 'No correspondence in the linked standard', approxTitle: 'Approximate correspondence – no direct match in the linked standard', } }; // ─── Helper: safely set text / title on an element ─────────────────────────── function setText(id, value) { const el = document.getElementById(id); if (el) el.textContent = value; } function setTitle(id, value) { const el = document.getElementById(id); if (el) el.title = value; } function setPlaceholder(id, value) { const el = document.getElementById(id); if (el) el.placeholder = value; } function qs(sel) { return document.querySelector(sel); } // ─── Language toggle ───────────────────────────────────────────────────────── document.getElementById('langToggle')?.addEventListener('click', () => { currentLang = currentLang === 'fr' ? 'en' : 'fr'; updateLanguage(); }); function updateLanguage() { const t = translations[currentLang]; // Header setText('mainTitle', t.mainTitle); setText('subTitle', t.subTitle); setPlaceholder('searchInput', t.searchPh); // Guide panel content setText('help1Title', t.help1Title); setText('help1Text', t.help1Text); setText('help2Title', t.help2Title); setText('help2Text', t.help2Text); setText('help3Title', t.help3Title); setText('help3Text', t.help3Text); setText('help4Text', t.help4Text); setText('help5Title', t.help5Title); setText('help5Text', t.help5Text); // Guide button label (changes based on panel state) const isOpen = qs('#guidePanel')?.classList.contains('open'); setText('guideBtnLabel', isOpen ? t.guideBtnClose : t.guideBtn); // Column headers const ifsH2 = qs('.ifs-column .column-header h2'); const brcH2 = qs('.brc-column .column-header h2'); if (ifsH2) ifsH2.textContent = t.ifsTitle; if (brcH2) brcH2.textContent = t.brcTitle; // Buttons setTitle('expandIfs', t.expandTip); setTitle('collapseIfs', t.collapseTip); setTitle('expandBrc', t.expandTip); setTitle('collapseBrc', t.collapseTip); setTitle('compareMode', comparisonMode ? t.compareModeOff : t.compareModeOn); setText('clearCompare', '✕ ' + t.clearSel); // Visual indicator on FR|EN button const frEl = qs('.lang-fr'); const enEl = qs('.lang-en'); if (frEl) frEl.style.opacity = currentLang === 'fr' ? '1' : '0.4'; if (enEl) enEl.style.opacity = currentLang === 'en' ? '1' : '0.4'; updateComparisonUI(); } // ─── Guide panel ───────────────────────────────────────────────────────────── const guideToggleBtn = document.getElementById('guideToggle'); const guidePanel = document.getElementById('guidePanel'); guideToggleBtn?.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = guidePanel.classList.toggle('open'); guidePanel.setAttribute('aria-hidden', String(!isOpen)); const t = translations[currentLang]; setText('guideBtnLabel', isOpen ? t.guideBtnClose : t.guideBtn); guideToggleBtn.classList.toggle('active-mode', isOpen); }); // Close guide panel when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.guide-wrapper')) { guidePanel?.classList.remove('open'); guidePanel?.setAttribute('aria-hidden', 'true'); setText('guideBtnLabel', translations[currentLang].guideBtn); guideToggleBtn?.classList.remove('active-mode'); } }); // ─── Data loading ──────────────────────────────────────────────────────────── if (window.APP_DATA) { appData = window.APP_DATA; renderLists(appData); } else { console.error('APP_DATA not found. Ensure data.js is loaded.'); } // ─── Render ────────────────────────────────────────────────────────────────── function renderLists(data) { renderGroupedList(ifsScrollable, groupData(data.ifs_standards, 'chapter', 'section'), 'ifs'); renderGroupedList(brcScrollable, groupData(data.brc_standards, 'chapter', 'subsection'), 'brc'); } function groupData(items, key1, key2) { const groups = {}; items.forEach(item => { const k1 = (item[key1] && item[key1] !== 'undefined') ? item[key1] : 'Général'; const k2 = (item[key2] && item[key2] !== 'undefined') ? item[key2] : 'Section Générale'; if (!groups[k1]) groups[k1] = {}; if (!groups[k1][k2]) groups[k1][k2] = []; groups[k1][k2].push(item); }); return groups; } function renderGroupedList(container, groups, type) { container.innerHTML = ''; // Build chapter-number map const chNumMap = {}; [...(appData?.ifs_standards || []), ...(appData?.brc_standards || [])].forEach(item => { if (item.chapter && item.chapter_num) chNumMap[item.chapter] = item.chapter_num; }); Object.keys(groups).forEach(l1Key => { const detailsL1 = document.createElement('details'); detailsL1.className = 'acc-l1'; const summaryL1 = document.createElement('summary'); summaryL1.className = 'acc-summary-l1'; let title = l1Key; if (chNumMap[l1Key]) { title = `${type === 'ifs' ? 'Chapitre' : 'Section'} ${chNumMap[l1Key]}: ${l1Key}`; } summaryL1.innerHTML = `${title}`; detailsL1.appendChild(summaryL1); const contentL1 = document.createElement('div'); contentL1.className = 'acc-content-l1'; Object.keys(groups[l1Key]).forEach(l2Key => { const detailsL2 = document.createElement('details'); detailsL2.className = 'acc-l2'; const summaryL2 = document.createElement('summary'); summaryL2.className = 'acc-summary-l2'; summaryL2.innerHTML = `${l2Key}`; detailsL2.appendChild(summaryL2); const contentL2 = document.createElement('div'); contentL2.className = 'acc-content-l2'; groups[l1Key][l2Key].forEach(item => contentL2.appendChild(createCard(item, type))); detailsL2.appendChild(contentL2); contentL1.appendChild(detailsL2); }); detailsL1.appendChild(contentL1); container.appendChild(detailsL1); }); } function createCard(item, type) { const div = document.createElement('div'); div.className = `item-card ${type}-card`; div.dataset.id = cleanId(item.id); div.dataset.type = type; const isKO = item.ko === true; const koBadge = isKO ? 'KO' : ''; div.innerHTML = `
${item.id} ${koBadge}
${truncateText(item.text, 300)}
`; // Pre-build search index so innerText (which ignores collapsed
) is never used div.dataset.search = (item.id + ' ' + (item.text || '')).toLowerCase(); div.addEventListener('click', () => handleCardClick(div, item.id, type)); return div; } // ─── Click handling ────────────────────────────────────────────────────────── function handleCardClick(element, id, type) { if (comparisonMode && type === 'ifs') { handleComparisonClick(element, id); return; } const cleanID = cleanId(id); // Reset document.querySelectorAll('.item-card.active').forEach(el => el.classList.remove('active')); document.querySelectorAll('.approximate-indicator, .no-match-indicator').forEach(el => el.remove()); document.querySelectorAll('.approximate-active, .no-match-active').forEach(el => { el.classList.remove('approximate-active', 'no-match-active'); }); element.classList.add('active'); openParents(element); scrollToInContainer(element, type === 'ifs' ? ifsScrollable : brcScrollable); // Resolve mapping let targetIds = []; let isApproximate = false; let hasNoMatch = false; if (type === 'ifs') { const mapping = appData.mappings.find(m => cleanId(m.source_id) === cleanID); if (mapping && mapping.target_ids && mapping.target_ids.length > 0) { targetIds = mapping.target_ids; isApproximate = mapping.approximate === true; } else if (mapping && mapping.approximate) { isApproximate = true; } else { hasNoMatch = true; } } else { const related = appData.mappings.filter(m => m.target_ids && m.target_ids.some(tid => cleanId(tid) === cleanID) ); targetIds = related.map(m => m.source_id); hasNoMatch = related.length === 0; isApproximate = related.some(m => m.approximate === true); } const targetContainer = type === 'ifs' ? brcScrollable : ifsScrollable; highlightTargets(targetIds, type === 'ifs' ? 'brc' : 'ifs', isApproximate, hasNoMatch, targetContainer); // Mobile: auto-switch column if (window.innerWidth <= 768 && targetIds.length > 0) { switchMobileTab(type === 'ifs' ? 'brc' : 'ifs'); } } function highlightTargets(ids, targetType, isApproximate, hasNoMatch, container) { if (!ids || ids.length === 0) { const activeEl = qs('.item-card.active'); if (activeEl) { if (hasNoMatch) addNoMatchIndicator(activeEl); else if (isApproximate) addApproximateIndicator(activeEl); } return; } let firstFound = null; const foundTargets = []; ids.forEach(id => { const el = qs(`.${targetType}-card[data-id="${cleanId(id)}"]`); if (el) { el.classList.add('active'); openParents(el); foundTargets.push(el); if (!firstFound) firstFound = el; } }); if (isApproximate) { // Mark the source card (already .active) const sourceEl = qs('.item-card.active:not(.' + targetType + '-card)'); if (sourceEl) addApproximateIndicator(sourceEl); // Mark each target card with the same ⚠ indicator foundTargets.forEach(el => addApproximateIndicator(el)); } if (firstFound && container) { setTimeout(() => scrollToInContainer(firstFound, container), 120); } } // ─── Scroll sync ───────────────────────────────────────────────────────────── function scrollToInContainer(element, container) { const cRect = container.getBoundingClientRect(); const eRect = element.getBoundingClientRect(); const target = container.scrollTop + (eRect.top - cRect.top) - (cRect.height / 2) + (eRect.height / 2); container.scrollTo({ top: Math.max(0, target), behavior: 'smooth' }); } // ─── Comparison mode ───────────────────────────────────────────────────────── document.getElementById('compareMode')?.addEventListener('click', () => { comparisonMode = !comparisonMode; if (!comparisonMode) clearComparison(); updateComparisonUI(); }); document.getElementById('clearCompare')?.addEventListener('click', clearComparison); function clearComparison() { selectedIfsIds.clear(); document.querySelectorAll('.ifs-card.selected').forEach(el => el.classList.remove('selected')); document.querySelectorAll('.item-card.active').forEach(el => el.classList.remove('active')); document.querySelectorAll('.approximate-indicator, .no-match-indicator').forEach(el => el.remove()); document.querySelectorAll('.approximate-active, .no-match-active').forEach(el => { el.classList.remove('approximate-active', 'no-match-active'); }); updateComparisonUI(); } function updateComparisonUI() { const t = translations[currentLang]; const btn = document.getElementById('compareMode'); const bar = document.getElementById('compareBar'); const cnt = document.getElementById('compareCount'); if (btn) { btn.classList.toggle('active-mode', comparisonMode); btn.title = comparisonMode ? t.compareModeOff : t.compareModeOn; } if (bar) bar.classList.toggle('visible', comparisonMode); if (cnt) cnt.textContent = selectedIfsIds.size + ' ' + t.selected; } function handleComparisonClick(element, id) { const cleanID = cleanId(id); if (selectedIfsIds.has(cleanID)) { selectedIfsIds.delete(cleanID); element.classList.remove('selected'); } else { selectedIfsIds.add(cleanID); element.classList.add('selected'); } updateComparisonUI(); recomputeComparisonHighlights(); } function recomputeComparisonHighlights() { document.querySelectorAll('.brc-card.active').forEach(el => el.classList.remove('active')); document.querySelectorAll('.approximate-indicator, .no-match-indicator').forEach(el => el.remove()); if (selectedIfsIds.size === 0) return; const union = new Set(); selectedIfsIds.forEach(ifsId => { const m = appData.mappings.find(m => cleanId(m.source_id) === ifsId); if (m && m.target_ids) m.target_ids.forEach(tid => union.add(cleanId(tid))); }); let first = null; union.forEach(brcId => { const el = qs(`.brc-card[data-id="${brcId}"]`); if (el) { el.classList.add('active'); openParents(el); if (!first) first = el; } }); if (first) setTimeout(() => scrollToInContainer(first, brcScrollable), 120); } // ─── Mobile tabs ────────────────────────────────────────────────────────────── document.getElementById('tabIfs')?.addEventListener('click', () => switchMobileTab('ifs')); document.getElementById('tabBrc')?.addEventListener('click', () => switchMobileTab('brc')); function switchMobileTab(tab) { const ifsCol = document.getElementById('ifsColumn'); const brcCol = document.getElementById('brcColumn'); const tabIfs = document.getElementById('tabIfs'); const tabBrc = document.getElementById('tabBrc'); ifsCol?.classList.toggle('mobile-hidden', tab !== 'ifs'); brcCol?.classList.toggle('mobile-hidden', tab !== 'brc'); tabIfs?.classList.toggle('active', tab === 'ifs'); tabBrc?.classList.toggle('active', tab === 'brc'); } // ─── Search ────────────────────────────────────────────────────────────────── searchInput.addEventListener('input', (e) => { const term = e.target.value.toLowerCase().trim(); // 1. Show/hide individual cards using the pre-built index (works on closed accordions too) document.querySelectorAll('.item-card').forEach(card => { const match = !term || card.dataset.search.includes(term); card.style.display = match ? '' : 'none'; if (match && term.length > 1) openParents(card); }); // 2. Hide L2 accordions whose every card is hidden document.querySelectorAll('details.acc-l2').forEach(l2 => { const hasVisible = [...l2.querySelectorAll('.item-card')] .some(c => c.style.display !== 'none'); l2.style.display = hasVisible ? '' : 'none'; }); // 3. Hide L1 accordions whose every L2 is hidden document.querySelectorAll('details.acc-l1').forEach(l1 => { const hasVisible = [...l1.querySelectorAll('details.acc-l2')] .some(l2 => l2.style.display !== 'none'); l1.style.display = hasVisible ? '' : 'none'; }); }); // ─── Expand / Collapse ─────────────────────────────────────────────────────── document.getElementById('expandIfs')?.addEventListener('click', () => toggleAll('ifsList', true)); document.getElementById('collapseIfs')?.addEventListener('click', () => toggleAll('ifsList', false)); document.getElementById('expandBrc')?.addEventListener('click', () => toggleAll('brcList', true)); document.getElementById('collapseBrc')?.addEventListener('click', () => toggleAll('brcList', false)); function toggleAll(containerId, expand) { document.getElementById(containerId)?.querySelectorAll('details').forEach(d => { d.open = expand; }); } // ─── Utilities ─────────────────────────────────────────────────────────────── function cleanId(id) { return id ? id.replace(/\*$/, '').trim() : ''; } function openParents(el) { let p = el.parentElement; while (p) { if (p.tagName === 'DETAILS') p.open = true; p = p.parentElement; } } function truncateText(text, len) { if (!text) return ''; return text.length <= len ? text : text.substring(0, len) + '...'; } function addNoMatchIndicator(el) { if (el.querySelector('.no-match-indicator')) return; const span = document.createElement('span'); span.className = 'no-match-indicator'; span.innerHTML = '⛔'; span.title = translations[currentLang].noMatchTitle; const ref = el.querySelector('.ref-id'); ref ? ref.parentNode.insertBefore(span, ref.nextSibling) : el.appendChild(span); el.classList.add('no-match-active'); } function addApproximateIndicator(el) { if (el.querySelector('.approximate-indicator')) return; const span = document.createElement('span'); span.className = 'approximate-indicator'; span.innerHTML = '⚠'; span.title = translations[currentLang].approxTitle; const ref = el.querySelector('.ref-id'); ref ? ref.parentNode.insertBefore(span, ref.nextSibling) : el.appendChild(span); el.classList.add('approximate-active'); } });