document.addEventListener('DOMContentLoaded', function() { console.log('Modules page initializing...'); // Reference to container and filters const modulesContainer = document.getElementById('modules-container'); const moduleSearch = document.getElementById('module-search'); const stageFilter = document.getElementById('stage-filter'); const componentFilter = document.getElementById('component-filter'); const templateFilter = document.getElementById('template-filter'); // Data structures let allModules = []; let stageNames = {}; let componentNames = {}; let templateUsage = {}; let processedTemplates = []; // Functions function initializeData() { console.log('Initializing data...'); // Check if data is available if (typeof moduleData === 'undefined') { console.error('Error: moduleData is undefined'); modulesContainer.innerHTML = '
Error: Module data not available
'; return false; } if (typeof rawProtocolTemplates === 'undefined' || typeof templateMapper === 'undefined') { console.error('Error: Template data or mapper not available'); modulesContainer.innerHTML = '
Error: Template data not available
'; return false; } // Log what we're working with console.log('Module categories:', Object.keys(moduleData)); console.log('Templates count:', rawProtocolTemplates.length); try { // Process templates rawProtocolTemplates.forEach(template => { const processedTemplate = templateMapper.convertTemplateToModules(template, moduleData); processedTemplates.push(processedTemplate); }); // Collect all modules into a flat array for (const category in moduleData) { moduleData[category].forEach(module => { // Add category to module module.category = category; // Initialize template usage module.usedInTemplates = []; // Ensure all modules have a componentId - no uncategorized modules allowed if (!module.componentId) { // Try to infer from category first if (category !== 'uncategorized') { console.log(`Module ${module.id} missing componentId, using category: ${category}`); module.componentId = category; } // If that doesn't work, infer from title or content else { // First, try to find clues in the title if (module.title) { const titleLower = module.title.toLowerCase(); // Check for key component names in the title if (titleLower.includes('principle')) module.componentId = 'principles'; else if (titleLower.includes('process')) module.componentId = 'process'; else if (titleLower.includes('assessment')) module.componentId = 'assessment'; else if (titleLower.includes('intake')) module.componentId = 'intake'; else if (titleLower.includes('appeal')) module.componentId = 'appeal'; else if (titleLower.includes('deliberat')) module.componentId = 'deliberation'; else if (titleLower.includes('resolut')) module.componentId = 'resolution'; else if (titleLower.includes('facilit')) module.componentId = 'facilitation'; else if (titleLower.includes('particip')) module.componentId = 'participants'; else if (titleLower.includes('file') || titleLower.includes('submit')) module.componentId = 'filing'; else if (titleLower.includes('notif') || titleLower.includes('inform')) module.componentId = 'notification'; else if (titleLower.includes('delegat')) module.componentId = 'delegation'; else module.componentId = 'process'; // Default to process } // If no title clues, check content else if (module.content) { const contentLower = module.content.toLowerCase(); // Same checks in content if (contentLower.includes('principle')) module.componentId = 'principles'; else if (contentLower.includes('process')) module.componentId = 'process'; else if (contentLower.includes('assessment')) module.componentId = 'assessment'; else if (contentLower.includes('intake')) module.componentId = 'intake'; else if (contentLower.includes('appeal')) module.componentId = 'appeal'; else if (contentLower.includes('deliberat')) module.componentId = 'deliberation'; else if (contentLower.includes('resolut')) module.componentId = 'resolution'; else if (contentLower.includes('facilit')) module.componentId = 'facilitation'; else if (contentLower.includes('particip')) module.componentId = 'participants'; else if (contentLower.includes('file') || contentLower.includes('submit')) module.componentId = 'filing'; else if (contentLower.includes('notif') || contentLower.includes('inform')) module.componentId = 'notification'; else if (contentLower.includes('delegat')) module.componentId = 'delegation'; else module.componentId = 'process'; // Default to process } // Last resort default else { module.componentId = 'process'; } console.log(`Module ${module.id} had no componentId, assigned to: ${module.componentId}`); } } // Add to all modules array allModules.push(module); }); } console.log('Total modules collected:', allModules.length); // Track which templates use each module processedTemplates.forEach(template => { for (const stageId in template.moduleRefs) { if (!stageNames[stageId]) { // Create a readable stage name stageNames[stageId] = stageId.charAt(0).toUpperCase() + stageId.slice(1).replace(/_/g, ' '); } for (const componentId in template.moduleRefs[stageId]) { if (!componentNames[componentId]) { // Create a readable component name componentNames[componentId] = componentId.charAt(0).toUpperCase() + componentId.slice(1).replace(/_/g, ' '); } for (const fieldId in template.moduleRefs[stageId][componentId]) { const moduleId = template.moduleRefs[stageId][componentId][fieldId]; // Find the module with this id const matchingModule = allModules.find(m => m.id === moduleId); if (matchingModule) { // Add template to module's usage if (!matchingModule.usedInTemplates.includes(template.title)) { matchingModule.usedInTemplates.push(template.title); } // Set stage and component for the module if not already set if (!matchingModule.stageId) { matchingModule.stageId = stageId; } // Track template usage if (!templateUsage[template.title]) { templateUsage[template.title] = []; } if (!templateUsage[template.title].includes(moduleId)) { templateUsage[template.title].push(moduleId); } } } } } }); // Define official stages from YAML file const officialStages = { 'intake': 'Intake', 'process': 'Process', 'assessment': 'Assessment', 'deliberation': 'Deliberation', 'resolution': 'Resolution', 'appeal': 'Appeal', 'delegation': 'Delegation' }; // Define official components with standardized display names const officialComponents = { // Process stage components 'principles': 'Principles', 'community_values': 'Values', 'participants': 'Participants', 'facilitation': 'Facilitation', 'ground_rules': 'Ground Rules', 'skills': 'Skills', // Intake stage components 'process_start': 'Process Start', 'filing': 'Filing', 'notification': 'Notification', 'rules_access': 'Rules Access', 'information_access': 'Information Access', 'participant_inclusion': 'Participant Inclusion', // Assessment stage components 'dispute_assessment': 'Assessment', 'values_adherence': 'Values Adherence', 'jurisdiction': 'Jurisdiction', // Deliberation stage components 'deliberation_process': 'Deliberation Process', 'additional_voices': 'Additional Voices', 'deliberation_conclusion': 'Deliberation Conclusion', // Resolution stage components 'resolution_process': 'Resolution Process', 'resolution_failure': 'Resolution Failure', // Appeal stage components 'appeal_criteria': 'Appeal Criteria', 'appeal_process': 'Appeal Process', // Delegation stage components 'delegation_options': 'Delegation Options' }; // Map all component IDs (including custom ones) to official stages const componentToStageMap = { // Process stage components 'principles': 'process', 'community_values': 'process', 'participants': 'process', 'facilitation': 'process', 'ground_rules': 'process', 'skills': 'process', 'values': 'process', 'agreements': 'process', 'participation_commitments': 'process', // Intake stage components 'process_start': 'intake', 'filing': 'intake', 'notification': 'intake', 'rules_access': 'intake', 'information_access': 'intake', 'participant_inclusion': 'intake', 'reporting': 'intake', // Assessment stage components 'dispute_assessment': 'assessment', 'values_adherence': 'assessment', 'jurisdiction': 'assessment', 'non_participation': 'assessment', // Deliberation stage components 'deliberation_process': 'deliberation', 'additional_voices': 'deliberation', 'deliberation_conclusion': 'deliberation', 'decision_making': 'deliberation', 'discussion': 'deliberation', // Resolution stage components 'resolution_process': 'resolution', 'resolution_failure': 'resolution', // Appeal stage components 'appeal_criteria': 'appeal', 'appeal_process': 'appeal', 'appeal_deliberation': 'appeal', 'appeal_resolution': 'appeal', // Delegation stage components 'delegation_options': 'delegation' }; // Map non-standard components to official ones const componentMapping = { 'direct_conversation': 'deliberation_process', 'conflict_awareness': 'dispute_assessment', 'accessing_help': 'process_start', 'preparation': 'principles', 'selection': 'participant_inclusion', 'criteria': 'appeal_criteria', 'agreement': 'resolution_process', 'process': 'principles', 'process_change': 'resolution_failure', 'assessment': 'dispute_assessment', 'situation': 'dispute_assessment', 'reflection': 'deliberation_conclusion', 'monitoring': 'dispute_assessment', 'participation_requirement': 'participant_inclusion' }; allModules.forEach(module => { // If module has no stageId, try to infer from category if (!module.stageId && module.category) { // Check if category matches a known stage if (stageNames[module.category]) { module.stageId = module.category; } else { // Use category as stageId if not already recognized module.stageId = module.category; // Create a readable stage name from category stageNames[module.category] = module.category.charAt(0).toUpperCase() + module.category.slice(1).replace(/_/g, ' '); } } // If still no stageId, try to infer from componentId using the mapping if (!module.stageId && module.componentId) { const mappedStage = componentToStageMap[module.componentId]; if (mappedStage) { module.stageId = mappedStage; console.log(`Module ${module.id} missing stageId, inferred from component: ${mappedStage}`); } else { // Default stage if no mapping exists module.stageId = 'process'; console.log(`Module ${module.id} missing stageId, defaulting to: process`); } } // If STILL no stageId (somehow), assign to process if (!module.stageId) { module.stageId = 'process'; console.log(`Module ${module.id} had no stageId, assigned to: process`); } // Force module to use only official stages if (officialStages[module.stageId]) { // Use official stage name module.stageName = officialStages[module.stageId]; } else { // Map to closest official stage const mappedStage = componentToStageMap[module.componentId]; if (mappedStage && officialStages[mappedStage]) { module.stageId = mappedStage; module.stageName = officialStages[mappedStage]; console.log(`Module ${module.id} had invalid stage "${module.stageId}", remapped to: ${mappedStage}`); } else { // Default to Process stage if can't map anywhere else module.stageId = 'process'; module.stageName = officialStages['process']; console.log(`Module ${module.id} had invalid stage "${module.stageId}", defaulting to: Process`); } } // Handle component mapping for non-standard components if (componentMapping[module.componentId]) { const originalComponentId = module.componentId; module.componentId = componentMapping[originalComponentId]; console.log(`Module ${module.id} had non-standard component "${originalComponentId}", mapped to: ${module.componentId}`); } // Set a readable component name using official list if (officialComponents[module.componentId]) { module.componentName = officialComponents[module.componentId]; } else { // Generate a readable name for custom components module.componentName = module.componentId.charAt(0).toUpperCase() + module.componentId.slice(1).replace(/_/g, ' '); // Log any component not in the official list console.log(`Module ${module.id} uses custom component: ${module.componentId}`); } }); // Log the distribution of modules by stage const stageDistribution = {}; allModules.forEach(module => { stageDistribution[module.stageName] = (stageDistribution[module.stageName] || 0) + 1; }); console.log('Module distribution by stage:', stageDistribution); console.log('Data initialization complete'); console.log('Stages:', Object.keys(stageNames)); console.log('Components:', Object.keys(componentNames)); console.log('Templates:', Object.keys(templateUsage)); return true; } catch (error) { console.error('Error initializing data:', error); modulesContainer.innerHTML = `
Error initializing data: ${error.message}
`; return false; } } function populateFilters() { console.log('Populating filters...'); // Clear existing options stageFilter.innerHTML = ''; componentFilter.innerHTML = ''; templateFilter.innerHTML = ''; // Get unique stages and components const stages = [...new Set(allModules.map(m => m.stageName))].sort(); const components = [...new Set(allModules.map(m => m.componentName))].sort(); const templates = Object.keys(templateUsage).sort(); console.log('Filter options - Stages:', stages); console.log('Filter options - Components:', components); console.log('Filter options - Templates:', templates); // Add options to filters stages.forEach(stage => { const option = document.createElement('option'); option.value = stage; option.textContent = stage; stageFilter.appendChild(option); }); components.forEach(component => { const option = document.createElement('option'); option.value = component; option.textContent = component; componentFilter.appendChild(option); }); templates.forEach(template => { const option = document.createElement('option'); option.value = template; option.textContent = template; templateFilter.appendChild(option); }); } function renderModules() { console.log('Rendering modules...'); // Clear container modulesContainer.innerHTML = ''; // Get filter values const searchText = moduleSearch.value.toLowerCase(); const stageValue = stageFilter.value; const componentValue = componentFilter.value; const templateValue = templateFilter.value; console.log('Filter values:', { search: searchText, stage: stageValue, component: componentValue, template: templateValue }); // Create a stage-based organization of modules const modulesByStage = {}; // Filter modules based on search and filter criteria allModules.forEach(module => { // Check if module matches search text const matchesSearch = searchText === '' || module.title.toLowerCase().includes(searchText) || (module.content && module.content.toLowerCase().includes(searchText)); // Check if module matches stage filter const matchesStage = stageValue === '' || module.stageName === stageValue; // Check if module matches component filter const matchesComponent = componentValue === '' || module.componentName === componentValue; // Check if module matches template filter const matchesTemplate = templateValue === '' || module.usedInTemplates.includes(templateValue); // Include module if it matches all criteria if (matchesSearch && matchesStage && matchesComponent && matchesTemplate) { // Initialize stage object if not exists if (!modulesByStage[module.stageName]) { modulesByStage[module.stageName] = []; } // Add module to its stage modulesByStage[module.stageName].push(module); } }); // Sort stages according to the official order from the YAML file const stageOrder = [ 'Intake', 'Process', 'Assessment', 'Deliberation', 'Resolution', 'Appeal', 'Delegation' ]; // Get all stages from the data const availableStages = Object.keys(modulesByStage); // Sort stages according to the defined order, with any others at the end const sortedStages = []; // First add stages in the predefined order (if they exist in the data) stageOrder.forEach(stage => { if (availableStages.includes(stage)) { sortedStages.push(stage); } }); // Then add any other stages not in the predefined order (sorted alphabetically) availableStages .filter(stage => !stageOrder.includes(stage)) .sort() .forEach(stage => sortedStages.push(stage)); // If no modules match, show message if (sortedStages.length === 0) { modulesContainer.innerHTML = '
No modules match your search criteria
'; return; } // Create HTML for each stage and its modules sortedStages.forEach(stageName => { const stageModules = modulesByStage[stageName]; // Create stage section using similar structure to builder const stageSection = document.createElement('div'); stageSection.className = 'stage-section'; // Create stage header const stageHeader = document.createElement('div'); stageHeader.className = 'stage-header'; const stageHeaderContent = document.createElement('div'); stageHeaderContent.className = 'stage-header-content'; stageHeaderContent.innerHTML = `

${stageName}

Contains ${stageModules.length} module${stageModules.length !== 1 ? 's' : ''}
`; // Create toggle button const toggleBtn = document.createElement('button'); toggleBtn.className = 'toggle-btn'; toggleBtn.innerHTML = '+'; toggleBtn.setAttribute('aria-label', 'Toggle stage content'); stageHeader.appendChild(stageHeaderContent); stageHeader.appendChild(toggleBtn); stageSection.appendChild(stageHeader); // Create stage body const stageBody = document.createElement('div'); stageBody.className = 'stage-body'; stageBody.style.display = 'none'; // Group modules by component const modulesByComponent = {}; stageModules.forEach(module => { // Use just the component name without duplicating stage info const cleanComponentName = module.componentName.replace(module.stageName, '').trim(); const displayComponentName = cleanComponentName || module.componentName; if (!modulesByComponent[displayComponentName]) { modulesByComponent[displayComponentName] = []; } modulesByComponent[displayComponentName].push(module); }); // Sort components const sortedComponents = Object.keys(modulesByComponent).sort(); // Create components container const componentsContainer = document.createElement('div'); componentsContainer.className = 'components'; // Create component sections sortedComponents.forEach(componentName => { const componentModules = modulesByComponent[componentName]; // Create a component group heading with count const componentHeading = document.createElement('h3'); componentHeading.className = 'component-group-heading'; // Create main text const headingText = document.createTextNode(componentName); componentHeading.appendChild(headingText); // Add count in parentheses const moduleCount = componentModules.length; const countSpan = document.createElement('span'); countSpan.className = 'component-module-count'; countSpan.textContent = ` (${moduleCount} module${moduleCount !== 1 ? 's' : ''})`; componentHeading.appendChild(countSpan); componentsContainer.appendChild(componentHeading); // Add modules to component section componentModules.forEach(module => { const moduleCard = createModuleCard(module); componentsContainer.appendChild(moduleCard); }); }); stageBody.appendChild(componentsContainer); stageSection.appendChild(stageBody); // Add toggle functionality stageHeaderContent.addEventListener('click', function() { if (stageBody.style.display === 'none') { stageBody.style.display = 'block'; toggleBtn.innerHTML = '−'; } else { stageBody.style.display = 'none'; toggleBtn.innerHTML = '+'; } }); toggleBtn.addEventListener('click', function(e) { e.stopPropagation(); if (stageBody.style.display === 'none') { stageBody.style.display = 'block'; toggleBtn.innerHTML = '−'; } else { stageBody.style.display = 'none'; toggleBtn.innerHTML = '+'; } }); modulesContainer.appendChild(stageSection); }); console.log('Modules rendering complete'); } function createModuleCard(module) { const card = document.createElement('div'); card.className = 'component-card'; card.dataset.id = module.id; // Format templates list const templatesList = module.usedInTemplates.length > 0 ? module.usedInTemplates.join(', ') : 'Not used in any template'; // Create card content with structure similar to builder component card.innerHTML = `

${module.title}

${module.stageName} ${module.componentName}
Used in templates: ${templatesList}
`; // Make the header clickable to toggle content visibility const header = card.querySelector('.component-header'); const body = card.querySelector('.component-body'); // Add toggle button to the header (already positioned by CSS flexbox) const toggleBtn = document.createElement('button'); toggleBtn.className = 'toggle-btn'; toggleBtn.innerHTML = '+'; toggleBtn.setAttribute('aria-label', 'Toggle module content'); header.appendChild(toggleBtn); // Start with content hidden body.style.display = 'none'; // Toggle functionality function toggleContent() { if (body.style.display === 'none') { body.style.display = 'block'; toggleBtn.innerHTML = '−'; card.classList.add('expanded'); } else { body.style.display = 'none'; toggleBtn.innerHTML = '+'; card.classList.remove('expanded'); } } header.addEventListener('click', toggleContent); return card; } // Initialize page function init() { console.log('Initializing modules page...'); // Load data if (!initializeData()) { console.error('Failed to initialize data'); return; } // Populate filters populateFilters(); // Render modules renderModules(); // Add event listeners to filters moduleSearch.addEventListener('input', renderModules); stageFilter.addEventListener('change', renderModules); componentFilter.addEventListener('change', renderModules); templateFilter.addEventListener('change', renderModules); console.log('Modules page initialized with', allModules.length, 'modules'); } // Start initialization init(); });