Files
builder-prototype/static/js/modules-page.js
Nathan Schneider 521c782c42 Initial commit
2025-03-23 21:44:39 -06:00

695 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = '<div class="error">Error: Module data not available</div>';
return false;
}
if (typeof rawProtocolTemplates === 'undefined' || typeof templateMapper === 'undefined') {
console.error('Error: Template data or mapper not available');
modulesContainer.innerHTML = '<div class="error">Error: Template data not available</div>';
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 = `<div class="error">Error initializing data: ${error.message}</div>`;
return false;
}
}
function populateFilters() {
console.log('Populating filters...');
// Clear existing options
stageFilter.innerHTML = '<option value="">All Stages</option>';
componentFilter.innerHTML = '<option value="">All Components</option>';
templateFilter.innerHTML = '<option value="">All Templates</option>';
// 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 = '<div class="no-results">No modules match your search criteria</div>';
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 = `
<h2>${stageName}</h2>
<div class="stage-brief">
Contains ${stageModules.length} module${stageModules.length !== 1 ? 's' : ''}
</div>
`;
// 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 = `
<div class="component-header">
<h3>${module.title}</h3>
</div>
<div class="component-body">
<div class="module-meta">
<span class="tag stage-tag">${module.stageName}</span>
<span class="tag component-tag">${module.componentName}</span>
</div>
<div class="module-content">
<textarea readonly>${module.content}</textarea>
</div>
<div class="module-templates">
<strong>Used in templates:</strong> ${templatesList}
</div>
</div>
`;
// 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();
});