hm_ai_qc_report_tool/static/js/tabs.js
nickviljoen e6f3e9387e Add modular architecture, core framework, and web UI
New blueprint-based module system (hm_qc, video_qc, video_master,
reporting), core framework (database, config, templates), and
unified web interface with progress tracking and tab navigation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:39:04 +02:00

288 lines
8.6 KiB
JavaScript

/**
* Tab Management for HM QC Platform
*
* Handles tab state preservation, navigation, and cross-tab communication
*/
const TabManager = {
/**
* Save state for a specific tab
* @param {string} tabId - Tab identifier (e.g., 'hm-qc', 'video-qc')
* @param {object} state - State object to save
*/
saveState(tabId, state) {
try {
const stateKey = `tab_${tabId}_state`;
sessionStorage.setItem(stateKey, JSON.stringify({
...state,
timestamp: new Date().toISOString()
}));
} catch (error) {
console.error(`Error saving state for tab ${tabId}:`, error);
}
},
/**
* Load state for a specific tab
* @param {string} tabId - Tab identifier
* @returns {object|null} Saved state or null if not found
*/
loadState(tabId) {
try {
const stateKey = `tab_${tabId}_state`;
const stateStr = sessionStorage.getItem(stateKey);
if (!stateStr) {
return null;
}
return JSON.parse(stateStr);
} catch (error) {
console.error(`Error loading state for tab ${tabId}:`, error);
return null;
}
},
/**
* Clear state for a specific tab
* @param {string} tabId - Tab identifier
*/
clearState(tabId) {
try {
const stateKey = `tab_${tabId}_state`;
sessionStorage.removeItem(stateKey);
} catch (error) {
console.error(`Error clearing state for tab ${tabId}:`, error);
}
},
/**
* Clear all tab states
*/
clearAllStates() {
try {
const keys = Object.keys(sessionStorage);
keys.forEach(key => {
if (key.startsWith('tab_')) {
sessionStorage.removeItem(key);
}
});
} catch (error) {
console.error('Error clearing all tab states:', error);
}
},
/**
* Get current active tab
* @returns {string|null} Active tab ID or null
*/
getActiveTab() {
const activeLink = document.querySelector('.hm-tabs .nav-link.active');
return activeLink ? activeLink.dataset.tab : null;
},
/**
* Set active tab
* @param {string} tabId - Tab identifier to activate
*/
setActiveTab(tabId) {
// Remove active class from all tabs
document.querySelectorAll('.hm-tabs .nav-link').forEach(link => {
link.classList.remove('active');
});
// Add active class to target tab
const targetLink = document.querySelector(`.hm-tabs .nav-link[data-tab="${tabId}"]`);
if (targetLink) {
targetLink.classList.add('active');
}
},
/**
* Check if a tab has new content/notifications
* @param {string} tabId - Tab identifier
* @returns {boolean} True if tab has notifications
*/
hasNotification(tabId) {
const state = this.loadState(tabId);
return state && state.hasNotification === true;
},
/**
* Set notification status for a tab
* @param {string} tabId - Tab identifier
* @param {boolean} hasNotification - Notification status
*/
setNotification(tabId, hasNotification) {
const state = this.loadState(tabId) || {};
state.hasNotification = hasNotification;
this.saveState(tabId, state);
// Update UI to show notification indicator
this.updateNotificationIndicator(tabId, hasNotification);
},
/**
* Update notification indicator in tab UI
* @param {string} tabId - Tab identifier
* @param {boolean} show - Whether to show indicator
*/
updateNotificationIndicator(tabId, show) {
const tabLink = document.querySelector(`.hm-tabs .nav-link[data-tab="${tabId}"]`);
if (!tabLink) return;
// Remove existing indicator
const existingIndicator = tabLink.querySelector('.notification-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
// Add new indicator if needed
if (show) {
const indicator = document.createElement('span');
indicator.className = 'notification-indicator';
indicator.innerHTML = '<i class="bi bi-dot"></i>';
indicator.style.cssText = 'color: #dc3545; font-size: 2rem; line-height: 0; margin-left: -8px;';
tabLink.appendChild(indicator);
}
}
};
/**
* Initialize tab management when DOM is loaded
*/
document.addEventListener('DOMContentLoaded', function() {
console.log('Tab management initialized');
// Handle tab click events
document.querySelectorAll('.hm-tabs .nav-link').forEach(link => {
link.addEventListener('click', function(e) {
const currentTab = TabManager.getActiveTab();
const targetTab = this.dataset.tab;
// Don't do anything if clicking the same tab
if (currentTab === targetTab) {
e.preventDefault();
return;
}
// Save current tab state before switching
if (currentTab) {
const currentState = getCurrentTabState(currentTab);
if (currentState) {
TabManager.saveState(currentTab, currentState);
}
}
// Clear notification when switching to a tab
TabManager.setNotification(targetTab, false);
console.log(`Switching from ${currentTab} to ${targetTab}`);
});
});
// Restore state for current active tab
const activeTab = TabManager.getActiveTab();
if (activeTab) {
const state = TabManager.loadState(activeTab);
if (state) {
console.log(`Restoring state for ${activeTab}:`, state);
restoreTabState(activeTab, state);
}
}
// Check for notifications from other tabs
checkForNotifications();
});
/**
* Get current state for a tab (to be implemented per module)
* @param {string} tabId - Tab identifier
* @returns {object} Current state object
*/
function getCurrentTabState(tabId) {
// This function should be overridden by each module
// to return its current state
return {
scrollPosition: window.scrollY,
timestamp: new Date().toISOString()
};
}
/**
* Restore state for a tab (to be implemented per module)
* @param {string} tabId - Tab identifier
* @param {object} state - State to restore
*/
function restoreTabState(tabId, state) {
// This function should be overridden by each module
// to restore its state
if (state.scrollPosition) {
window.scrollTo(0, state.scrollPosition);
}
}
/**
* Check for notifications from other tabs
*/
function checkForNotifications() {
const tabs = ['hm-qc', 'video-qc', 'video-master', 'reporting'];
tabs.forEach(tabId => {
if (TabManager.hasNotification(tabId)) {
TabManager.updateNotificationIndicator(tabId, true);
}
});
}
/**
* Show notification toast
* @param {string} message - Notification message
* @param {string} type - Notification type (success, error, warning, info)
*/
function showNotification(message, type = 'info') {
// Create toast container if it doesn't exist
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '9999';
document.body.appendChild(toastContainer);
}
// Create toast element
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
toastContainer.appendChild(toast);
// Show toast
const bsToast = new bootstrap.Toast(toast, {
delay: 5000,
autohide: true
});
bsToast.show();
// Remove toast after it's hidden
toast.addEventListener('hidden.bs.toast', function() {
toast.remove();
});
}
// Make TabManager available globally
window.TabManager = TabManager;
window.showNotification = showNotification;