532 lines
24 KiB
JavaScript

// --- 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 = `
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 fst-italic text-muted"></p>
`;
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, "&lt;").replace(/>/g, "&gt;");
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 = '<p class="text-center fst-italic">No panel data available yet.</p>';
} 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 = `
<thead>
<tr><th>Alias</th><th>Panel</th><th>SCADA Status</th><th>Drawing Status</th><th>Equipment Type</th><th>Type of Conveyor</th></tr>
</thead>
<tbody></tbody>
`;
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 = '<span class="status-yes">Yes</span>';
row.insertCell().innerHTML = '<span class="status-no">No</span>';
row.insertCell().textContent = item.equipment_type || 'N/A';
row.insertCell().textContent = item.conveyor_type || 'N/A';
});
container.appendChild(table);
}
});
if (panelsWithConflicts === 0) {
container.innerHTML = '<p class="text-center fst-italic">No conflicts found across all panels.</p>';
}
}
// 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
? "<p class=\"text-center fst-italic\">No projects loaded. Use 'Add Project' to create one.</p>"
: "<p class=\"text-center fst-italic\">Select a project to view data.</p>";
// --- 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<string>} 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, ... };