您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds shortcuts, persistent filters, and a location logger. Implements an "Enforcement Loop" to win filter race conditions and enhances button UI. Makes the barcode popup draggable.
// ==UserScript== // @name KAAUH Lab - Robust Buttons & Highlighting // @namespace Violentmonkey Scripts // @version 2.9.5 // @description Adds shortcuts, persistent filters, and a location logger. Implements an "Enforcement Loop" to win filter race conditions and enhances button UI. Makes the barcode popup draggable. // @match *://his.kaauh.org/lab/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // @author Hamad Al-Shegifi // ==/UserScript== (async function () { // The script must be async to use GM_getValue 'use strict'; console.log("KAAUH Lab - Robust Buttons & Highlighting Script v2.9.5 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, isApplyingFilters: false, lastFilterApplication: 0, observerThrottle: null, selectedWorkbenchId: null, resetButtonRelocated: false, isProcessingReset: false, }; let scrollListenerAttached = false; // Flag to prevent attaching multiple scroll listeners 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', agBodyViewport: '.ag-body-viewport', // Added selector for the grid's scrollable area resetButton: 'button[translateid="lab-order-list.Reset"]', referenceLabToggle: '.nova-toggle', }; //================================================================================ // --- STYLE INJECTION (GM_addStyle) --- //================================================================================ GM_addStyle(` /* Button container styles */ .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; } /* General button styles */ .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; } /* Rule 1: All other buttons are 10% opaque */ .filter-btn-group .btn:not(.selected) { opacity: 0.1 !important; } /* Rule 2: The selected button is 100% opaque */ .filter-btn-group .btn.selected { opacity: 1 !important; 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; } /* Rule 3: ANY button on hover is 100% opaque */ .filter-btn-group .btn:hover { opacity: 1 !important; transform: scale(1.05); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } /* Container for the relocated reset button */ .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; } /* Color definitions for buttons */ ${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')} /* Row highlighting on hover */ .ag-row:hover .ag-cell { background-color: lightblue !important; } `); //================================================================================ // --- ENHANCED FILTER PERSISTENCE FUNCTIONS --- //================================================================================ 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); } } 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 {}; } } 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); } } /** * [NEW] Aggressively clears an input field, fighting race conditions. * @param {HTMLInputElement} inputElement The input to clear. */ function robustClearInput(inputElement) { if (!inputElement || inputElement.value === '') return; inputElement.value = ''; inputElement.dispatchEvent(new Event('input', { bubbles: true })); inputElement.dispatchEvent(new Event('change', { bubbles: true })); inputElement.dispatchEvent(new Event('blur', { bubbles: true })); // Add blur for good measure // Start a short enforcement loop to ensure it stays cleared let cycles = 0; const maxCycles = 5; // Enforce for 500ms const enforcementInterval = setInterval(() => { if (!document.body.contains(inputElement) || cycles >= maxCycles) { clearInterval(enforcementInterval); return; } if (inputElement.value !== '') { console.warn(`Input value was reverted! Re-clearing.`); inputElement.value = ''; inputElement.dispatchEvent(new Event('input', { bubbles: true })); inputElement.dispatchEvent(new Event('change', { bubbles: true })); } cycles++; }, 100); } /** * Reusable function to perform a full reset of all filters and script states. */ async function performFullReset() { if (state.isProcessingReset) { return; } state.isProcessingReset = true; console.log("Starting full reset process..."); try { // 1. Clear all text filters in the table header robustly document.querySelectorAll(SELECTORS.agHeaderViewport + ' .ag-floating-filter-input').forEach(robustClearInput); // 2. Clear any filters saved in storage by this script await clearSavedFilters(); // 3. Reset Workbench dropdown to its first option ("---select---") const workbenchSelect = document.getElementById(SELECTORS.workbenchDropdown.substring(1)); if (workbenchSelect) { await setAndVerifyDropdown(workbenchSelect, workbenchSelect.options[0].value, 'Workbench'); } // 4. Reset Status dropdown to its first option ("---select---") const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select'); if (statusDropdown) { await setAndVerifyDropdown(statusDropdown, statusDropdown.options[0].value, 'Status'); } // 5. Reset script-specific state state.selectedWorkbenchId = null; await GM_setValue(WORKBENCH_SELECTION_KEY, JSON.stringify({})); document.querySelectorAll('.filter-btn-group .btn').forEach(btn => btn.classList.remove('selected')); console.log("Full reset process completed successfully."); } catch (error) { console.error("Error during reset process:", error); } finally { setTimeout(() => { state.isProcessingReset = false; }, 1000); // Cooldown period } } //================================================================================ // --- BARCODE POPUP & SHORTCUTS MODULE --- //================================================================================ (function() { 'use strict'; // --- Configuration --- const TARGET_SEARCH_PLACEHOLDER = 'Search by MRN / NationalId & IqamaId'; const TARGET_BARCODE_COLUMN_HEADER = 'Barcode'; // <--- IMPORTANT: This should match the column title exactly. const BARCODE_POPUP_POSITION_KEY = 'kaauh_barcode_popup_position'; // Key for saving position // --- Core Functions --- function clearAllFilters() { console.log("Clearing all filters."); const clearAndNotify = (inputElement) => { if (inputElement && inputElement.value !== '') { inputElement.value = ''; const updateEvent = new Event('input', { bubbles: true }); inputElement.dispatchEvent(updateEvent); } }; const mainSearchInput = document.querySelector(`input[placeholder="${TARGET_SEARCH_PLACEHOLDER}"]`); clearAndNotify(mainSearchInput); const agGridFilters = document.querySelectorAll('input.ag-floating-filter-input'); agGridFilters.forEach(clearAndNotify); } // --- Manual 'Delete' key listener --- document.addEventListener('keydown', (event) => { // Do not trigger if user is typing in the popup if (document.activeElement.id === 'barcode-popup-input') return; if (event.key === 'Delete') { console.log("Manual clear triggered by 'Delete' key."); clearAllFilters(); } }); // --- Barcode Popup Functionality --- function findBarcodeFilterInput() { // 1. Find the header cell containing the title "Barcode" const allTitleSpans = document.querySelectorAll('.ag-header-row[aria-rowindex="1"] .ag-header-cell-text'); let titleHeaderCell = null; allTitleSpans.forEach(span => { if (span.textContent.trim() === TARGET_BARCODE_COLUMN_HEADER) { titleHeaderCell = span.closest('.ag-header-cell'); } }); if (!titleHeaderCell) { console.error(`Could not find a column header with the text: "${TARGET_BARCODE_COLUMN_HEADER}"`); return null; } // 2. Get the horizontal position (left style) of that header cell const targetLeftPosition = titleHeaderCell.style.left; if (!targetLeftPosition) { console.error('Found the barcode header, but it has no "left" style property to match with.'); return null; } // 3. Find the corresponding filter cell in the second header row that has the same horizontal position const allFilterCells = document.querySelectorAll('.ag-header-row[aria-rowindex="2"] .ag-header-cell'); let filterCell = null; allFilterCells.forEach(cell => { if (cell.style.left === targetLeftPosition) { filterCell = cell; } }); if (!filterCell) { console.error(`Found barcode header at left=${targetLeftPosition}, but could not find a filter cell at the same position.`); return null; } // 4. Find the input field within that specific filter cell const filterInput = filterCell.querySelector('input.ag-floating-filter-input'); if (!filterInput) { console.error('Found the correct filter cell, but no input field was inside it.'); return null; } console.log(`Successfully found barcode filter input at left: ${targetLeftPosition}`); return filterInput; } async function createBarcodePopup() { // Check if popup already exists and return it if it does let popupContainer = document.getElementById('barcode-popup-container'); if (popupContainer) return popupContainer; const savedPositionJSON = await GM_getValue(BARCODE_POPUP_POSITION_KEY, null); const savedPosition = savedPositionJSON ? JSON.parse(savedPositionJSON) : null; popupContainer = document.createElement('div'); popupContainer.id = 'barcode-popup-container'; Object.assign(popupContainer.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.5)', display: 'none', zIndex: '10000' }); const popupBox = document.createElement('div'); const initialStyle = { position: 'absolute', background: '#fff', padding: '25px', borderRadius: '8px', boxShadow: '0 5px 15px rgba(0,0,0,0.3)', textAlign: 'center', width: '350px' }; if (savedPosition && savedPosition.top && savedPosition.left) { initialStyle.top = savedPosition.top; initialStyle.left = savedPosition.left; } else { initialStyle.top = '50%'; initialStyle.left = '50%'; initialStyle.transform = 'translate(-50%, -50%)'; } Object.assign(popupBox.style, initialStyle); const popupTitle = document.createElement('h3'); popupTitle.textContent = 'Rapid Barcode Entry'; Object.assign(popupTitle.style, { margin: '0 0 15px 0', color: '#333', cursor: 'move', userSelect: 'none' }); const popupInput = document.createElement('input'); popupInput.id = 'barcode-popup-input'; popupInput.type = 'text'; popupInput.placeholder = 'Scan or type barcode and press Enter'; Object.assign(popupInput.style, { width: '100%', padding: '10px', fontSize: '16px', border: '2px solid #ccc', borderRadius: '4px' }); popupBox.appendChild(popupTitle); popupBox.appendChild(popupInput); popupContainer.appendChild(popupBox); document.body.appendChild(popupContainer); // --- Drag and Drop Logic --- popupTitle.addEventListener('mousedown', (e) => { e.preventDefault(); if (popupBox.style.transform && popupBox.style.transform !== 'none') { const rect = popupBox.getBoundingClientRect(); popupBox.style.left = `${rect.left}px`; popupBox.style.top = `${rect.top}px`; popupBox.style.transform = 'none'; } const initialLeft = popupBox.offsetLeft; const initialTop = popupBox.offsetTop; const startX = e.clientX; const startY = e.clientY; const onMouseMove = (moveEvent) => { const dx = moveEvent.clientX - startX; const dy = moveEvent.clientY - startY; popupBox.style.left = `${initialLeft + dx}px`; popupBox.style.top = `${initialTop + dy}px`; }; const onMouseUp = async () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); const newPosition = { top: popupBox.style.top, left: popupBox.style.left }; await GM_setValue(BARCODE_POPUP_POSITION_KEY, JSON.stringify(newPosition)); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); popupContainer.addEventListener('click', (e) => { if (e.target === popupContainer) { popupContainer.style.display = 'none'; } }); popupInput.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); const barcodeValue = popupInput.value.trim(); if (!barcodeValue) return; const barcodeFilterInput = findBarcodeFilterInput(); if (barcodeFilterInput) { barcodeFilterInput.value = barcodeValue; barcodeFilterInput.dispatchEvent(new Event('input', { bubbles: true })); popupInput.value = ''; // Clear for next scan } else { console.error(`Critical Error: Could not find the barcode filter on the page. Make sure a column is named "${TARGET_BARCODE_COLUMN_HEADER}".`); popupInput.style.borderColor = 'red'; setTimeout(() => { popupInput.style.borderColor = '#ccc'; }, 2000); } } }); return popupContainer; } // --- UI Setup --- async function addControls(container) { // Create the popup on first run, but don't show it. const popup = await createBarcodePopup(); // Add the Barcode Entry Button if it doesn't exist if (!document.getElementById('barcode-entry-btn')) { const barcodeButton = document.createElement('button'); barcodeButton.id = 'barcode-entry-btn'; barcodeButton.textContent = 'Barcode Entry'; Object.assign(barcodeButton.style, { backgroundColor: '#5bc0de', transition: 'background-color 0.3s', padding: '6px 12px', fontSize: '13px', fontWeight: 'bold', borderRadius: '6px', color: '#ffffff', border: '1px solid #285e79', cursor: 'pointer', marginLeft: '8px' }); // Now clears filters before showing the popup barcodeButton.addEventListener('click', async () => { await performFullReset(); popup.style.display = 'block'; document.getElementById('barcode-popup-input').focus(); }); container.appendChild(barcodeButton); console.log('Barcode Entry button added.'); } } // --- Persistent Injection Logic --- async function placeButton() { const buttonContainer = document.querySelector('.reset-button-container'); if (buttonContainer) { await addControls(buttonContainer); } } // Use a MutationObserver as the primary, efficient method. const observer = new MutationObserver(placeButton); observer.observe(document.body, { childList: true, subtree: true }); // Use a setInterval as a persistent backup setInterval(placeButton, 1000); })(); //================================================================================ // --- COLUMN FILTERING & PERSISTENCE --- //================================================================================ function setColumnFilter(columnName, value, shouldPersist = true) { const headerViewport = document.querySelector(SELECTORS.agHeaderViewport); if (!headerViewport) { 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) { return false; } const filterInput = headerViewport.querySelector(`.ag-header-row[aria-rowindex="2"]`)?.children[columnIndex]?.querySelector('.ag-floating-filter-input'); if (!filterInput) { return false; } if (filterInput.value === value) { return true; } console.log(`Starting aggressive filter set for ${columnName} to "${value}".`); filterInput.value = value; filterInput.dispatchEvent(new Event('input', { bubbles: true })); filterInput.dispatchEvent(new Event('change', { bubbles: true })); let enforcementCycles = 0; const maxCycles = 10; const enforcementInterval = setInterval(() => { if (!document.body.contains(filterInput)) { clearInterval(enforcementInterval); return; } if (filterInput.value !== value) { console.warn(`Filter for '${columnName}' was reverted! Re-enforcing value: "${value}".`); filterInput.value = value; filterInput.dispatchEvent(new Event('input', { bubbles: true })); filterInput.dispatchEvent(new Event('change', { bubbles: true })); } enforcementCycles++; if (enforcementCycles >= maxCycles) { clearInterval(enforcementInterval); } }, 200); if (shouldPersist) { setTimeout(async () => { const currentFilters = await loadFilterValues(); currentFilters[columnName] = value; await saveFilterValues(currentFilters); }, 250); } return true; } async function applySavedFilters() { if (state.isApplyingFilters) { return true; } const now = Date.now(); if (now - state.lastFilterApplication < 1000) { return true; } state.isApplyingFilters = true; state.lastFilterApplication = now; try { const savedFilters = await loadFilterValues(); if (Object.keys(savedFilters).length === 0) { return true; } console.log("Applying saved filters:", savedFilters); for (const [columnName, value] of Object.entries(savedFilters)) { setColumnFilter(columnName, value, false); await new Promise(resolve => setTimeout(resolve, 250)); } return true; } finally { setTimeout(() => { state.isApplyingFilters = false; }, 500); } } //================================================================================ // --- [IMPROVED] CORE LOGIC & HELPER FUNCTIONS --- //================================================================================ /** * [IMPROVED FOR ROBUSTNESS] * Sets a dropdown's value and starts an "enforcement loop" to win race conditions. */ function setAndVerifyDropdown(dropdown, targetValue, description, duration = 1000) { return new Promise((resolve) => { if (!dropdown) { console.error(`[Enforcer] Cannot set dropdown: ${description} dropdown not found.`); return resolve(false); } if (dropdown.value === targetValue) { console.log(`[Enforcer] ${description} dropdown already set to "${targetValue}".`); return resolve(true); } console.log(`[Enforcer] Starting enforcement for ${description} dropdown. Target: "${targetValue}".`); // Initial set and event dispatch dropdown.value = targetValue; dropdown.dispatchEvent(new Event('input', { bubbles: true })); dropdown.dispatchEvent(new Event('change', { bubbles: true })); let enforcementCycles = 0; const intervalDuration = 100; const maxCycles = duration / intervalDuration; const enforcementInterval = setInterval(() => { if (!document.body.contains(dropdown)) { clearInterval(enforcementInterval); console.warn(`[Enforcer] ${description} dropdown was removed from the page during enforcement.`); return resolve(false); } if (dropdown.value !== targetValue) { console.warn(`[Enforcer] ${description} value was reverted! Re-applying "${targetValue}".`); dropdown.value = targetValue; dropdown.dispatchEvent(new Event('input', { bubbles: true })); dropdown.dispatchEvent(new Event('change', { bubbles: true })); } enforcementCycles++; if (enforcementCycles >= maxCycles) { clearInterval(enforcementInterval); const success = dropdown.value === targetValue; if (success) { console.log(`[Enforcer] Successfully set and verified ${description} to "${targetValue}".`); } else { console.error(`[Enforcer] FAILED to set ${description} to "${targetValue}". Final value: "${dropdown.value}".`); } return resolve(success); } }, intervalDuration); }); } //================================================================================ // --- LOCATION LOGGER MODULE --- //================================================================================ (function() { 'use strict'; /** * Extracts the patient's location. */ function getPatientLocation() { // First, try the most specific selector (often includes unit/ward) let locElement = document.querySelector('div.patient-info span[title*="UNIT/"]'); // If that fails, fall back to the more general selector if (!locElement) { locElement = document.querySelector('div.patient-info span[title]'); } if (locElement) { // The 'title' attribute is the most reliable source const titleAttr = locElement.getAttribute('title'); if (titleAttr && titleAttr.trim() !== '') { // Return the cleaned-up title attribute return titleAttr.trim().replace(/\s+/g, ' '); } else { // If title is empty, fall back to the element's text content const clone = locElement.cloneNode(true); // Remove the 'h6' label (e.g., "Location") to get only the value const h6InLoc = clone.querySelector('h6'); if (h6InLoc) h6InLoc.remove(); let cleanedText = clone.textContent.trim().replace(/\s+/g, ' '); // Further clean up common prefixes like "Bed" cleanedText = cleanedText.replace(/^Bed\s*/i, '').trim(); return cleanedText || null; // Return the text, or null if it's empty } } // Return null if no location element was found return null; } /** * Creates and inserts the location element into the barcode display box. */ function addLocationToHeader() { const mrnDisplay = document.getElementById('mrn-display'); const barcodeBox = document.getElementById('barcode-display-box'); // Exit if the necessary elements aren't on the page yet, // or if we have already added the location display. if (!mrnDisplay || !barcodeBox || document.getElementById('suite-location-display')) { return; } const location = getPatientLocation(); if (location) { const locationDiv = document.createElement('div'); locationDiv.id = 'suite-location-display'; locationDiv.textContent = `${location}`; // Apply styles to make it a distinct blue badge Object.assign(locationDiv.style, { fontWeight: 'bold', fontSize: '18px', color: '#ffffff', backgroundColor: '#0000ff', borderRadius: '4px', padding: '4px 10px', marginLeft: '12px', display: 'flex', alignItems: 'center' }); // Insert the new location element right after the MRN display mrnDisplay.after(locationDiv); } } // Use a MutationObserver to watch for when the header bar is added to the page. const observer = new MutationObserver(() => { addLocationToHeader(); }); // Start observing the entire document for changes to its structure. observer.observe(document.body, { childList: true, subtree: true }); // Also run the function once on script start. addLocationToHeader(); })(); //================================================================================ // --- HIDE INFO PANEL MODULE --- //================================================================================ (function() { 'use strict'; // --- Configuration --- const elementToHideSelector = '.lo-view-detail-top'; const buttonContainerSelector = '.btn-area'; // The container where the button will be placed // --- Icons (SVG) --- const eyeIcon = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"> <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/> <circle cx="12" cy="12" r="3"/> </svg>`; const eyeOffIcon = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"> <path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/> <path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/> <path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/> <line x1="2" x2="22" y1="2" y2="22"/> </svg>`; // --- Styling --- GM_addStyle(` /* This class is used to hide the patient info panel */ .gm-hide-element { display: none !important; } /* MODIFIED: Force the text and icon color of the toggle button to be white */ #gm-toggle-button { color: white !important; } `); // --- Main Logic --- /** * Updates the visibility of the target element based on the stored preference. */ const updateElementVisibility = (element) => { if (!element) return; const isHidden = GM_getValue('elementHidden', false); if (isHidden) { element.classList.add('gm-hide-element'); } else { element.classList.remove('gm-hide-element'); } }; /** * Creates and injects the toggle button into the button container. */ const createToggleButton = (container) => { const toggleButton = document.createElement('button'); toggleButton.id = 'gm-toggle-button'; // Add classes to match the other buttons on the page. toggleButton.className = 'btn btn-color-11'; // Function to update the button's icon and title const updateButtonState = () => { const isHidden = GM_getValue('elementHidden', false); if (isHidden) { toggleButton.innerHTML = eyeOffIcon + ' Show Info'; toggleButton.setAttribute('title', 'Show patient info header'); } else { toggleButton.innerHTML = eyeIcon + ' Hide Info'; toggleButton.setAttribute('title', 'Hide patient info header'); } }; // Add the click event listener toggleButton.addEventListener('click', (e) => { e.preventDefault(); // Prevent any default button action const isCurrentlyHidden = GM_getValue('elementHidden', false); GM_setValue('elementHidden', !isCurrentlyHidden); // Save the new state updateButtonState(); // Update the button's look // Immediately apply the change to the element if it exists const elementToHide = document.querySelector(elementToHideSelector); if (elementToHide) { updateElementVisibility(elementToHide); } }); // Create a wrapper div to match the structure of other buttons const buttonWrapper = document.createElement('div'); buttonWrapper.className = 'btn-group mr-1'; buttonWrapper.appendChild(toggleButton); // Insert the button at the beginning of the container. container.prepend(buttonWrapper); updateButtonState(); // Set the initial icon and text }; // Use a MutationObserver to watch for the button container and the element to hide const observer = new MutationObserver(() => { // --- Handle Button Creation --- const buttonContainer = document.querySelector(buttonContainerSelector); if (buttonContainer && !document.getElementById('gm-toggle-button')) { createToggleButton(buttonContainer); } // --- Handle Element Visibility --- const targetElement = document.querySelector(elementToHideSelector); if (targetElement) { updateElementVisibility(targetElement); } }); // Start observing the entire page for changes. observer.observe(document.documentElement, { childList: true, subtree: true }); })(); //================================================================================ // --- UI SETUP, STYLING, & MAIN OBSERVER --- //================================================================================ 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'; } function relocateResetButton() { if (state.resetButtonRelocated) return; const resetButton = document.querySelector(SELECTORS.resetButton); const referenceLabToggle = document.querySelector(SELECTORS.referenceLabToggle); if (!resetButton || !referenceLabToggle || resetButton.classList.contains('relocated-reset-button')) { return; } console.log("Relocating Reset button..."); const container = document.createElement('div'); container.className = 'reset-button-container'; const relocatedResetButton = resetButton.cloneNode(true); relocatedResetButton.classList.add('relocated-reset-button'); referenceLabToggle.parentNode.insertBefore(container, referenceLabToggle); container.appendChild(relocatedResetButton); container.appendChild(referenceLabToggle); resetButton.remove(); // Now uses the reusable performFullReset function relocatedResetButton.addEventListener('click', async (event) => { event.preventDefault(); event.stopPropagation(); await performFullReset(); }); state.resetButtonRelocated = true; } function applyConditionalCellStyles() { const rows = document.querySelectorAll('.ag-center-cols-container .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; } } } } } /** * [FIX] Attaches a scroll listener to the grid to re-apply styles in real-time. */ function setupGridScrollListener() { if (scrollListenerAttached) return; const gridViewport = document.querySelector(SELECTORS.agBodyViewport); if (gridViewport) { let scrollTimeout; gridViewport.addEventListener('scroll', () => { // Use requestAnimationFrame to throttle the style function for smooth scrolling if (scrollTimeout) { window.cancelAnimationFrame(scrollTimeout); } scrollTimeout = window.requestAnimationFrame(() => { applyConditionalCellStyles(); }); }, { passive: true }); // Use passive listener for better scroll performance scrollListenerAttached = true; console.log("Grid scroll listener attached for live cell styling."); } } function addStatusDropdownListener() { const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select'); if (!statusDropdown || statusDropdown.dataset.statusListenerAttached === 'true') { return; } statusDropdown.addEventListener('change', async (event) => { try { const selectedOption = event.target.options[event.target.selectedIndex]; const selectedText = selectedOption.textContent.trim(); if (selectedOption.value === '0' || selectedText.toLowerCase().includes('select')) { if (state.isProcessingReset) return; const relocatedResetButton = document.querySelector('.relocated-reset-button'); if (relocatedResetButton) relocatedResetButton.click(); return; } setTimeout(async () => { if (selectedText === "Sample Rejected") { setColumnFilter('sampleStatus', 'Rejected'); setColumnFilter('testStatus', ''); } else if (selectedText === "Sample Refused") { setColumnFilter('sampleStatus', 'Refused'); setColumnFilter('testStatus', ''); } else { const statusMap = { "Verified 1": "VerifiedLevel1", "Verified 2": "VerifiedLevel2", "Cancelled": "Cancelled" }; const filterText = statusMap[selectedText] || selectedText; setColumnFilter('testStatus', filterText); setColumnFilter('sampleStatus', 'Received'); } }, 100); } catch (error) { console.error("Error in status dropdown listener:", error); } }); statusDropdown.dataset.statusListenerAttached = 'true'; } function updateButtonSelection(selectedId) { state.selectedWorkbenchId = selectedId; document.querySelectorAll('.filter-btn-group .btn').forEach(btn => { btn.classList.toggle('selected', btn.dataset.workbenchId === selectedId); }); } 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++ % 20}`; btn.dataset.workbenchId = id; const sessionKey = `workbench_status_${id || 'all'}`; let currentStatus = sessionStorage.getItem(sessionKey) || 'Ordered'; btn.textContent = `${name} (${currentStatus})`; if (select.value === id) { btn.classList.add('selected'); state.selectedWorkbenchId = id; } // Button click now uses the robust dropdown setter btn.addEventListener('click', async () => { currentStatus = (currentStatus === 'Ordered') ? 'Resulted' : 'Ordered'; sessionStorage.setItem(sessionKey, currentStatus); btn.textContent = `${name} (${currentStatus})`; updateButtonSelection(id); GM_setValue(WORKBENCH_SELECTION_KEY, JSON.stringify({ id, status: currentStatus })); const workbenchSelect = document.getElementById(SELECTORS.workbenchDropdown.substring(1)); await setAndVerifyDropdown(workbenchSelect, id, 'Workbench'); const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select'); if (statusDropdown) { const optionToSelect = Array.from(statusDropdown.options).find(opt => opt.textContent.trim() === currentStatus); if (optionToSelect) { await setAndVerifyDropdown(statusDropdown, optionToSelect.value, 'Status'); } } }); group.appendChild(btn); } target.parentNode.insertBefore(group, target.nextSibling); if (!state.selectedWorkbenchId && select.value) { updateButtonSelection(select.value); } } async function applyPersistentWorkbenchFilter() { 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) { console.log("Applying persistent workbench filter:", savedWorkbench); // Set workbench robustly const workbenchSuccess = await setAndVerifyDropdown(workbenchSelect, savedWorkbench.id, 'Workbench'); // Set status robustly const optionToSelect = Array.from(statusDropdown.options).find(opt => opt.textContent.trim() === savedWorkbench.status); let statusSuccess = false; if (optionToSelect) { statusSuccess = await setAndVerifyDropdown(statusDropdown, optionToSelect.value, 'Status'); } else { console.warn(`Could not find status option for "${savedWorkbench.status}"`); } // Only update the visual button state if both changes were successful if (workbenchSuccess && statusSuccess) { updateButtonSelection(savedWorkbench.id); } } } } catch (e) { console.error("Error applying persistent workbench filter:", e); } } 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})`); setTimeout(() => applyFiltersWithRetry(), 1500); } else if (!success) { console.warn("Max filter retry attempts reached"); } } async function masterObserverCallback() { if (state.observerThrottle) clearTimeout(state.observerThrottle); state.observerThrottle = setTimeout(async () => { if (location.href !== state.lastUrl) { state.lastUrl = location.href; Object.assign(state, { hasAppliedInitialFilters: false, filterRetryCount: 0, isApplyingFilters: false, selectedWorkbenchId: null, resetButtonRelocated: false, isProcessingReset: false }); scrollListenerAttached = false; // Reset listener flag on navigation } setupNavigationShortcut(); if (!location.href.includes('/lab-orders/lab-test-analyzer')) return; relocateResetButton(); insertFilterButtons(); addStatusDropdownListener(); setupGridScrollListener(); // Attach the scroll listener for live styling if (!state.hasAppliedInitialFilters) { console.log("Running one-time filter application..."); await applyPersistentWorkbenchFilter(); // This is now robust await applyFiltersWithRetry(); state.hasAppliedInitialFilters = true; console.log("One-time filter application complete."); } applyConditionalCellStyles(); // Apply styles on initial load and other changes }, 100); } const observer = new MutationObserver(masterObserverCallback); observer.observe(document.body, { childList: true, subtree: true }); masterObserverCallback(); })();