Initial commit

This commit is contained in:
Nathan Schneider
2025-03-23 21:44:39 -06:00
commit 521c782c42
56 changed files with 8295 additions and 0 deletions

695
static/js/modules-page.js Normal file
View File

@ -0,0 +1,695 @@
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();
});