// ==UserScript==
// @name KAAUH Lab - Robust Buttons & Highlighting
// @namespace Violentmonkey Scripts
// @version 2.7.2
// @description Adds shortcuts and persistent filters. Implements an aggressive "Enforcement Loop" to win filter race conditions and enhances button UI.
// @match *://his.kaauh.org/lab/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// @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 v2.7.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,
isApplyingFilters: false,
lastFilterApplication: 0,
observerThrottle: null,
selectedWorkbenchId: null,
resetButtonRelocated: false,
isProcessingReset: false,
};
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(`
/* 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 {};
}
}
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);
}
}
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);
}
}
//================================================================================
// --- [IMPROVED] CORE LOGIC & HELPER FUNCTIONS ---
//================================================================================
/**
* [IMPROVED FOR ROBUSTNESS]
* Sets a dropdown's value and starts an "enforcement loop" to win race conditions.
* This new method actively monitors and re-applies the value for a short duration
* if the website's own JavaScript tries to revert it, which is a more aggressive
* and reliable way to handle SPA race conditions that caused the original bug.
* @param {HTMLSelectElement} dropdown - The select element to change.
* @param {string} targetValue - The value to set the dropdown to.
* @param {string} description - A description for logging purposes (e.g., 'Workbench').
* @param {number} [duration=1000] - How long to enforce the value in milliseconds.
* @returns {Promise<boolean>} - Resolves with true if the value was successfully enforced, false otherwise.
*/
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);
});
}
(function() {
'use strict';
/**
* Extracts the patient's location using the same robust logic
* from the KAAUH Lab Super Suite.
* @returns {string|null} The patient's location string, or null if not found.
*/
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 red badge
Object.assign(locationDiv.style, {
fontWeight: 'bold',
fontSize: '18px',
color: '#ffffff', // Changed: White text for contrast
backgroundColor: '#0000ff', // Changed: The original red is now the background
borderRadius: '4px', // Added: Rounded corners for a badge look
padding: '4px 10px', // Changed: Better padding for a badge
marginLeft: '12px',
display: 'flex',
alignItems: 'center'
// borderLeft was removed as it's not needed with a background color
});
// 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.
// This is the most reliable way to handle dynamically loaded content.
const observer = new MutationObserver(() => {
// When any change happens in the DOM, we try to add the location.
// The function has checks to prevent it from running unnecessarily.
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, in case the
// elements are already present when the script loads.
addLocationToHeader();
})();
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();
relocatedResetButton.addEventListener('click', async (event) => {
event.preventDefault();
event.stopPropagation();
if (state.isProcessingReset) { return; }
state.isProcessingReset = true;
console.log("Relocated Reset button clicked - starting reset process");
try {
document.querySelectorAll(SELECTORS.agHeaderViewport + ' .ag-floating-filter-input').forEach(input => {
if (input.value !== '') {
input.value = '';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
});
await clearSavedFilters();
const workbenchSelect = document.getElementById(SELECTORS.workbenchDropdown.substring(1));
if (workbenchSelect) {
await setAndVerifyDropdown(workbenchSelect, workbenchSelect.options[0].value, 'Workbench');
}
const statusDropdown = document.querySelector(SELECTORS.statusDropdownTrigger)?.closest('select');
if (statusDropdown) {
await setAndVerifyDropdown(statusDropdown, statusDropdown.options[0].value, 'Status');
}
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("Reset process completed successfully");
} catch (error) {
console.error("Error during reset process:", error);
} finally {
setTimeout(() => { state.isProcessingReset = false; }, 1000);
}
});
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;
}
}
}
}
}
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() {
'use strict';
// --- Configuration ---
const elementToHideSelector = '.lo-view-detail-top';
const buttonContainerSelector = '.btn-area'; // The container where the button will be placed
// --- Icons (SVG) ---
// Using slightly smaller icons to better fit inside the button
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.
* @param {HTMLElement} element The element to hide or show.
*/
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.
* @param {HTMLElement} container The .btn-area element.
*/
const createToggleButton = (container) => {
const toggleButton = document.createElement('button');
toggleButton.id = 'gm-toggle-button';
// Add classes to match the other buttons on the page, including the default button color.
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 the container exists but our button doesn't, create it.
if (buttonContainer && !document.getElementById('gm-toggle-button')) {
createToggleButton(buttonContainer);
}
// --- Handle Element Visibility ---
const targetElement = document.querySelector(elementToHideSelector);
// If the target element exists, ensure its visibility is correct.
if (targetElement) {
updateElementVisibility(targetElement);
}
});
// Start observing the entire page for changes.
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
})();
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 });
}
setupNavigationShortcut();
if (!location.href.includes('/lab-orders/lab-test-analyzer')) return;
relocateResetButton();
insertFilterButtons();
addStatusDropdownListener();
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();
}, 10); // Increased throttle slightly for SPAs
}
const observer = new MutationObserver(masterObserverCallback);
observer.observe(document.body, { childList: true, subtree: true });
masterObserverCallback();
})();