// Vaccine Catalog Interactive Functionality - FIXED VERSION // Addresses data loading issues and button selector problems class VaccineCatalog { constructor() { this.data = []; this.filteredData = []; this.searchQuery = ''; this.statusFilter = ''; this.sortColumn = ''; this.sortDirection = 'asc'; this.apiEndpoint = null; this.isInitialized = false; // Wait for DOM to be ready before initializing if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.init()); } else { this.init(); } } init() { console.log('๐ VaccineCatalog initializing...'); // Verify DOM elements exist before binding if (!this.verifyDOMElements()) { console.error('โ Required DOM elements not found, retrying in 500ms...'); setTimeout(() => this.init(), 500); return; } this.bindEvents(); this.loadData(); this.showLoadingState(); this.isInitialized = true; console.log('โ VaccineCatalog initialized successfully'); } verifyDOMElements() { const requiredElements = [ 'searchInput', 'statusFilter', 'clearFilters', 'vaccineTable', 'vaccineTableBody', 'resultsCounter', 'loadingState', 'emptyState' ]; const missing = requiredElements.filter(id => !document.getElementById(id)); if (missing.length > 0) { console.warn('Missing DOM elements:', missing); return false; } return true; } setApiEndpoint(url) { this.apiEndpoint = url; console.log('๐ก API endpoint set:', url); } bindEvents() { console.log('๐ Binding events...'); const searchInput = document.getElementById('searchInput'); const statusFilter = document.getElementById('statusFilter'); const clearFilters = document.getElementById('clearFilters'); const sortHeaders = document.querySelectorAll('[data-sort]'); if (searchInput) { searchInput.addEventListener('input', this.debounce((e) => { this.searchQuery = e.target.value.trim().toLowerCase(); this.filterData(); }, 300)); console.log('โ Search input event bound'); } if (statusFilter) { statusFilter.addEventListener('change', (e) => { this.statusFilter = e.target.value; this.filterData(); }); console.log('โ Status filter event bound'); } if (clearFilters) { clearFilters.addEventListener('click', () => { this.clearFilters(); }); console.log('โ Clear filters event bound'); } // Bind sorting events sortHeaders.forEach(header => { header.addEventListener('click', (e) => { const column = e.currentTarget.getAttribute('data-sort'); this.sortData(column); }); }); console.log(`โ ${sortHeaders.length} sort headers bound`); // Better button selectors using more specific approach this.bindActionButtons(); } bindActionButtons() { // Find buttons by their text content and context (more reliable) const buttons = document.querySelectorAll('button'); buttons.forEach(button => { const buttonText = button.textContent.trim(); if (buttonText.includes('Export to CSV')) { button.addEventListener('click', () => this.exportToCSV()); console.log('โ Export button event bound'); } else if (buttonText.includes('Refresh Data')) { button.addEventListener('click', () => this.loadData()); console.log('โ Refresh button event bound'); } else if (buttonText.includes('Add New Vaccine')) { button.addEventListener('click', () => this.showAddVaccineModal()); console.log('โ Add vaccine button event bound'); } }); } showAddVaccineModal() { alert('Add New Vaccine functionality would open a modal here.\n\nThis would connect to your database to add new entries.'); } debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } async loadData() { console.log('๐ Loading vaccine data...'); this.showLoadingState(); try { let data; if (this.apiEndpoint) { console.log(`๐ก Fetching from API: ${this.apiEndpoint}`); const response = await fetch(this.apiEndpoint, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' } }); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } data = await response.json(); if (!Array.isArray(data)) { throw new Error('API response is not an array'); } console.log(`โ Loaded ${data.length} vaccines from API`); this.showApiStatus(true, `API data loaded: ${data.length} vaccines`); } else { console.log('๐ Using local sample data (no API configured)'); data = this.getSampleData(); this.showApiStatus(false, `Local sample data: ${data.length} vaccines`); } this.data = data; this.filteredData = [...this.data]; this.hideLoadingState(); this.renderTable(); this.updateResultsCounter(); console.log(`โ Data loading complete: ${this.data.length} vaccines loaded`); } catch (error) { console.error('โ Error loading vaccine data:', error); this.showApiError(error.message); // Fallback to local sample data console.log('๐ Falling back to sample data...'); this.data = this.getSampleData(); this.filteredData = [...this.data]; this.hideLoadingState(); this.renderTable(); this.updateResultsCounter(); this.showApiStatus(false, `API failed - using fallback data (${this.data.length} vaccines)`); } } showApiStatus(isSuccess, message) { const counter = document.getElementById('resultsCounter'); if (counter) { const statusIcon = isSuccess ? '' : ''; setTimeout(() => { counter.innerHTML = `${statusIcon}${message}`; console.log('๐ Status updated:', message); }, 100); } } showApiError(errorMessage) { const resultsCounter = document.getElementById('resultsCounter'); if (resultsCounter) { resultsCounter.innerHTML = ` Connection failed: ${errorMessage} `; console.error('๐จ API Error displayed:', errorMessage); } } getSampleData() { // Enhanced sample data with more realistic examples return [ { vaccine_uid: 'VAC-COVID-001', vaccine_common_name: 'COVID-19 mRNA', disease_target: 'SARS-CoV-2', product_name_brand: 'Comirnaty', platform: 'mRNA', manufacturer_primary: 'Pfizer-BioNTech', first_public_use_date: '2020-12-11', earliest_trial_start_date: '2020-04-29', status_current_use: 'in_use', reg_authority_first: 'FDA', who_prequalified: true, fda_bla_number: '125742', ema_procedure_number: 'EMEA/H/C/005735' }, { vaccine_uid: 'VAC-COVID-002', vaccine_common_name: 'COVID-19 mRNA', disease_target: 'SARS-CoV-2', product_name_brand: 'Spikevax', platform: 'mRNA', manufacturer_primary: 'Moderna', first_public_use_date: '2020-12-18', earliest_trial_start_date: '2020-07-14', status_current_use: 'in_use', reg_authority_first: 'FDA', who_prequalified: true, fda_bla_number: '125663', ema_procedure_number: 'EMEA/H/C/005791' }, { vaccine_uid: 'VAC-COVID-003', vaccine_common_name: 'COVID-19 Viral Vector', disease_target: 'SARS-CoV-2', product_name_brand: 'Vaxzevria', platform: 'viral_vector', manufacturer_primary: 'AstraZeneca', first_public_use_date: '2021-01-04', earliest_trial_start_date: '2020-04-23', status_current_use: 'withdrawn', reg_authority_first: 'EMA', who_prequalified: true, fda_bla_number: null, ema_procedure_number: 'EMEA/H/C/005675' }, { vaccine_uid: 'VAC-FLU-001', vaccine_common_name: 'Seasonal Influenza', disease_target: 'Influenza A/B', product_name_brand: 'Fluzone Quadrivalent', platform: 'inactivated', manufacturer_primary: 'Sanofi Pasteur', first_public_use_date: '1945-01-01', earliest_trial_start_date: '1943-01-01', status_current_use: 'in_use', reg_authority_first: 'FDA', who_prequalified: true, fda_bla_number: '103851', ema_procedure_number: null }, { vaccine_uid: 'VAC-MMR-001', vaccine_common_name: 'Measles, Mumps, Rubella', disease_target: 'Measles/Mumps/Rubella', product_name_brand: 'M-M-R II', platform: 'live_attenuated', manufacturer_primary: 'Merck & Co.', first_public_use_date: '1971-04-01', earliest_trial_start_date: '1969-01-01', status_current_use: 'in_use', reg_authority_first: 'FDA', who_prequalified: true, fda_bla_number: '103516', ema_procedure_number: null }, { vaccine_uid: 'VAC-HPV-001', vaccine_common_name: 'Human Papillomavirus', disease_target: 'HPV Types 16,18,6,11,31,33,45,52,58', product_name_brand: 'Gardasil 9', platform: 'protein_subunit', manufacturer_primary: 'Merck & Co.', first_public_use_date: '2014-12-10', earliest_trial_start_date: '2007-09-01', status_current_use: 'in_use', reg_authority_first: 'FDA', who_prequalified: true, fda_bla_number: '125508', ema_procedure_number: 'EMEA/H/C/003852' }, { vaccine_uid: 'VAC-RSV-001', vaccine_common_name: 'Respiratory Syncytial Virus', disease_target: 'RSV', product_name_brand: 'Arexvy', platform: 'protein_subunit', manufacturer_primary: 'GSK', first_public_use_date: '2023-05-03', earliest_trial_start_date: '2017-03-01', status_current_use: 'in_use', reg_authority_first: 'FDA', who_prequalified: false, fda_bla_number: '125769', ema_procedure_number: 'EMEA/H/C/005993' }, { vaccine_uid: 'VAC-SHINGLES-001', vaccine_common_name: 'Zoster (Shingles)', disease_target: 'Varicella-Zoster Virus', product_name_brand: 'Shingrix', platform: 'protein_subunit', manufacturer_primary: 'GSK', first_public_use_date: '2017-10-20', earliest_trial_start_date: '2010-03-15', status_current_use: 'in_use', reg_authority_first: 'FDA', who_prequalified: true, fda_bla_number: '125259', ema_procedure_number: 'EMEA/H/C/004336' }, { vaccine_uid: 'VAC-EXP-001', vaccine_common_name: 'Experimental Cancer Vaccine', disease_target: 'Multiple Cancer Types', product_name_brand: 'mRNA-4157', platform: 'mRNA', manufacturer_primary: 'Moderna', first_public_use_date: null, earliest_trial_start_date: '2019-08-01', status_current_use: 'investigational', reg_authority_first: null, who_prequalified: false, fda_bla_number: null, ema_procedure_number: null }, { vaccine_uid: 'VAC-OLD-001', vaccine_common_name: 'Oral Polio Vaccine', disease_target: 'Poliovirus', product_name_brand: 'OPV', platform: 'live_attenuated', manufacturer_primary: 'Various', first_public_use_date: '1961-01-01', earliest_trial_start_date: '1957-01-01', status_current_use: 'withdrawn', reg_authority_first: 'FDA', who_prequalified: false, fda_bla_number: null, ema_procedure_number: null }, { vaccine_uid: 'VAC-DISC-001', vaccine_common_name: 'Anthrax Vaccine Adsorbed', disease_target: 'Bacillus anthracis', product_name_brand: 'BioThrax', platform: 'protein_subunit', manufacturer_primary: 'Emergent BioSolutions', first_public_use_date: '1970-01-01', earliest_trial_start_date: '1954-01-01', status_current_use: 'discontinued', reg_authority_first: 'FDA', who_prequalified: false, fda_bla_number: '103432', ema_procedure_number: null } ]; } filterData() { const loadingIndicator = document.getElementById('loadingIndicator'); if (loadingIndicator) { loadingIndicator.classList.remove('hidden'); } // Use shorter timeout for better responsiveness setTimeout(() => { this.filteredData = this.data.filter(vaccine => { const searchText = [ vaccine.vaccine_common_name, vaccine.disease_target, vaccine.product_name_brand, vaccine.platform, vaccine.manufacturer_primary, vaccine.reg_authority_first, vaccine.fda_bla_number, vaccine.ema_procedure_number ].filter(Boolean).join(' ').toLowerCase(); const matchesSearch = !this.searchQuery || searchText.includes(this.searchQuery); const matchesStatus = !this.statusFilter || vaccine.status_current_use === this.statusFilter; return matchesSearch && matchesStatus; }); this.renderTable(); this.updateResultsCounter(); if (loadingIndicator) { loadingIndicator.classList.add('hidden'); } console.log(`๐ Filtered results: ${this.filteredData.length} of ${this.data.length}`); }, 100); } sortData(column) { if (this.sortColumn === column) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.sortColumn = column; this.sortDirection = 'asc'; } this.filteredData.sort((a, b) => { let aVal, bVal; switch (column) { case 'name': aVal = a.vaccine_common_name || ''; bVal = b.vaccine_common_name || ''; break; case 'disease': aVal = a.disease_target || ''; bVal = b.disease_target || ''; break; case 'platform': aVal = a.platform || ''; bVal = b.platform || ''; break; case 'manufacturer': aVal = a.manufacturer_primary || ''; bVal = b.manufacturer_primary || ''; break; case 'status': aVal = a.status_current_use || ''; bVal = b.status_current_use || ''; break; case 'first_use': aVal = a.first_public_use_date || ''; bVal = b.first_public_use_date || ''; break; case 'trial_start': aVal = a.earliest_trial_start_date || ''; bVal = b.earliest_trial_start_date || ''; break; default: return 0; } const comparison = aVal.localeCompare(bVal); return this.sortDirection === 'asc' ? comparison : -comparison; }); this.updateSortIcons(column); this.renderTable(); console.log(`๐ Sorted by ${column} (${this.sortDirection})`); } updateSortIcons(activeColumn) { const headers = document.querySelectorAll('[data-sort]'); headers.forEach(header => { const icon = header.querySelector('i'); const column = header.getAttribute('data-sort'); if (column === activeColumn) { icon.className = `fas fa-sort-${this.sortDirection === 'asc' ? 'up' : 'down'} ml-1 text-xs`; } else { icon.className = 'fas fa-sort ml-1 text-xs'; } }); } renderTable() { const tbody = document.getElementById('vaccineTableBody'); const emptyState = document.getElementById('emptyState'); const tableContainer = document.querySelector('#vaccineTable').parentElement; if (!tbody) { console.error('โ Table body element not found'); return; } if (this.filteredData.length === 0) { tbody.innerHTML = ''; if (emptyState) emptyState.classList.remove('hidden'); if (tableContainer) tableContainer.style.display = 'none'; console.log('๐ Empty state displayed'); return; } if (emptyState) emptyState.classList.add('hidden'); if (tableContainer) tableContainer.style.display = 'block'; tbody.innerHTML = this.filteredData.map((vaccine, index) => { const rowClass = index % 2 === 0 ? 'bg-white' : 'bg-gray-50'; const displayName = vaccine.vaccine_common_name + (vaccine.product_name_brand ? ` โ ${vaccine.product_name_brand}` : ''); return `