// --- UI Update Functions --- // Store references to frequently used DOM elements const uiElements = { overallScadaProgress: document.getElementById('overall-scada-progress'), scadaPanelsProgress: document.getElementById('scada-panels-progress'), overallDrawingProgress: document.getElementById('overall-drawing-progress'), drawingPanelsProgress: document.getElementById('drawing-panels-progress'), panelsConflicts: document.getElementById('panels-conflicts'), overallScadaText: document.getElementById('overall-scada-text'), overallDrawingText: document.getElementById('overall-drawing-text'), conflictCountBadge: document.getElementById('conflict-count'), statusBarProjectName: document.getElementById('selected-project-status-name'), statusBarMessage: document.getElementById('status-message'), statusBarCommit: document.getElementById('last-commit'), scadaContent: document.getElementById('scada-content'), drawingsContent: document.getElementById('drawings-content'), conflictsContent: document.getElementById('conflicts-content'), navLinks: document.querySelectorAll('.nav-link'), projectNameDisplays: document.querySelectorAll('.project-name-display'), // Status message elements for Manage Files modal manageFilesStatus: document.getElementById('manageFilesStatus'), uploadStatus: document.getElementById('uploadStatus'), analysisTriggerStatus: document.getElementById('analysisTriggerStatus'), deleteProjectStatus: document.getElementById('deleteProjectStatus'), uploadManifestStatus: document.getElementById('uploadManifestStatus') }; // --- Loading Indicators --- /** * Displays a loading indicator within a container element. * @param {HTMLElement} containerElement - The DOM element to show the indicator in. * @param {string} message - The message to display below the spinner. */ function uiShowLoadingIndicator(containerElement, message = "Processing data...") { if (!containerElement) return; let indicatorDiv = containerElement.querySelector('.processing-indicator'); // --- Check if projects exist --- const projectSelector = document.getElementById('projectSelector'); const noProjectsExist = projectSelector && projectSelector.options.length === 1 && projectSelector.options[0].disabled; // Determine appropriate message if none was explicitly provided let displayMessage = message; if (message === "Processing data..." || message === "Select a project" || message === "Loading data...") { // Check against default/generic messages displayMessage = noProjectsExist ? "No projects available. Please add one." : (selectedProjectName ? "Loading data..." : "Select a project"); } // --- End Check --- if (!indicatorDiv) { // If indicator doesn't exist, clear the container first containerElement.innerHTML = ''; // Create and add the indicator indicatorDiv = document.createElement('div'); indicatorDiv.className = 'text-center p-4 processing-indicator'; indicatorDiv.innerHTML = `
Loading...

