KAAUH Lab - Robust Buttons & Highlighting

Adds navigation shortcut from Lab Orders to Lab Test Status tab. Also includes all previous enhancements with persistent input filters and anti-flickering optimizations. Fixed workbench button selection and relocated Reset button.

As of 2025-06-30. See the latest version.

// ==UserScript==
// @name         KAAUH Lab - Robust Buttons & Highlighting
// @namespace    Violentmonkey Scripts
// @version      1.7.8
// @description  Adds navigation shortcut from Lab Orders to Lab Test Status tab. Also includes all previous enhancements with persistent input filters and anti-flickering optimizations. Fixed workbench button selection and relocated Reset button.
// @match        https://his.kaauh.org/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @author       Hamad Al-Shegifi (Refactored for specific function)
// ==/UserScript==

(async function () { // The script must be async to use GM_getValue
    'use strict';

    console.log("KAAUH Lab - Robust Buttons & Highlighting Script v1.6.0 Loading...");

    //================================================================================
    // --- CONFIGURATION & STATE VARIABLES ---
    //================================================================================

    const WORKBENCH_SELECTION_KEY = 'kaauh_last_workbench_selection';
    const FILTER_PERSISTENCE_KEY = 'kaauh_filter_persistence';

    let state = {
        lastUrl: location.href,
        hasAppliedInitialFilters: false,
        filterRetryCount: 0,
        maxFilterRetries: 5, // Reduced from 10 to prevent excessive retries
        isApplyingFilters: false, // [NEW] Prevent concurrent filter applications
        lastFilterApplication: 0, // [NEW] Throttle filter applications
        observerThrottle: null, // [NEW] Throttle observer callbacks
        selectedWorkbenchId: null, // [FIXED] Track selected workbench
        resetButtonRelocated: false, // [NEW] Track if reset button has been relocated
    };

    const SELECTORS = {
        workbenchDropdown: '#filterSec',
        statusDropdownTrigger: 'option[translateid="lab-test-analyzer.result-status.Ordered"]',
        buttonGroupContainer: 'ul.nav.nav-tabs.tab-container',
        buttonGroup: '.filter-btn-group',
        agHeaderViewport: '.ag-header-viewport',
        resetButton: 'button[translateid="lab-order-list.Reset"]',
        referenceLabToggle: '.nova-toggle',
    };

    //================================================================================
    // --- STYLE INJECTION (GM_addStyle) ---
    //================================================================================

    GM_addStyle(`
        .filter-btn-group { display: flex !important; flex-wrap: nowrap !important; gap: 6px !important; margin-top: 12px !important; overflow-x: auto !important; padding-bottom: 6px !important; padding-inline: 10px !important; }
        .filter-btn-group::-webkit-scrollbar { height: 8px; }
        .filter-btn-group::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
        .filter-btn-group::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
        .filter-btn-group::-webkit-scrollbar-thumb:hover { background: #555; }
        .filter-btn-group .btn { padding: 6px 12px !important; font-size: 13px !important; font-weight: bold !important; border-radius: 6px !important; border: none !important; color: #fff !important; white-space: nowrap !important; cursor: pointer !important; flex-shrink: 0 !important; transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; }
        .filter-btn-group .btn:hover { transform: scale(1.05); box-shadow: 0 4px 8px rgba(0,0,0,0.15); }
        .filter-btn-group .btn.selected {
            box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.8), 0 0 0 5px rgba(0, 123, 255, 0.5) !important;
            transform: scale(1.02) !important;
            z-index: 10 !important;
            position: relative !important;
        }
        .filter-btn-group .btn:not(.selected) {
            opacity: 0.4 !important;
        }

        /* [NEW] Reset button relocation styles */
        .reset-button-container {
            display: flex !important;
            align-items: center !important;
            gap: 15px !important;
            margin-bottom: 10px !important;
        }

        .relocated-reset-button {
            order: -1 !important;
            margin-right: 15px !important;
        }

        ${Array.from({ length: 20 }, (_, i) => {
            const colors = [ '#28a745', '#ffc107', '#17a2b8', '#dc3545', '#6f42c1', '#fd7e14', '#20c997', '#6610f2', '#e83e8c', '#343a40', '#198754', '#0d6efd', '#d63384', '#6c757d', '#ff5733', '#9c27b0', '#00bcd4', '#795548', '#3f51b5'];
            return `.btn-color-${i} { background-color: ${colors[i % colors.length]} !important; }`;
        }).join('\n')}

        .ag-row:hover .ag-cell {
            background-color: lightblue !important;
        }
    `);

    //================================================================================
    // --- ENHANCED FILTER PERSISTENCE FUNCTIONS ---
    //================================================================================

    /**
     * [NEW] Save current filter values to persistent storage
     */
    async function saveFilterValues(filters) {
        try {
            await GM_setValue(FILTER_PERSISTENCE_KEY, JSON.stringify(filters));
            console.log("Filter values saved:", filters);
        } catch (e) {
            console.error("Error saving filter values:", e);
        }
    }

    /**
     * [NEW] Load filter values from persistent storage
     */
    async function loadFilterValues() {
        try {
            const filtersJSON = await GM_getValue(FILTER_PERSISTENCE_KEY, null);
            return filtersJSON ? JSON.parse(filtersJSON) : {};
        } catch (e) {
            console.error("Error loading filter values:", e);
            return {};
        }
    }

    /**
     * [OPTIMIZED] Improved setColumnFilter function with anti-flickering optimizations
     */
    function setColumnFilter(columnName, value, shouldPersist = true) {
        const headerViewport = document.querySelector(SELECTORS.agHeaderViewport);
        if (!headerViewport) {
            console.warn(`Header viewport not found for column ${columnName}`);
            return false;
        }

        const allCols = Array.from(headerViewport.querySelectorAll('.ag-header-cell')).map(cell => cell.getAttribute('col-id'));
        const columnIndex = allCols.indexOf(columnName);
        if (columnIndex === -1) {
            console.warn(`Column ${columnName} not found in grid`);
            return false;
        }

        const filterInput = headerViewport.querySelector(`.ag-header-row[aria-rowindex="2"]`)?.children[columnIndex]?.querySelector('.ag-floating-filter-input');
        if (!filterInput) {
            console.warn(`Filter input not found for column ${columnName}`);
            return false;
        }

        // [OPTIMIZATION] Check if value is already set to avoid unnecessary updates
        if (filterInput.value === value) {
            console.log(`Filter for ${columnName} already set to: "${value}"`);
            return true;
        }

        console.log(`Setting filter for ${columnName} to: "${value}"`);

        // [OPTIMIZATION] Use a single, smooth operation instead of multiple events
        filterInput.value = value;

        // [OPTIMIZATION] Use only the essential events, reduce redundancy
        const inputEvent = new Event('input', { bubbles: true });
        filterInput.dispatchEvent(inputEvent);

        // [OPTIMIZATION] Delayed change event to ensure proper registration
        setTimeout(() => {
            const changeEvent = new Event('change', { bubbles: true });
            filterInput.dispatchEvent(changeEvent);
            console.log(`Filter applied for ${columnName}: "${value}"`);
        }, 50); // Reduced delay from 100ms to 50ms

        // Save to persistent storage if requested (including empty values)
        if (shouldPersist) {
            setTimeout(async () => {
                const currentFilters = await loadFilterValues();
                currentFilters[columnName] = value;
                await saveFilterValues(currentFilters);
            }, 100); // Reduced delay from 200ms to 100ms
        }

        return true;
    }

    /**
     * [OPTIMIZED] Apply all saved filters with improved throttling
     */
    async function applySavedFilters() {
        // [OPTIMIZATION] Prevent concurrent filter applications
        if (state.isApplyingFilters) {
            console.log("Filter application already in progress, skipping...");
            return true;
        }

        // [OPTIMIZATION] Throttle filter applications
        const now = Date.now();
        if (now - state.lastFilterApplication < 1000) { // 1 second throttle
            console.log("Filter application throttled, skipping...");
            return true;
        }

        state.isApplyingFilters = true;
        state.lastFilterApplication = now;

        try {
            const savedFilters = await loadFilterValues();

            if (Object.keys(savedFilters).length === 0) {
                console.log("No saved filters to apply");
                return true;
            }

            console.log("Applying saved filters:", savedFilters);

            let allApplied = true;

            // [OPTIMIZATION] Apply filters sequentially with small delays to prevent flickering
            for (const [columnName, value] of Object.entries(savedFilters)) {
                const success = setColumnFilter(columnName, value, false);
                if (!success) {
                    allApplied = false;
                }
                // [OPTIMIZATION] Small delay between filter applications
                await new Promise(resolve => setTimeout(resolve, 25));
            }

            return allApplied;
        } finally {
            // [OPTIMIZATION] Reset the flag after a delay
            setTimeout(() => {
                state.isApplyingFilters = false;
            }, 500);
        }
    }

    /**
     * [NEW] Clear all saved filters
     */
    async function clearSavedFilters() {
        try {
            await GM_setValue(FILTER_PERSISTENCE_KEY, JSON.stringify({}));
            console.log("Saved filters cleared");
        } catch (e) {
            console.error("Error clearing saved filters:", e);
        }
    }

    //================================================================================
    // --- CORE LOGIC ---
    //================================================================================

    function setupNavigationShortcut() {
        const labOrdersLink = document.querySelector('span.csi-menu-text[title="Lab Orders"]')?.closest('a');

        if (!labOrdersLink || labOrdersLink.dataset.shortcutAttached === 'true') {
            return;
        }

        labOrdersLink.addEventListener('click', () => {
            setTimeout(() => {
                const labTestStatusTab = document.querySelector('a[href="#/lab-orders/lab-test-analyzer"]');
                if (labTestStatusTab) {
                    labTestStatusTab.click();
                }
            }, 100);
        });

        labOrdersLink.dataset.shortcutAttached = 'true';
    }

    /**
     * [NEW] Relocate Reset button to be inline with Reference Lab Orders toggle
     */
    function relocateResetButton() {
        if (state.resetButtonRelocated) return;

        const resetButton = document.querySelector(SELECTORS.resetButton);
        const referenceLabToggle = document.querySelector(SELECTORS.referenceLabToggle);

        if (!resetButton || !referenceLabToggle) {
            console.log("Reset button or Reference Lab toggle not found for relocation");
            return;
        }

        // Check if already relocated
        if (resetButton.classList.contains('relocated-reset-button')) {
            return;
        }

        console.log("Relocating Reset button to be inline with Reference Lab Orders toggle");

        // Create a container for the reset button and toggle
        const container = document.createElement('div');
        container.className = 'reset-button-container';

        // Clone the reset button and add styling
        const relocatedResetButton = resetButton.cloneNode(true);
        relocatedResetButton.classList.add('relocated-reset-button');

        // Insert the container before the reference lab toggle
        referenceLabToggle.parentNode.insertBefore(container, referenceLabToggle);

        // Move elements into the container
        container.appendChild(relocatedResetButton);
        container.appendChild(referenceLabToggle);

        // Remove the original reset button
        resetButton.remove();

        // Ensure the relocated button maintains its functionality
        relocatedResetButton.addEventListener('click', async () => {
            console.log("Relocated Reset button clicked");
            await clearSavedFilters();

            // Clear filters manually as fallback
            setColumnFilter('testStatus', '', false);
            setColumnFilter('sampleStatus', '', false);

            // Reset workbench selection
            const workbenchSelect = document.getElementById(SELECTORS.workbenchDropdown.substring(1));
            if (workbenchSelect) {
                workbenchSelect.selectedIndex = 0;
                workbenchSelect.dispatchEvent(new Event('change', { bubbles: true }));
            }

            // Reset status dropdown
            const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select');
            if (statusDropdown) {
                statusDropdown.selectedIndex = 0;
                statusDropdown.dispatchEvent(new Event('change', { bubbles: true }));
            }
        });

        state.resetButtonRelocated = true;
        console.log("Reset button successfully relocated");
    }

    function applyConditionalCellStyles() {
        const rows = document.querySelectorAll('.ag-row');

        for (const row of rows) {
            const clinicCell = row.querySelector('.ag-cell[col-id="clinic"]');
            const isEmergency = clinicCell && clinicCell.textContent.trim().toUpperCase() === 'EMERGENCY';

            const cellsInRow = row.querySelectorAll('.ag-cell');

            for (const cell of cellsInRow) {
                cell.style.backgroundColor = '';
                cell.style.color = '';
                cell.style.fontWeight = '';

                if (isEmergency) {
                    cell.style.backgroundColor = '#ffcccb';
                    cell.style.color = 'black';
                }

                const colId = cell.getAttribute('col-id');
                const text = cell.textContent.trim();

                if (colId === 'testStatus') {
                    switch (text) {
                        case 'Resulted':
                            cell.style.backgroundColor = '#ffb733';
                            cell.style.color = 'black';
                            break;
                        case 'Ordered':
                            cell.style.backgroundColor = 'yellow';
                            cell.style.color = 'black';
                            break;
                        case 'VerifiedLevel1':
                        case 'VerifiedLevel2':
                            cell.style.backgroundColor = 'lightgreen';
                            cell.style.color = 'black';
                            break;
                    }
                } else if (colId === 'sampleStatus') {
                    switch (text) {
                        case 'Received':
                            cell.style.backgroundColor = 'lightgreen';
                            cell.style.color = 'black';
                            cell.style.fontWeight = 'bold';
                            break;
                        case 'Rejected':
                            cell.style.backgroundColor = 'red';
                            cell.style.color = 'black';
                            cell.style.fontWeight = 'bold';
                            break;
                        case 'Collected':
                            cell.style.backgroundColor = 'orange';
                            cell.style.color = 'black';
                            cell.style.fontWeight = 'bold';
                            break;
                    }
                }
            }
        }
    }

    /**
     * [OPTIMIZED] Enhanced status dropdown listener with reduced delays
     */
    function addStatusDropdownListener() {
        const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select');

        if (!statusDropdown || statusDropdown.dataset.statusListenerAttached === 'true') {
            return;
        }

        statusDropdown.addEventListener('change', async (event) => {
            const selectedOption = event.target.options[event.target.selectedIndex];
            const selectedText = selectedOption.textContent.trim();

            // [OPTIMIZATION] Reduced timeout from 500ms to 200ms
            setTimeout(async () => {
                // Handle the "---Select---" option to trigger a reset
                if (selectedOption.value === '0') {
                    console.log("---Select--- chosen, clearing all filters and resetting.");

                    await clearSavedFilters();

                    let resetButton = document.querySelector(SELECTORS.resetButton) || document.querySelector('.relocated-reset-button');

                    if (!resetButton) {
                        console.log("Reset button not found by selector, trying fallback search...");
                        const allButtons = Array.from(document.querySelectorAll('button'));
                        resetButton = allButtons.find(btn => btn.textContent.trim().toLowerCase() === 'reset');
                    }

                    if (resetButton) {
                        console.log("Simulating click on Reset button.");
                        resetButton.click();
                    } else {
                        console.warn("Could not find Reset button. Manually clearing filters as a final fallback.");
                        setColumnFilter('testStatus', '', false);
                        setColumnFilter('sampleStatus', '', false);
                    }
                    return;
                }

                // Handle other statuses and save them
                if (selectedText === "Sample Rejected") {
                    setColumnFilter('sampleStatus', 'Rejected');
                    setColumnFilter('testStatus', '');
                    console.log("Sample Rejected selected: sampleStatus='Rejected', testStatus='' (cleared)");
                } else if (selectedText === "Sample Refused") {
                    setColumnFilter('sampleStatus', 'Refused');
                    setColumnFilter('testStatus', '');
                    console.log("Sample Refused selected: sampleStatus='Refused', testStatus='' (cleared)");
                } else {
                    const statusMap = {
                        "Verified 1": "VerifiedLevel1",
                        "Verified 2": "VerifiedLevel2",
                        "Cancelled": "Cancelled",
                    };
                    const filterText = statusMap[selectedText] || selectedText;
                    setColumnFilter('testStatus', filterText);
                    setColumnFilter('sampleStatus', 'Received');
                    console.log(`Status selected: testStatus='${filterText}', sampleStatus='Received'`);
                }

            }, 200); // Reduced from 500ms
        });

        statusDropdown.dataset.statusListenerAttached = 'true';
    }

    /**
     * [FIXED] Update button selection visual state
     */
    function updateButtonSelection(selectedId) {
        state.selectedWorkbenchId = selectedId;
        const buttons = document.querySelectorAll('.filter-btn-group .btn');

        buttons.forEach(btn => {
            const btnId = btn.dataset.workbenchId;
            if (btnId === selectedId) {
                btn.classList.add('selected');
            } else {
                btn.classList.remove('selected');
            }
        });
    }

    /**
     * [FIXED] Enhanced insertFilterButtons with proper selection state management
     */
    function insertFilterButtons() {
        if (document.querySelector(SELECTORS.buttonGroup)) return;
        const target = document.querySelector(SELECTORS.buttonGroupContainer);
        const select = document.getElementById(SELECTORS.workbenchDropdown.substring(1));

        if (!target || !select || select.options.length <= 1) return;

        const workbenches = Array.from(select.options).reduce((acc, option) => {
            const id = option.value?.trim();
            let name = option.textContent?.trim();
            if (id && name) {
                if (name.toLowerCase().includes('---select---') || ['select', 'all'].includes(name.toLowerCase())) {
                    name = 'ALL WORK BENCHES';
                }
                acc[name] = id;
            }
            return acc;
        }, {});

        if (Object.keys(workbenches).length <= 1) return;

        const group = document.createElement('div');
        group.className = 'filter-btn-group';
        let colorIndex = 0;

        for (const [name, id] of Object.entries(workbenches)) {
            const btn = document.createElement('button');
            btn.className = `btn btn-color-${colorIndex++ % 10}`;
            btn.dataset.workbenchId = id; // [FIXED] Store workbench ID for selection tracking

            const sessionKey = `workbench_status_${id || 'all'}`;
            let currentStatus = sessionStorage.getItem(sessionKey) || 'Ordered';
            btn.textContent = `${name} (${currentStatus})`;

            // [FIXED] Set initial selection state based on current dropdown value
            if (select.value === id) {
                btn.classList.add('selected');
                state.selectedWorkbenchId = id;
            }

            btn.addEventListener('click', async () => {
                currentStatus = (currentStatus === 'Ordered') ? 'Resulted' : 'Ordered';
                sessionStorage.setItem(sessionKey, currentStatus);
                btn.textContent = `${name} (${currentStatus})`;

                // [FIXED] Update visual selection state
                updateButtonSelection(id);

                GM_setValue(WORKBENCH_SELECTION_KEY, JSON.stringify({ id, status: currentStatus }));

                select.value = id;
                select.dispatchEvent(new Event('change', { bubbles: true }));

                const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select');
                if (statusDropdown) {
                    const optionToSelect = Array.from(statusDropdown.options).find(opt => opt.textContent.trim() === currentStatus);
                    if (optionToSelect && statusDropdown.value !== optionToSelect.value) {
                        statusDropdown.value = optionToSelect.value;
                        statusDropdown.dispatchEvent(new Event('change', { bubbles: true }));
                    }
                }

                // [OPTIMIZATION] Reduced timeout from 200ms to 100ms
                setTimeout(() => {
                    setColumnFilter('sampleStatus', 'Received');
                }, 100);
            });
            group.appendChild(btn);
        }
        target.parentNode.insertBefore(group, target.nextSibling);

        // [FIXED] Initialize selection state if no button is currently selected
        if (!state.selectedWorkbenchId && select.value) {
            updateButtonSelection(select.value);
        }
    }

    async function applyPersistentWorkbenchFilter() {
        if (state.hasAppliedInitialFilters) return;
        const workbenchSelect = document.getElementById(SELECTORS.workbenchDropdown.substring(1));
        const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select');
        if (!workbenchSelect || !statusDropdown) return;

        try {
            const savedWorkbenchJSON = await GM_getValue(WORKBENCH_SELECTION_KEY, null);

            if (savedWorkbenchJSON) {
                const savedWorkbench = JSON.parse(savedWorkbenchJSON);
                if (savedWorkbench?.id && savedWorkbench?.status) {
                    workbenchSelect.value = savedWorkbench.id;
                    const optionToSelect = Array.from(statusDropdown.options).find(opt => opt.textContent.trim() === savedWorkbench.status);
                    if (optionToSelect) {
                        statusDropdown.value = optionToSelect.value;
                    }
                    workbenchSelect.dispatchEvent(new Event('change', { bubbles: true }));
                    statusDropdown.dispatchEvent(new Event('change', { bubbles: true }));

                    // [FIXED] Update button selection state after applying persistent filter
                    updateButtonSelection(savedWorkbench.id);

                    // [OPTIMIZATION] Reduced timeout from 200ms to 100ms
                    setTimeout(() => {
                        setColumnFilter('sampleStatus', 'Received');
                    }, 100);
                }
            }
        } catch (e) {
            console.error("Error applying persistent workbench filter:", e);
        }
        state.hasAppliedInitialFilters = true;
    }

    /**
     * [OPTIMIZED] Enhanced function to apply saved filters with improved retry mechanism
     */
    async function applyFiltersWithRetry() {
        const headerViewport = document.querySelector(SELECTORS.agHeaderViewport);
        if (!headerViewport) {
            return;
        }

        const success = await applySavedFilters();

        if (!success && state.filterRetryCount < state.maxFilterRetries) {
            state.filterRetryCount++;
            console.log(`Filter application failed, retrying... (${state.filterRetryCount}/${state.maxFilterRetries})`);
            // [OPTIMIZATION] Increased retry delay from 1000ms to 1500ms to reduce rapid retries
            setTimeout(() => applyFiltersWithRetry(), 1500);
        } else if (success) {
            console.log("All saved filters applied successfully");
            state.filterRetryCount = 0;
        } else {
            console.warn("Max filter retry attempts reached");
            state.filterRetryCount = 0;
        }
    }

    /**
     * [OPTIMIZED] Throttled master observer callback to reduce flickering
     */
    async function masterObserverCallback() {
        // [OPTIMIZATION] Throttle observer callbacks to prevent excessive execution
        if (state.observerThrottle) {
            clearTimeout(state.observerThrottle);
        }

        state.observerThrottle = setTimeout(async () => {
            if (location.href !== state.lastUrl) {
                state.lastUrl = location.href;
                state.hasAppliedInitialFilters = false;
                state.filterRetryCount = 0;
                state.isApplyingFilters = false; // Reset filter application flag
                state.selectedWorkbenchId = null; // [FIXED] Reset selection state on URL change
                state.resetButtonRelocated = false; // [NEW] Reset relocation state on URL change
            }

            setupNavigationShortcut();

            if (!location.href.includes('/lab-orders/lab-test-analyzer')) {
                return;
            }

            // [NEW] Relocate reset button
            relocateResetButton();

            insertFilterButtons();
            addStatusDropdownListener();
            await applyPersistentWorkbenchFilter();

            // [OPTIMIZATION] Apply saved filters with improved throttling
            await applyFiltersWithRetry();

            applyConditionalCellStyles();
        }, 100); // 100ms throttle for observer callbacks
    }

    // [OPTIMIZATION] Use throttled observer with reduced sensitivity
    const observer = new MutationObserver(masterObserverCallback);
    observer.observe(document.body, {
        childList: true,
        subtree: true,
        // [OPTIMIZATION] Removed attributes and characterData observation to reduce noise
    });

    masterObserverCallback();
})();