// ==UserScript==
// @name KAAUH Lab Enhancement Suite 1 (Buttons, Alerts, Auto-Actions) - Modified Alert Handling
// @namespace Violentmonkey Scripts
// @version 5.6.4
// @description Combines verification buttons (F7/F8), dynamic alerts (>, NO RESULT, X-NORESULT, CL/CH) showing only the *last* alert per scan, checkbox automation, toggle back-nav, and improved scrolling.
// @match *://his.kaauh.org/lab/*
// @grant none
// @author Hamad AlShegifi
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// --- Configuration ---
const CONFIG = {
// General & Debugging
DEBUG_MODE: true,
PERSISTENT_MODALS: true, // Modals require manual dismissal
NO_RESULT_MESSAGE: "NO-RESULT DETECTED!!", // Standardized message
// Script 1: Buttons & Verification
TARGET_EDIT_PAGE_URL_PREFIX: 'https://his.kaauh.org/lab/#/lab-orders/edit-lab-order/',
EXCLUDE_WORDS: ['culture', "gram's stain", 'stain', 'bacterial', 'fungal', 'culture', 'pcr', 'Meningitis', 'MRSA', 'Mid', 'stream', 'Cryptococcus'],
VERIFY1_BUTTON_SELECTOR: '#custom-script-buttons button.btn-success',
VERIFY2_BUTTON_SELECTOR: '#custom-script-buttons button.btn-primary',
COMPLETE_TECH_VERIFY_SELECTOR: 'button.dropdown-item[translateid="test-results.CompleteTechnicalVerification"]',
COMPLETE_MED_VERIFY_SELECTOR: 'button.dropdown-item[translateid="test-results.CompleteMedicalVerification"]',
FINAL_VERIFY_BUTTON_SELECTOR: 'button.btn-success.btn-sm.min-width[translateid="test-verification.Verify"]',
NEXT_BUTTON_SELECTOR: 'button#btnNext',
TEST_DESC_SELECTOR: 'div[col-id="TestDesc"]',
UNCHECKED_BOX_SELECTOR: 'span.ag-icon-checkbox-unchecked[unselectable="on"]',
CHECKBOX_PARENT_ROW_SELECTOR: '.ag-row',
// Script 2: Alerts & Scanning
SCAN_INTERVAL: 150,
FLASH_COLOR: "pink",
FLASH_INTERVAL: 500,
// MODAL_TIMEOUT: 10000, // Removed as PERSISTENT_MODALS=true makes it irrelevant
RESULT_CELL_SELECTOR: 'div[role="gridcell"][col-id="TestResult"] app-result-value-render div',
CRITICAL_FLAG_SELECTOR: 'div[role="gridcell"][col-id="LTFlag"] app-ref-high-low div span.critial-alret-indication',
// Script 3 & General Toast Handling
UNDEFINED_URL_CHECK_INTERVAL: 200,
TOAST_CONTAINER_SELECTOR: '#toast-container',
TOAST_CLOSE_BUTTON_SELECTOR: 'button.toast-close-button',
SUCCESS_TOAST_SELECTOR: '.toast-success',
};
// --- State Variables ---
let verify1Toggle = localStorage.getItem('verify1Toggle') === 'true';
let verify2Toggle = localStorage.getItem('verify2Toggle') === 'true';
let verify1Clicked = false;
let verify2Clicked = false;
let issueScanIntervalId = null;
let isScanningActive = false;
const activeModals = new Set();
// --- Utility Functions ---
function logDebug(message) {
// Added check for console availability
if (CONFIG.DEBUG_MODE && typeof console !== 'undefined' && console.log) {
console.log(`[Lab Suite v5.6.4] ${message}`);
}
}
function loadFontAwesome() {
const existingLink = document.querySelector('link[href*="font-awesome"]');
if (!existingLink) {
const link = document.createElement('link');
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css';
link.rel = 'stylesheet';
document.head.appendChild(link);
logDebug('Font Awesome loaded');
}
}
function isVisible(element) {
return !!(element && (element.offsetWidth || element.offsetHeight || element.getClientRects().length));
}
// --- Modal Functions ---
function showModal(message) {
// Remove duplicate modals with same message - prevents spam if scan is too fast
document.querySelectorAll('.lab-suite-modal').forEach(modal => {
const contentElement = modal.querySelector('p'); // Target the content paragraph
if (contentElement && contentElement.textContent === message) {
const overlay = document.querySelector('.lab-suite-modal-overlay[data-modal-ref="' + modal.id + '"]');
logDebug(`Removing duplicate modal for message: ${message}`);
if (overlay) overlay.remove();
modal.remove();
activeModals.delete(modal); // Ensure removal from active set
}
});
logDebug(`Showing modal: ${message}`);
const modalId = `lab-suite-modal-${Date.now()}-${Math.random().toString(16).slice(2)}`; // Unique ID
const overlay = document.createElement("div");
overlay.className = 'lab-suite-modal-overlay';
overlay.style.position = "fixed";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.height = "100%";
overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
overlay.style.zIndex = "2000";
overlay.style.opacity = "0";
overlay.style.transition = "opacity 0.3s ease";
overlay.setAttribute('data-modal-ref', modalId); // Link overlay to modal
document.body.appendChild(overlay);
const modal = document.createElement("div");
modal.id = modalId; // Assign unique ID
modal.className = 'lab-suite-modal';
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%) scale(0.5)";
modal.style.backgroundColor = "#f4f4f9";
modal.style.padding = "30px";
modal.style.boxShadow = "0px 10px 30px rgba(0, 0, 0, 0.15)";
modal.style.zIndex = "2001";
modal.style.borderRadius = "15px";
modal.style.textAlign = "center";
modal.style.transition = "transform 0.3s ease, opacity 0.3s ease";
modal.style.opacity = "0";
modal.style.maxWidth = "450px";
modal.setAttribute('role', 'alertdialog'); // More specific role
modal.setAttribute('aria-labelledby', `${modalId}-heading`);
modal.setAttribute('aria-describedby', `${modalId}-content`);
modal.setAttribute('aria-modal', 'true');
const heading = document.createElement("h2");
heading.id = `${modalId}-heading`; // ID for aria-labelledby
heading.textContent = "Attention!";
heading.style.fontFamily = "'Arial', sans-serif";
heading.style.color = "#333";
heading.style.marginBottom = "10px";
heading.style.fontSize = "24px";
modal.appendChild(heading);
const content = document.createElement("p");
content.id = `${modalId}-content`; // ID for aria-describedby
// Standardize NO RESULT messages
let displayMessage = message;
if (message.includes("NO RESULT") || message.includes("X-NORESULT")) {
displayMessage = CONFIG.NO_RESULT_MESSAGE;
}
content.textContent = displayMessage;
content.style.fontFamily = "'Arial', sans-serif";
content.style.color = "#555";
content.style.marginBottom = "20px";
content.style.fontSize = "16px";
content.style.lineHeight = "1.5";
modal.appendChild(content);
const okButton = createModalButton("OK", "#ff4081", () => {
logDebug(`Modal [${modalId}] manually dismissed.`);
closeModal(modal, overlay);
});
modal.appendChild(okButton);
document.body.appendChild(modal);
okButton.focus(); // Focus the OK button for accessibility
// Animate appearance
setTimeout(() => {
overlay.style.opacity = "1";
modal.style.transform = "translate(-50%, -50%) scale(1)";
modal.style.opacity = "1";
}, 10);
activeModals.add(modal);
// Auto-close functionality (if PERSISTENT_MODALS is false) - currently disabled by config
// if (!CONFIG.PERSISTENT_MODALS && CONFIG.MODAL_TIMEOUT > 0) {
// setTimeout(() => {
// logDebug(`Modal [${modalId}] automatically closing after timeout.`);
// closeModal(modal, overlay);
// }, CONFIG.MODAL_TIMEOUT);
// }
}
function createModalButton(text, backgroundColor, onClick) {
const button = document.createElement("button");
button.textContent = text;
button.style.padding = "10px 20px";
button.style.border = "none";
button.style.backgroundColor = backgroundColor;
button.style.color = "white";
button.style.borderRadius = "30px";
button.style.cursor = "pointer";
button.style.fontSize = "16px";
button.style.transition = "background-color 0.3s ease, transform 0.2s ease";
button.style.minWidth = '80px';
button.style.margin = '0 5px';
button.addEventListener("mouseenter", () => {
button.style.backgroundColor = darkenColor(backgroundColor, 20);
button.style.transform = "scale(1.05)";
});
button.addEventListener("mouseleave", () => {
button.style.backgroundColor = backgroundColor;
button.style.transform = "scale(1)";
});
button.addEventListener("click", onClick);
return button;
}
function closeModal(modal, overlay) {
if (!modal || !overlay || !document.body.contains(modal)) {
logDebug("Attempted to close a modal that doesn't exist or is already removed.");
return;
}
logDebug(`Closing modal [${modal.id}].`);
modal.style.transform = "translate(-50%, -50%) scale(0.5)";
modal.style.opacity = "0";
overlay.style.opacity = "0";
// Remove after transition
setTimeout(() => {
if (document.body.contains(modal)) {
document.body.removeChild(modal);
activeModals.delete(modal); // Ensure removal from active set
logDebug(`Modal [${modal.id}] removed from DOM.`);
} else {
logDebug(`Modal [${modal.id}] was already removed from DOM before timeout.`);
}
if (document.body.contains(overlay)) {
document.body.removeChild(overlay);
logDebug(`Overlay for modal [${modal.id}] removed from DOM.`);
} else {
logDebug(`Overlay for modal [${modal.id}] was already removed from DOM before timeout.`);
}
}, 300); // Corresponds to transition duration
}
function darkenColor(color, percent) {
try {
// Ensure color starts with #
if (!color.startsWith('#')) {
logDebug(`Invalid color format for darkenColor: ${color}`);
return color; // Return original if format is wrong
}
let num = parseInt(color.slice(1), 16);
let amt = Math.round(2.55 * percent);
let R = (num >> 16) - amt;
let G = ((num >> 8) & 0x00ff) - amt;
let B = (num & 0x0000ff) - amt;
R = R < 0 ? 0 : R;
G = G < 0 ? 0 : G;
B = B < 0 ? 0 : B;
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
} catch (e) {
logDebug(`Error darkening color ${color}: ${e}`);
return color; // Return original on error
}
}
// --- Script 1 Functions ---
function isCorrectPage() {
return window.location.href.startsWith(CONFIG.TARGET_EDIT_PAGE_URL_PREFIX);
}
function createVerifyButton(label, className, onClick) {
let button = document.createElement('button');
button.type = 'button';
button.innerHTML = label;
button.className = className;
let buttonColors = {
'btn btn-success btn-sm': '#28a745', // Green for VERIFY1
'btn btn-primary btn-sm': '#2594d9' // Blue for VERIFY2
};
// Basic styles - !important might be needed depending on site CSS specificity
button.style.cssText = `
font-family: 'Arial', sans-serif !important;
font-size: 14px !important;
font-weight: normal !important;
color: #ffffff !important;
background-color: ${buttonColors[className] || '#6c757d'} !important; /* Default grey */
padding: 8px 16px !important;
border: none !important;
border-radius: 5px !important;
text-shadow: none !important;
cursor: pointer !important;
margin-right: 5px !important;
line-height: 1.5 !important;
vertical-align: middle; /* Align with icons */
`;
button.onclick = onClick;
return button;
}
function createToggleIcon(id, isActive, onClick) {
let icon = document.createElement('span');
icon.id = id;
// Using Font Awesome classes directly in innerHTML
icon.innerHTML = `<i class="fas fa-arrow-circle-left" aria-hidden="true" style="color: ${isActive ? '#008000' : '#d1cfcf'}; font-size: 1.3em; vertical-align: middle;"></i>`;
icon.style.cursor = 'pointer';
icon.style.marginRight = '10px'; // Space after icon
icon.style.marginLeft = '-1px'; // Adjust spacing relative to button
icon.onclick = onClick;
icon.title = "Toggle: Go back automatically after this verification?"; // More descriptive title
return icon;
}
function handleVerify1IconToggle() {
verify1Toggle = !verify1Toggle;
localStorage.setItem('verify1Toggle', verify1Toggle);
const iconElement = document.querySelector('#verify1Icon i'); // Target the <i> tag
if (iconElement) iconElement.style.color = verify1Toggle ? '#008000' : '#d1cfcf'; // Green/Grey
logDebug(`Verify1 Auto-Back Toggle set to: ${verify1Toggle}`);
}
function handleVerify2IconToggle() {
verify2Toggle = !verify2Toggle;
localStorage.setItem('verify2Toggle', verify2Toggle);
const iconElement = document.querySelector('#verify2Icon i'); // Target the <i> tag
if (iconElement) iconElement.style.color = verify2Toggle ? '#008000' : '#d1cfcf'; // Green/Grey
logDebug(`Verify2 Auto-Back Toggle set to: ${verify2Toggle}`);
}
function addButtons() {
if (document.getElementById('custom-script-buttons') || !isCorrectPage()) return;
const nextButton = document.querySelector(CONFIG.NEXT_BUTTON_SELECTOR);
if (nextButton && nextButton.parentNode) {
logDebug("Adding custom VERIFY buttons.");
let buttonDiv = document.createElement('div');
buttonDiv.id = 'custom-script-buttons';
buttonDiv.style.display = 'inline-block'; // Keep elements inline
buttonDiv.style.marginLeft = '10px'; // Space from Next button
buttonDiv.style.verticalAlign = 'middle'; // Align container vertically
// VERIFY1 Button & Toggle
let verify1Button = createVerifyButton('VERIFY1 (F7)', 'btn btn-success btn-sm', () => {
logDebug("VERIFY1 button clicked.");
verify1Clicked = true;
verify2Clicked = false;
checkAllVisibleBoxesWithoutDuplicates();
// Add slight delay before clicking dropdown item
setTimeout(() => { clickCompleteTechnicalVerificationButton(); }, 500);
});
let verify1Icon = createToggleIcon('verify1Icon', verify1Toggle, handleVerify1IconToggle);
// VERIFY2 Button & Toggle
let verify2Button = createVerifyButton('VERIFY2 (F8)', 'btn btn-primary btn-sm', () => {
logDebug("VERIFY2 button clicked.");
verify2Clicked = true;
verify1Clicked = false;
checkAllVisibleBoxesWithoutDuplicates();
// Add slight delay before clicking dropdown item
setTimeout(() => { clickCompleteMedicalVerificationButton(); }, 500);
});
let verify2Icon = createToggleIcon('verify2Icon', verify2Toggle, handleVerify2IconToggle);
// Author Credit (Styled)
let modedByText = document.createElement('span');
modedByText.textContent = "Modded by: Hamad AlShegifi";
modedByText.style.fontSize = '11px'; // Slightly smaller
modedByText.style.fontWeight = 'bold';
modedByText.style.color = '#e60000'; // Darker red
modedByText.style.marginLeft = '15px'; // Space before credit
modedByText.style.border = '1px solid #e60000';
modedByText.style.borderRadius = '5px'; // Matched button radius
modedByText.style.padding = '3px 6px';
modedByText.style.backgroundColor = '#fff0f0'; // Very light pink background
modedByText.style.verticalAlign = 'middle'; // Align with buttons/icons
// Append elements in order
buttonDiv.appendChild(verify1Button);
buttonDiv.appendChild(verify1Icon);
buttonDiv.appendChild(verify2Button);
buttonDiv.appendChild(verify2Icon);
buttonDiv.appendChild(modedByText); // Add credit last
// Insert the div after the Next button
nextButton.parentNode.insertBefore(buttonDiv, nextButton.nextSibling);
} else {
logDebug("Could not find Next button ('" + CONFIG.NEXT_BUTTON_SELECTOR + "') to anchor custom buttons.");
// Fallback: Append to body or a known container if Next button is missing?
// For now, it just logs the failure.
}
}
function checkAllVisibleBoxesWithoutDuplicates() {
logDebug("Checking checkboxes...");
const testDivs = document.querySelectorAll(CONFIG.TEST_DESC_SELECTOR);
let seenTests = new Set();
let boxesChecked = 0;
// Ensure exclude words are lowercase for comparison
const excludeWordsLower = CONFIG.EXCLUDE_WORDS.map(word => word.toLowerCase());
testDivs.forEach(testDiv => {
const testName = testDiv.textContent?.trim().toLowerCase() || '';
if (!testName) return; // Skip empty descriptions
// Check if test name contains any excluded word
if (excludeWordsLower.some(word => testName.includes(word))) {
logDebug(`Excluding checkbox for test containing excluded word: ${testName}`);
return; // Skip this test
}
// Check for duplicates (only check the first instance of a test name)
if (seenTests.has(testName)) {
logDebug(`Skipping duplicate test: ${testName}`);
return; // Skip this duplicate test
}
seenTests.add(testName); // Add unique test name to set
// Find the parent row and the unchecked checkbox within it
const parentRow = testDiv.closest(CONFIG.CHECKBOX_PARENT_ROW_SELECTOR);
if (parentRow) {
const checkbox = parentRow.querySelector(CONFIG.UNCHECKED_BOX_SELECTOR);
// Ensure checkbox exists and is visible before clicking
if (checkbox && isVisible(checkbox)) {
logDebug(`Clicking checkbox for unique, non-excluded test: ${testName}`);
// Simulate a user click more reliably
const event = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
checkbox.dispatchEvent(event);
boxesChecked++;
} else if (checkbox && !isVisible(checkbox)) {
logDebug(`Checkbox found but not visible for test: ${testName}`);
}
} else {
logDebug(`Could not find parent row for test: ${testName}`);
}
});
logDebug(`${boxesChecked} unique, non-excluded, visible checkboxes were checked.`);
}
function clickCompleteTechnicalVerificationButton() {
const button = document.querySelector(CONFIG.COMPLETE_TECH_VERIFY_SELECTOR);
if (button) {
button.click();
logDebug("Complete Technical Verification button clicked!");
// Wait slightly longer for the final verify button to appear/enable
setTimeout(() => { clickFinalVerifyButton(); }, 700);
} else {
logDebug("Complete Technical Verification button not found!");
showModal("Error: Unable to find the 'Complete Technical Verification' button.");
}
}
function clickCompleteMedicalVerificationButton() {
const button = document.querySelector(CONFIG.COMPLETE_MED_VERIFY_SELECTOR);
if (button) {
button.click();
logDebug("Complete Medical Verification button clicked!");
// Wait slightly longer for the final verify button to appear/enable
setTimeout(() => { clickFinalVerifyButton(); }, 700);
} else {
logDebug("Complete Medical Verification button not found!");
showModal("Error: Unable to find the 'Complete Medical Verification' button.");
}
}
function clickFinalVerifyButton() {
const verifyButton = document.querySelector(CONFIG.FINAL_VERIFY_BUTTON_SELECTOR);
if (verifyButton && !verifyButton.disabled) { // Check if button exists and is not disabled
verifyButton.click();
logDebug("Final Verify button clicked!");
// Reset click flags AFTER successful final verify click
// verify1Clicked = false; // Moved reset logic to toast observer/click
// verify2Clicked = false;
} else if (verifyButton && verifyButton.disabled) {
logDebug("Final Verify button found, but it is disabled.");
showModal("Error: The final 'Verify' button is disabled. Cannot proceed.");
} else {
logDebug("Final Verify button not found!");
showModal("Error: Unable to find the final 'Verify' button.");
}
}
// --- Script 2 Functions ---
function applyFlashingEffect(rows) {
rows.forEach(row => {
// Prevent multiple intervals on the same row
if (row.dataset.flashing === 'true') {
// logDebug("Flashing already active on this row.");
return;
}
row.dataset.flashing = 'true'; // Mark as flashing
logDebug(`Applying flashing effect to row ID: ${row.getAttribute('row-id') || 'N/A'}`);
// Store original background for restoring later if needed, default to transparent
const originalBg = row.style.backgroundColor || 'transparent';
row.dataset.originalBg = originalBg; // Store it
row.style.transition = "background-color 0.5s ease";
let isPink = false;
const intervalId = setInterval(() => {
// Check if row still exists in DOM
if (!document.body.contains(row)) {
clearInterval(intervalId);
// No need to reset background if element is gone
logDebug(`Row ${row.getAttribute('row-id') || 'N/A'} removed, stopping its flash interval.`);
return;
}
// Check if flashing should stop (e.g., modal dismissed or issue resolved - needs external trigger)
if (row.dataset.flashing === 'false') {
clearInterval(intervalId);
row.style.backgroundColor = row.dataset.originalBg || 'transparent'; // Restore original
logDebug(`Flashing stopped externally for row ID: ${row.getAttribute('row-id') || 'N/A'}. Restored background.`);
delete row.dataset.flashing; // Clean up attribute
delete row.dataset.originalBg;
delete row.dataset.flashIntervalId;
return;
}
isPink = !isPink;
row.style.backgroundColor = isPink ? CONFIG.FLASH_COLOR : originalBg;
}, CONFIG.FLASH_INTERVAL);
// Store interval ID on the row itself for potential external clearing
row.dataset.flashIntervalId = intervalId;
});
}
function stopFlashingEffect(row) {
if (row && row.dataset.flashing === 'true') {
logDebug(`Requesting to stop flashing for row ID: ${row.getAttribute('row-id') || 'N/A'}`);
row.dataset.flashing = 'false'; // Signal interval to stop and restore color
// The interval itself handles cleanup
}
}
function getNotificationSessionKey(type, identifier = 'general') {
// Include pathname to make keys specific to the current view/order
return `labSuiteNotified_${window.location.pathname}_${type}_${identifier}`;
}
function hasAlreadyNotified(type, identifier = 'general') {
const key = getNotificationSessionKey(type, identifier);
const notified = sessionStorage.getItem(key) === 'true';
// if (notified) logDebug(`Notification flag FOUND for key: ${key}`);
return notified;
}
function setNotificationFlag(type, identifier = 'general') {
const key = getNotificationSessionKey(type, identifier);
logDebug(`Setting notification flag for key: ${key}`);
sessionStorage.setItem(key, 'true');
}
// ==================================================
// == MODIFIED checkForIssues Function Starts Here ==
// ==================================================
function checkForIssues() {
const resultDivs = document.querySelectorAll(CONFIG.RESULT_CELL_SELECTOR);
const criticalDivs = document.querySelectorAll(CONFIG.CRITICAL_FLAG_SELECTOR);
const potentialAlerts = []; // Store potential alerts found in this scan
// Helper function to scroll with 'nearest' block alignment
function scrollToRowNearest(element) {
const row = element.closest(CONFIG.CHECKBOX_PARENT_ROW_SELECTOR);
if (row) {
const rowId = row.getAttribute('row-id');
logDebug(`Scrolling to nearest position for row ID: ${rowId || 'ID not found'}`);
// Use 'nearest' to minimize scrolling distance and avoid centering
try {
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (e) {
logDebug(`Error during scrollIntoView: ${e}. Falling back to basic scroll.`);
row.scrollIntoView(); // Fallback
}
return true;
}
logDebug('Could not find parent row to scroll to.');
return false;
}
// 1. Check for Critical Flags (CL/CH)
criticalDivs.forEach(div => {
const text = div.textContent?.trim() || '';
if (text === "CL" || text === "CH") {
const rowElement = div.closest(CONFIG.CHECKBOX_PARENT_ROW_SELECTOR);
if (rowElement) {
const specificRowKey = rowElement.getAttribute('row-id') || text + "_" + Array.from(criticalDivs).indexOf(div); // Use row-id or text+index as fallback
if (!hasAlreadyNotified('critical', specificRowKey)) {
const message = text === "CL" ? "CRITICAL LOW RESULT DETECTED !!" : "CRITICAL HIGH RESULT DETECTED !!";
logDebug(`Found potential critical alert (${text}) for row ${specificRowKey}`);
potentialAlerts.push({
element: div, // Store the element that triggered the alert
rowElement: rowElement, // Store the row itself for easier scrolling later
type: 'critical',
message: message,
rowId: specificRowKey
});
} else {
// logDebug(`Critical alert (${text}) for row ${specificRowKey} already notified this session.`);
}
}
}
});
// 2. Check for "NO RESULT" / "X-NORESULT"
resultDivs.forEach(div => {
const text = div.textContent?.trim().toLowerCase() || '';
const isNoResultType = (text === "no result" || text === "no-xresult" || text === "x-noresult");
if (isNoResultType) {
const rowElement = div.closest(CONFIG.CHECKBOX_PARENT_ROW_SELECTOR);
if (rowElement) {
const specificRowKey = rowElement.getAttribute('row-id') || text.replace(/[\s-]+/g, '_') + "_" + Array.from(resultDivs).indexOf(div); // Use row-id or text+index
if (!hasAlreadyNotified('noresult', specificRowKey)) {
logDebug(`Found potential noresult alert (${text}) for row ${specificRowKey}`);
potentialAlerts.push({
element: div,
rowElement: rowElement,
type: 'noresult',
message: CONFIG.NO_RESULT_MESSAGE, // Use standardized message
rowId: specificRowKey
});
} else {
// logDebug(`Noresult alert (${text}) for row ${specificRowKey} already notified this session.`);
}
}
}
});
// 3. Check for ">" (Dilution Required)
resultDivs.forEach(div => {
const text = div.textContent?.trim() || ''; // Don't lowercase here, '>' is specific
if (text.includes(">")) {
const rowElement = div.closest(CONFIG.CHECKBOX_PARENT_ROW_SELECTOR);
if (rowElement) {
const rowId = rowElement.getAttribute('row-id') || ">_" + Array.from(resultDivs).indexOf(div); // Use row-id or symbol+index
if (!hasAlreadyNotified('greaterThan', rowId)) {
logDebug(`Found potential greaterThan alert (>) for row ${rowId}`);
// Apply flashing immediately for any '>' found, regardless of whether it's the final alert
applyFlashingEffect([rowElement]); // Pass row element in array
potentialAlerts.push({
element: div,
rowElement: rowElement,
type: 'greaterThan',
message: "Dilution is required for this sample (> detected)!",
rowId: rowId
});
} else {
// Apply flashing even if notified, as the condition persists
applyFlashingEffect([rowElement]);
// logDebug(`GreaterThan alert (>) for row ${rowId} already notified this session, but applying flash.`);
}
}
}
});
// --- Process the collected alerts ---
if (potentialAlerts.length > 0) {
// Get the LAST alert found during the scan (assumes DOM order is stable)
const lastAlert = potentialAlerts[potentialAlerts.length - 1];
logDebug(`Potential alerts found: ${potentialAlerts.length}. Selecting the last one: Type=${lastAlert.type}, RowID=${lastAlert.rowId}`);
// Check if *this specific last alert* has already been notified and dismissed in this session
if (!hasAlreadyNotified(lastAlert.type, lastAlert.rowId)) {
// Show the modal for the last alert found
showModal(lastAlert.message);
// Set the notification flag for this specific alert row/type
// This prevents this exact alert from showing again until the session ends or changes path
setNotificationFlag(lastAlert.type, lastAlert.rowId);
// Scroll to the row of the last alert using 'nearest'
scrollToRowNearest(lastAlert.element);
return true; // Indicate that an alert was processed in this cycle
} else {
logDebug(`Last potential alert (Type=${lastAlert.type}, RowID=${lastAlert.rowId}) was already notified in this session. Modal suppressed.`);
// Ensure flashing is still applied if it's a '>' type, even if notified
if (lastAlert.type === 'greaterThan' && lastAlert.rowElement) {
applyFlashingEffect([lastAlert.rowElement]);
}
}
} else {
// logDebug("No new alert conditions found in this scan cycle.");
}
return false; // No new, un-notified alert was processed in this cycle
}
// ================================================
// == MODIFIED checkForIssues Function Ends Here ==
// ================================================
function startContinuousScanning() {
if (isScanningActive) {
// logDebug("Scanning already active.");
return;
}
logDebug("Starting continuous issue scanning...");
isScanningActive = true;
// Clear any previous interval just in case
if (issueScanIntervalId) clearInterval(issueScanIntervalId);
issueScanIntervalId = setInterval(() => {
// Check if the grid elements we monitor still exist
const resultsGridExists = document.querySelector(CONFIG.RESULT_CELL_SELECTOR) || document.querySelector(CONFIG.CRITICAL_FLAG_SELECTOR);
if (!resultsGridExists && isScanningActive) { // Add isScanningActive check to prevent race condition log
logDebug("Monitored result/critical elements disappeared, stopping issue scan.");
stopContinuousScanning(); // Stop if grid is gone
return;
}
// Run the check
checkForIssues();
}, CONFIG.SCAN_INTERVAL);
}
function stopContinuousScanning() {
if (issueScanIntervalId) {
clearInterval(issueScanIntervalId);
issueScanIntervalId = null;
logDebug("Stopped continuous issue scanning.");
}
// Reset flashing state for all rows when scanning stops (e.g., page navigation)
document.querySelectorAll(CONFIG.CHECKBOX_PARENT_ROW_SELECTOR + '[data-flashing="true"]').forEach(row => {
stopFlashingEffect(row);
});
isScanningActive = false;
}
// --- Script 3 Function ---
function checkUrlAndTriggerClickForUndefined() {
// Avoid running if modals are open, as it might dismiss them unexpectedly
if (activeModals.size > 0) {
return;
}
const currentUrl = window.location.href;
// Check specifically for ending with /undefined
if (currentUrl.endsWith('/undefined')) {
logDebug('URL ends with /undefined. Checking for toast...');
const toastContainer = document.querySelector(CONFIG.TOAST_CONTAINER_SELECTOR);
if (toastContainer) {
const closeButton = toastContainer.querySelector(CONFIG.TOAST_CLOSE_BUTTON_SELECTOR);
// Ensure the button is visible before clicking
if (closeButton && isVisible(closeButton)) {
logDebug('Found visible toast close button on /undefined page. Clicking...');
closeButton.click();
// Optional: Navigate back or to a default page?
// window.history.back(); // Or window.location.href = '/lab/some/default/page';
} else {
logDebug('Toast container found, but close button not found or not visible.');
}
} else {
logDebug('Toast container not found on /undefined page.');
}
}
}
// --- Event Listeners & Observers ---
// Keyboard Shortcuts (F7/F8)
document.addEventListener('keydown', function (event) {
// Ignore keypresses if a modal is open or if typing in an input/textarea
if (activeModals.size > 0 || ['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName)) {
return;
}
if (event.key === 'F7') {
event.preventDefault(); // Prevent default F7 browser behavior
logDebug("F7 pressed: Triggering VERIFY1 button click");
const verify1Button = document.querySelector(CONFIG.VERIFY1_BUTTON_SELECTOR);
if (verify1Button) verify1Button.click();
else logDebug("VERIFY1 button not found for F7 shortcut.");
} else if (event.key === 'F8') {
event.preventDefault(); // Prevent default F8 browser behavior
logDebug("F8 pressed: Triggering VERIFY2 button click");
const verify2Button = document.querySelector(CONFIG.VERIFY2_BUTTON_SELECTOR);
if (verify2Button) verify2Button.click();
else logDebug("VERIFY2 button not found for F8 shortcut.");
}
});
// Observer for Success Toasts (for Auto-Back Navigation)
const toastObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
// Check if the added node is an element and matches the success toast selector
if (node.nodeType === Node.ELEMENT_NODE && node.matches && node.matches(CONFIG.SUCCESS_TOAST_SELECTOR)) {
logDebug('Success toast detected. Adding click listener for potential back navigation.');
// Use a named function for potential removal later if needed
const handleToastClick = () => {
logDebug('Success toast clicked.');
if (verify1Clicked && verify1Toggle) {
logDebug('Verify1 was active and toggle is ON. Navigating back.');
window.history.back();
} else if (verify2Clicked && verify2Toggle) {
logDebug('Verify2 was active and toggle is ON. Navigating back.');
window.history.back();
} else {
logDebug('Toast clicked, but no active verify toggle matched or toggle was OFF.');
}
// Reset flags regardless of navigation
verify1Clicked = false;
verify2Clicked = false;
// Remove listener after click to prevent multiple back navigations if toast persists
node.removeEventListener('click', handleToastClick);
};
node.addEventListener('click', handleToastClick);
}
});
});
});
// Main Observer for Page Changes (Adding/Removing Buttons, Starting/Stopping Scan)
const mainObserver = new MutationObserver((mutations) => {
// Optimization: Check if relevant nodes were added/removed, e.g., the grid or buttons container
let potentiallyRelevantChange = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
// Basic check: If any nodes were added/removed, re-evaluate state
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
potentiallyRelevantChange = true;
break;
}
// More specific checks could be added here if performance is an issue
}
}
if (!potentiallyRelevantChange) return; // Skip if no relevant DOM changes detected
// --- Handle Custom Buttons ---
if (isCorrectPage()) {
// If on the correct page, ensure buttons are present
if (!document.getElementById('custom-script-buttons')) {
addButtons(); // Add buttons if they are missing
}
} else {
// If not on the correct page, ensure buttons are removed
const buttonDiv = document.getElementById('custom-script-buttons');
if (buttonDiv) {
logDebug("Navigated away from edit page, removing custom buttons.");
buttonDiv.remove();
}
}
// --- Handle Continuous Scanning ---
// Check if the elements we scan for exist
const resultsGridExists = document.querySelector(CONFIG.RESULT_CELL_SELECTOR) || document.querySelector(CONFIG.CRITICAL_FLAG_SELECTOR);
if (resultsGridExists) {
// If grid exists, ensure scanning is active
if (!isScanningActive) {
startContinuousScanning();
}
} else {
// If grid does not exist, ensure scanning is stopped
if (isScanningActive) {
stopContinuousScanning();
}
}
});
// --- Initialization ---
try {
logDebug("KAAUH Lab Enhancement Suite Initializing (v5.6.4)...");
loadFontAwesome(); // Load icons
// Start checking for the '/undefined' URL issue periodically
setInterval(checkUrlAndTriggerClickForUndefined, CONFIG.UNDEFINED_URL_CHECK_INTERVAL);
logDebug(`Started URL check interval (${CONFIG.UNDEFINED_URL_CHECK_INTERVAL}ms) for /undefined toasts.`);
// Start observing the main body for changes relevant to buttons and scanning
mainObserver.observe(document.body, { childList: true, subtree: true });
logDebug("Started main MutationObserver.");
// Start observing for success toasts
// Observe the body, assuming toasts are appended there or within a container in the body
const toastTargetNode = document.body; // Or a more specific container if known e.g., document.getElementById('toast-container-parent')
toastObserver.observe(toastTargetNode, { childList: true, subtree: true }); // subtree: true if toasts can appear deep
logDebug("Started toast MutationObserver for back-navigation.");
// Initial setup checks on window load (covers cases where script loads after initial DOM ready)
window.addEventListener('load', () => {
logDebug("Page fully loaded (window.load event). Performing initial checks.");
// Ensure buttons are added if on the correct page initially
if (isCorrectPage()) {
if (!document.getElementById('custom-script-buttons')) {
addButtons();
}
}
// Start scanning if results grid exists on load
const resultsGridExists = document.querySelector(CONFIG.RESULT_CELL_SELECTOR) || document.querySelector(CONFIG.CRITICAL_FLAG_SELECTOR);
if (resultsGridExists && !isScanningActive) {
startContinuousScanning();
}
});
// Also run initial checks once DOM is ready (might be slightly earlier than window.load)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
logDebug("DOM fully loaded and parsed (DOMContentLoaded event). Performing initial checks.");
if (isCorrectPage()) {
if (!document.getElementById('custom-script-buttons')) {
addButtons();
}
}
const resultsGridExists = document.querySelector(CONFIG.RESULT_CELL_SELECTOR) || document.querySelector(CONFIG.CRITICAL_FLAG_SELECTOR);
if (resultsGridExists && !isScanningActive) {
startContinuousScanning();
}
});
} else {
// DOM already ready, run checks immediately
logDebug("DOM already ready. Performing initial checks.");
if (isCorrectPage()) {
if (!document.getElementById('custom-script-buttons')) {
addButtons();
}
}
const resultsGridExists = document.querySelector(CONFIG.RESULT_CELL_SELECTOR) || document.querySelector(CONFIG.CRITICAL_FLAG_SELECTOR);
if (resultsGridExists && !isScanningActive) {
startContinuousScanning();
}
}
logDebug("Initialization complete.");
} catch (error) {
console.error("[Lab Suite] Critical error during initialization:", error);
// Attempt to show a modal even if other parts failed
try {
showModal("A critical error occurred in the Lab Enhancement Suite. Please check the browser console (F12) for details.");
} catch (modalError) {
console.error("[Lab Suite] Could not even display the error modal:", modalError);
alert("A critical error occurred in the Lab Enhancement Suite script. Check the console (F12)."); // Fallback alert
}
}
})(); // End of Userscript IIFE