`; containerElement.prepend(indicatorDiv); } else { // If indicator already exists, ensure it's visible (it might have been hidden) indicatorDiv.style.display = 'block'; } // Update message (simple text sanitization) const messageElement = indicatorDiv.querySelector('p'); if (messageElement) { // Use the determined displayMessage const safeMessage = displayMessage.replace(//g, ">"); messageElement.textContent = safeMessage; } } /** * Removes the loading indicator from a container element and restores content visibility. * @param {HTMLElement} containerElement - The DOM element to clear the indicator from. */ function uiClearLoadingIndicator(containerElement) { if (!containerElement) return; const indicatorDiv = containerElement.querySelector('.processing-indicator'); if (indicatorDiv) { // Remove the indicator element completely indicatorDiv.remove(); } // No longer need to manage 'content-hidden-by-loader' class // Remove default "Loading panel data..." placeholders if they still exist const placeholderP = Array.from(containerElement.querySelectorAll('p.fst-italic')).find(p => p.textContent.toLowerCase().includes('loading') && !p.closest('.processing-indicator') ); if (placeholderP) { placeholderP.remove(); } } // --- Overall UI State --- /** * Sets the entire UI to reflect a processing state, showing loading indicators. * @param {string} statusMsg - The status message to display in the indicators. */ function uiShowProcessingState(statusMsg = "Loading data...") { console.log(`[UI] Setting processing state: "${statusMsg}"`); const containers = [ uiElements.overallScadaProgress, uiElements.scadaPanelsProgress, uiElements.overallDrawingProgress, uiElements.drawingPanelsProgress, uiElements.panelsConflicts ]; // Destroy existing charts (delegated to chartManager) chartManagerDestroyAll(); // Show loading indicator in all main content containers containers.forEach(container => { if (container) { uiShowLoadingIndicator(container, statusMsg); } }); // Clear text content that is updated directly if (uiElements.overallScadaText) uiElements.overallScadaText.textContent = ''; if (uiElements.overallDrawingText) uiElements.overallDrawingText.textContent = ''; if (uiElements.conflictCountBadge) { uiElements.conflictCountBadge.textContent = '...'; uiElements.conflictCountBadge.style.display = 'inline-block'; // Show badge while loading } // Ensure the currently selected section's container is visible // (uiShowSection handles this, but good belt-and-braces) const sectionId = currentVisibleSection; // Assumes currentVisibleSection is accessible (global state) if (sectionId === 'scada' && uiElements.scadaContent) uiElements.scadaContent.style.display = 'block'; else if (sectionId === 'drawings' && uiElements.drawingsContent) uiElements.drawingsContent.style.display = 'block'; else if (sectionId === 'conflicts' && uiElements.conflictsContent) uiElements.conflictsContent.style.display = 'block'; } /** * Clears loading indicators and prepares the UI for content rendering. * Note: Actual content rendering is triggered by calling core update functions. */ function uiClearProcessingState() { console.log(`[UI] Clearing processing state.`); const containers = [ uiElements.overallScadaProgress, uiElements.scadaPanelsProgress, uiElements.overallDrawingProgress, uiElements.drawingPanelsProgress, uiElements.panelsConflicts ]; containers.forEach(container => { if (container) uiClearLoadingIndicator(container); }); } // --- Section/Tab Navigation --- /** * Shows the specified content section and hides others. Updates active nav link. * @param {string} sectionId - 'scada', 'drawings', or 'conflicts'. */ function uiShowSection(sectionId) { console.log("[UI] Showing section:", sectionId); // Hide all sections first if (uiElements.scadaContent) uiElements.scadaContent.style.display = 'none'; if (uiElements.drawingsContent) uiElements.drawingsContent.style.display = 'none'; if (uiElements.conflictsContent) uiElements.conflictsContent.style.display = 'none'; let elementToShow = null; if (sectionId === 'scada' && uiElements.scadaContent) elementToShow = uiElements.scadaContent; else if (sectionId === 'drawings' && uiElements.drawingsContent) elementToShow = uiElements.drawingsContent; else if (sectionId === 'conflicts' && uiElements.conflictsContent) elementToShow = uiElements.conflictsContent; if (elementToShow) { elementToShow.style.display = 'block'; // Update global state (This might move to an event handler later) currentVisibleSection = sectionId; console.log(`[UI] Current visible section set to: ${currentVisibleSection}`); // Trigger redraw of the newly visible section // Needs access to global state: currentProjectData, selectedProjectName, isAnalysisGloballyActive const projectData = currentProjectData[selectedProjectName]; // Check if analysis is globally marked as active OR if the specific project status indicates processing const analysisIsActive = typeof isAnalysisGloballyActive !== 'undefined' && isAnalysisGloballyActive; const projectIsProcessing = projectData && isProcessing(projectData.status); // isProcessing needs to be accessible if (analysisIsActive || projectIsProcessing) { const statusMsg = projectData?.status || "Analysis in progress..."; // Use project status if available, otherwise generic message console.log(`[UI] Section ${sectionId} shown, project processing. Status: "${statusMsg}"`); // Ensure loading indicator is visible in the right place if (sectionId === 'scada') { uiShowLoadingIndicator(uiElements.overallScadaProgress, statusMsg); uiShowLoadingIndicator(uiElements.scadaPanelsProgress, statusMsg); } else if (sectionId === 'drawings') { uiShowLoadingIndicator(uiElements.overallDrawingProgress, statusMsg); uiShowLoadingIndicator(uiElements.drawingPanelsProgress, statusMsg); } else if (sectionId === 'conflicts') { uiShowLoadingIndicator(uiElements.panelsConflicts, statusMsg); } chartManagerDestroyAll(); // Ensure charts are gone if processing } else if (projectData) { // Project exists and is NOT processing // Project is ready, draw the content immediately console.log(`[UI] Section ${sectionId} shown, project ready. Drawing content.`); // Double check status before actually drawing (good practice) const currentData = currentProjectData[selectedProjectName]; const stillNotProcessing = currentData && !isProcessing(currentData.status); const stillGloballyInactive = typeof isAnalysisGloballyActive !== 'undefined' && !isAnalysisGloballyActive; if (stillNotProcessing && stillGloballyInactive) { // Explicitly clear any loading indicators before drawing uiClearProcessingState(); // Call core update functions directly if (sectionId === 'scada') updateUIScadaCore(currentData); else if (sectionId === 'drawings') updateUIDrawingCore(currentData); else if (sectionId === 'conflicts') updateUIConflictsCore(currentData); } else { // If state somehow changed back to processing *just* as we switched tabs, show loading const currentStatusMsg = currentData?.status || (isAnalysisGloballyActive ? "Analysis in progress..." : "State changed..."); console.log(`[UI] Status changed back to processing just before drawing for ${sectionId}. Status: "${currentStatusMsg}"`); uiShowProcessingState(currentStatusMsg); } } else { // No project data found for the selected project console.log(`[UI] Section ${sectionId} shown, but no data for project ${selectedProjectName}. Showing loading.`); // Show loading indicator as data is missing or project not selected // --- Check if projects exist --- const projectSelectorCheck = document.getElementById('projectSelector'); const noProjectsExistCheck = projectSelectorCheck && projectSelectorCheck.options.length === 1 && projectSelectorCheck.options[0].disabled; const msg = noProjectsExistCheck ? "No projects available. Please add one." : (selectedProjectName ? "Loading data..." : "Select a project"); // --- End Check --- if (sectionId === 'scada') { uiShowLoadingIndicator(uiElements.overallScadaProgress, msg); uiShowLoadingIndicator(uiElements.scadaPanelsProgress, msg); } else if (sectionId === 'drawings') { uiShowLoadingIndicator(uiElements.overallDrawingProgress, msg); uiShowLoadingIndicator(uiElements.drawingPanelsProgress, msg); } else if (sectionId === 'conflicts') { uiShowLoadingIndicator(uiElements.panelsConflicts, msg); } } } else { console.error("[UI] Attempted to show unknown section:", sectionId); // Default back to SCADA maybe? if(uiElements.scadaContent) uiElements.scadaContent.style.display = 'block'; currentVisibleSection = 'scada'; } // Update active nav link state uiElements.navLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('data-view') === sectionId) { link.classList.add('active'); } }); } // --- Content Updates --- /** * Updates all elements displaying the current project name. * @param {string} projectName - The name of the project. */ function uiUpdateProjectNameDisplay(projectName) { uiElements.projectNameDisplays.forEach(el => el.textContent = projectName || '...'); } /** * Updates the status bar content. * @param {string} projectName - Name of the current project. * @param {string} statusMsg - Status message to display. * @param {string} commitHash - Last commit hash (abbreviated). */ function uiUpdateStatusBar(projectName, statusMsg, commitHash) { if (uiElements.statusBarProjectName) uiElements.statusBarProjectName.textContent = projectName || '...'; if (uiElements.statusBarMessage) uiElements.statusBarMessage.textContent = statusMsg || 'N/A'; if (uiElements.statusBarCommit) uiElements.statusBarCommit.textContent = commitHash ? commitHash.substring(0, 7) : 'N/A'; } /** * Updates the text displaying overall statistics for SCADA or Drawing views. * @param {string} context - 'scada' or 'drawing'. * @param {number} foundCount - Number of items found. * @param {number} totalCount - Total number of items. * @param {number} percentage - Calculated percentage found. */ function uiUpdateOverallStatsText(context, foundCount, totalCount, percentage) { const element = context === 'scada' ? uiElements.overallScadaText : uiElements.overallDrawingText; if (element) { element.textContent = context === 'scada' ? `Found in SCADA: ${foundCount}/${totalCount} (${percentage}%)` : `Found in Drawing: ${foundCount}/${totalCount} (${percentage}%)`; } else { console.warn(`[UI] Element not found for overall stats text: ${context}`); } } /** * Updates the conflicts table display. * @param {object} panelsData - The panels data containing conflict lists. */ function uiUpdateConflictsTable(panelsData) { const container = uiElements.panelsConflicts; if (!container) return; container.innerHTML = ''; // Clear previous content let totalConflicts = 0; let panelsWithConflicts = 0; if (!panelsData || Object.keys(panelsData).length === 0) { container.innerHTML = '

No panel data available yet.

'; } else { const sortedPanels = Object.keys(panelsData).sort(); sortedPanels.forEach(panelName => { const panel = panelsData[panelName]; const conflictsList = panel.found_scada_only_list || []; if (conflictsList.length > 0) { panelsWithConflicts++; totalConflicts += conflictsList.length; const panelHeader = document.createElement('h4'); panelHeader.className = 'mt-4 mb-2'; panelHeader.textContent = `${panelName} (${conflictsList.length} conflicts)`; container.appendChild(panelHeader); const table = document.createElement('table'); table.className = 'table table-sm table-striped table-hover table-bordered'; table.innerHTML = ` AliasPanelSCADA StatusDrawing StatusEquipment TypeType of Conveyor `; const tbody = table.querySelector('tbody'); conflictsList.sort((a, b) => a.alias.localeCompare(b.alias)).forEach(item => { const row = tbody.insertRow(); row.classList.add('table-warning'); row.insertCell().textContent = item.alias; row.insertCell().textContent = item.control_panel; row.insertCell().innerHTML = 'Yes'; row.insertCell().innerHTML = 'No'; row.insertCell().textContent = item.equipment_type || 'N/A'; row.insertCell().textContent = item.conveyor_type || 'N/A'; }); container.appendChild(table); } }); if (panelsWithConflicts === 0) { container.innerHTML = '

No conflicts found across all panels.

'; } } // Update total count badge if (uiElements.conflictCountBadge) { uiElements.conflictCountBadge.textContent = totalConflicts; uiElements.conflictCountBadge.style.display = totalConflicts > 0 ? 'inline-block' : 'none'; } } /** * Clears all project-specific display areas (charts, text, tables). */ function uiClearProjectDisplay() { console.log("[UI] Clearing project display."); // Clear charts (delegated) chartManagerDestroyAll(); // Clear text if (uiElements.overallScadaText) uiElements.overallScadaText.textContent = 'Found in SCADA: N/A'; if (uiElements.overallDrawingText) uiElements.overallDrawingText.textContent = 'Found in Drawing: N/A'; if (uiElements.conflictCountBadge) { uiElements.conflictCountBadge.textContent = '0'; uiElements.conflictCountBadge.style.display = 'none'; } // Clear containers // --- Check if projects exist --- const projectSelector = document.getElementById('projectSelector'); const noProjectsExist = projectSelector && projectSelector.options.length === 1 && projectSelector.options[0].disabled; const defaultMsg = noProjectsExist ? "

No projects loaded. Use 'Add Project' to create one.

" : "

Select a project to view data.

"; // --- End Check --- if(uiElements.scadaPanelsProgress) uiElements.scadaPanelsProgress.innerHTML = defaultMsg; if(uiElements.drawingPanelsProgress) uiElements.drawingPanelsProgress.innerHTML = defaultMsg; if(uiElements.panelsConflicts) uiElements.panelsConflicts.innerHTML = defaultMsg; } // --- Status Messages --- /** * Shows a status message in a specific element, optionally auto-clearing. * @param {HTMLElement} element - The status display element. * @param {string} message - The message to show. * @param {string} type - 'info', 'success', 'warning', 'danger'. * @param {boolean} autoClear - Whether to hide the message after a delay. * @param {number} delay - Delay in milliseconds for auto-clear. */ function uiShowStatusMessage(element, message, type = 'info', autoClear = true, delay = 5000) { if (!element) return; // Use text-based classes for inline messages, alert classes for block messages const isAlert = element.classList.contains('alert'); if (isAlert) { element.className = `mt-3 alert alert-${type}`; } else { element.className = `mt-2 text-${type}`; if (type === 'danger' || type === 'warning' || type === 'success') { element.className += ' fw-bold'; } } element.textContent = message; element.style.display = 'block'; // Clear previous timeouts if any if (element.timeoutId) clearTimeout(element.timeoutId); element.timeoutId = null; // Clear the stored ID if (autoClear) { element.timeoutId = setTimeout(() => { element.style.display = 'none'; element.timeoutId = null; }, delay); } } function uiShowManageFilesStatus(message, type = 'info') { uiShowStatusMessage(uiElements.manageFilesStatus, message, type, false); // Not auto-clearing by default } function uiShowUploadStatus(message, type = 'info', autoClear = true) { uiShowStatusMessage(uiElements.uploadStatus, message, type, autoClear, 5000); } function uiShowAnalysisTriggerStatus(message, type = 'info', autoClear = true) { uiShowStatusMessage(uiElements.analysisTriggerStatus, message, type, autoClear, 8000); } function uiShowDeleteProjectStatus(message, type = 'info', autoClear = true) { uiShowStatusMessage(uiElements.deleteProjectStatus, message, type, autoClear, 6000); } function uiShowUploadManifestStatus(message, type = 'info', autoClear = true) { uiShowStatusMessage(uiElements.uploadManifestStatus, message, type, autoClear, 5000); } /** Clears all status messages within the Manage Files modal. */ function uiClearManageFilesStatusMessages() { const elements = [ uiElements.manageFilesStatus, uiElements.uploadStatus, uiElements.analysisTriggerStatus, uiElements.deleteProjectStatus, uiElements.uploadManifestStatus ]; elements.forEach(element => { if (element) { element.style.display = 'none'; if (element.timeoutId) { clearTimeout(element.timeoutId); element.timeoutId = null; } } }); } // --- Project Selector Update --- /** * Rebuilds the project selector dropdown based on the provided list. * Tries to maintain the current selection if possible. * @param {Array} incomingProjects - List of project names. * @returns {boolean} - True if the selection was changed/reset, false otherwise. */ function uiUpdateProjectSelector(incomingProjects) { const projectSelector = document.getElementById('projectSelector'); // Get fresh reference if (!projectSelector) return false; const currentOptions = Array.from(projectSelector.options).map(opt => opt.value); const previouslySelected = projectSelector.value; let selectionChanged = false; // Check if the list of projects has actually changed const hasChanged = incomingProjects.length !== currentOptions.length || incomingProjects.some(p => !currentOptions.includes(p)) || currentOptions.some(p => !incomingProjects.includes(p)); if (hasChanged) { console.log("[UI] Project list changed. Rebuilding project selector."); projectSelector.innerHTML = ''; // Clear existing options if (incomingProjects.length > 0) { incomingProjects.sort().forEach(projName => { const option = document.createElement('option'); option.value = projName; option.textContent = projName; projectSelector.appendChild(option); }); // Try to re-select the previous project if it still exists if (incomingProjects.includes(previouslySelected)) { projectSelector.value = previouslySelected; } else { projectSelector.selectedIndex = 0; // Select the first one selectionChanged = true; // Selection was reset to the first item } } else { // Handle empty project list const option = document.createElement('option'); option.value = ''; option.textContent = 'No projects found'; option.disabled = true; projectSelector.appendChild(option); selectionChanged = true; // Selection is now empty/disabled } } // Update button state based on current selection (even if list didn't change) const manageFilesBtn = document.getElementById('manageFilesBtn'); // Get fresh reference if (manageFilesBtn) { manageFilesBtn.disabled = !projectSelector.value; } return selectionChanged; } // --- Export (if using modules in the future) --- // export { uiShowLoadingIndicator, uiClearLoadingIndicator, ... };