KAAUH Lab Suite - Verification, Alerts & Enhancements

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.

当前为 2025-04-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         KAAUH Lab Suite - Verification, Alerts & Enhancements
// @namespace    Violentmonkey Scripts
// @version      6.6
// @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';

    function applySingleLineStyles() {
        const anchor = document.querySelector('li > a[href="#/lab-orders/doctor-request"]');
        if (anchor) {
            anchor.style.setProperty('white-space', 'nowrap', 'important');

            anchor.style.setProperty('overflow', 'visible', 'important');
            anchor.style.setProperty('text-overflow', 'unset', 'important');

            const spans = anchor.querySelectorAll('span');
            spans.forEach(span => {
                span.style.setProperty('display', 'inline', 'important');
                span.style.setProperty('font-size', '13px', 'important');
                span.style.setProperty('white-space', 'nowrap', 'important');
            });
        }

        const simplifySpan = (selector) => {
            const span = document.querySelector(selector);
            if (span) {
                span.style.setProperty('display', 'inline', 'important');
                span.style.setProperty('font-size', '20px', 'important');
                span.style.setProperty('white-space', 'nowrap', 'important');
                span.style.setProperty('overflow', 'visible', 'important');
                span.style.setProperty('text-overflow', 'unset', 'important');
                span.textContent = span.textContent.replace(/\s+/g, ''); // optional
            }
        };

        simplifySpan('span.to-do');
        simplifySpan('span.pending-orders');
    }

    // Debounce function to limit how often applySingleLineStyles is called
    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    const debouncedStyleUpdater = debounce(applySingleLineStyles, 300);

    const observer = new MutationObserver(debouncedStyleUpdater);
    observer.observe(document.body, { childList: true, subtree: true });

    // Initial run
    applySingleLineStyles();
})();

(function () {
  'use strict';

  /*** CONFIGURATION (Main Script) ***/
  const CONFIG_MAIN = {
    URLS: {
      EDIT_PAGE_PREFIX: 'https://his.kaauh.org/lab/#/lab-orders/edit-lab-order/',
    },
    SELECTORS: {
      VERIFY1_BTN: '#custom-script-buttons button.verify1-btn',
      VERIFY2_BTN: '#custom-script-buttons button.verify2-btn',
      COMPLETE_TECH: 'button.dropdown-item[translateid="test-results.CompleteTechnicalVerification"]',
      COMPLETE_MED: 'button.dropdown-item[translateid="test-results.CompleteMedicalVerification"]',
      FINAL_VERIFY: 'button.btn-success.btn-sm.min-width[translateid="test-verification.Verify"]',
      NEXT_BTN: 'button#btnNext',
      UNCHECKED_BOX: 'span.ag-icon-checkbox-unchecked[unselectable="on"]',
      CHECKBOX_ROW: '.ag-row',
      TEST_DESC_CELL: '[col-id="TestDesc"]', // Added for clarity
      ORDERED_STATUS_CELL: 'div[col-id="ResultStatus"]', // Added for clarity
      TOAST: {
        CONTAINER: '#toast-container',
        CLOSE_BTN: 'button.toast-close-button',
        SUCCESS: '.toast-success',
      }
    },
    CHECK_INTERVALS: {
      UNDEFINED_URL: 200,
      ORDERED_SCAN: 500,
      DISABLED_BTN_CHECK: 1000, // Check for disabled buttons every second
    },
    EXCLUDE_WORDS: [ // Words to exclude from automatic checkbox selection
      'culture', "gram's stain", 'stain', 'bacterial', 'fungal',
      'pcr', 'meningitis', 'mrsa', 'mid', 'stream', 'cryptococcus'
    ]
  };

  /*** STATE FLAGS ***/
  let verify1Clicked = false;
  let verify2Clicked = false;
  let verify1Toggle = localStorage.getItem('verify1Toggle') === 'true';
  let verify2Toggle = localStorage.getItem('verify2Toggle') === 'true';
  let hasScrolledToOrderedRow = false; // Track if we've scrolled to the "Ordered" row
  let lastDisabledButtonAlertTime = 0; // Track when we last alerted about disabled buttons
  const DISABLED_ALERT_COOLDOWN = 30000; // 30 seconds cooldown between alerts

  /*** UTILITY FUNCTIONS ***/
  const logDebugMain = msg => console.debug(`[LabScript Main] ${msg}`);
  const isCorrectPage = () => window.location.href.startsWith(CONFIG_MAIN.URLS.EDIT_PAGE_PREFIX);

  // Function to show alert for disabled buttons
  function showDisabledButtonAlert(message) {
    const now = Date.now();
    if (now - lastDisabledButtonAlertTime < DISABLED_ALERT_COOLDOWN) {
      return; // Still in cooldown period
    }
    lastDisabledButtonAlertTime = now;

    // Create a custom modal alert
    const modalOverlay = document.createElement('div');
    modalOverlay.id = 'disabled-button-alert-overlay';
    Object.assign(modalOverlay.style, {
      position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh',
      backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '10000', display: 'flex',
      justifyContent: 'center', alignItems: 'center'
    });

    const modalBox = document.createElement('div');
    Object.assign(modalBox.style, {
      backgroundColor: '#fff', padding: '25px', borderRadius: '8px',
      boxShadow: '0 5px 15px rgba(0,0,0,0.3)', width: 'auto', maxWidth: '80%',
      textAlign: 'center', borderTop: '5px solid #f0ad4e' // Warning color
    });

    const title = document.createElement('h3');
    title.textContent = 'Button Disabled';
    title.style.color = '#d9534f'; // Red color for title
    title.style.marginTop = '0';
    modalBox.appendChild(title);

    const messageElem = document.createElement('p');
    messageElem.textContent = message;
    messageElem.style.fontSize = '16px';
    messageElem.style.marginBottom = '20px';
    modalBox.appendChild(messageElem);

    const okButton = document.createElement('button');
    okButton.textContent = 'OK';
    Object.assign(okButton.style, {
      padding: '8px 20px', borderRadius: '5px', backgroundColor: '#5cb85c',
      color: '#fff', border: 'none', cursor: 'pointer', fontSize: '16px'
    });
    okButton.onclick = () => {
      document.body.removeChild(modalOverlay);
    };
    modalBox.appendChild(okButton);

    modalOverlay.appendChild(modalBox);
    document.body.appendChild(modalOverlay);
    okButton.focus();
  }

  const addFontAwesome = () => {
    if (!document.querySelector('link[href*="font-awesome"]')) {
      logDebugMain("Injecting Font Awesome.");
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css';
      document.head.appendChild(link);
    }
  };

  /*** BUTTON + ICON CREATORS ***/
  function createVerifyButton(label, className, onClick, id) {
    const button = document.createElement('button');
    button.type = 'button';
    button.className = className; // Base classes like btn btn-sm
    button.classList.add(id); // Specific ID class like verify1-btn
    button.innerText = label;

    // Apply consistent styling, overriding potential theme issues
    const styles = {
      'font-family': 'Arial, sans-serif',
      'font-size': '14px',
      'font-weight': 'normal',
      'color': '#ffffff',
      'background-color': className.includes('success') ? '#28a745' : '#2594d9', // Initial colors
      'padding': '8px 16px',
      'border': 'none',
      'border-radius': '5px',
      'text-shadow': 'none',
      'cursor': 'pointer',
      'margin-right': '5px',
      'line-height': '1.5',
      'vertical-align': 'middle',
    };

    for (const [prop, value] of Object.entries(styles)) {
      button.style.setProperty(prop, value, 'important');
    }

    button.onclick = onClick;
    return button;
  }

  function createToggleIcon(id, isActive, onClick) {
    const icon = document.createElement('span');
    icon.id = id;
    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';
    icon.style.marginLeft = '-1px'; // Fine-tuning position
    icon.title = "Toggle: Go back automatically after this verification success toast is clicked";
    icon.onclick = onClick;
    return icon;
  }

  function handleVerifyToggle(type) {
    logDebugMain(`Toggling auto-back for ${type}`);
    const toggle = type === 'verify1' ? !verify1Toggle : !verify2Toggle;
    localStorage.setItem(type + 'Toggle', toggle);
    const icon = document.querySelector(`#${type}Icon i`);
    if (icon) icon.style.setProperty('color', toggle ? '#008000' : '#d1cfcf', 'important');

    if (type === 'verify1') verify1Toggle = toggle;
    else verify2Toggle = toggle;
  }

  /*** BUTTONS INJECTION ***/
  function addButtons() {
    if (document.getElementById('custom-script-buttons') || !isCorrectPage()) return;

    const nextButton = document.querySelector(CONFIG_MAIN.SELECTORS.NEXT_BTN);
    if (!nextButton || !nextButton.parentNode) {
        // Wait a bit and retry if the next button isn't rendered yet
        setTimeout(addButtons, 500);
        return;
    }

    logDebugMain("Adding custom buttons to page.");
    const buttonDiv = document.createElement('div');
    buttonDiv.id = 'custom-script-buttons';
    buttonDiv.style.setProperty('display', 'inline-block', 'important');
    buttonDiv.style.setProperty('margin-left', '10px', 'important');
    buttonDiv.style.setProperty('vertical-align', 'middle', 'important');

    const verify1Button = createVerifyButton('VERIFY1 (F7)', 'btn btn-success btn-sm', () => {
      logDebugMain('VERIFY1 (F7) clicked.');
      verify1Clicked = true;
      verify2Clicked = false;
      checkAllVisibleBoxesWithoutDuplicates();
      setTimeout(clickCompleteTechnicalVerificationButton, 500); // Delay for checkbox processing
    }, 'verify1-btn');

    const verify2Button = createVerifyButton('VERIFY2 (F8)', 'btn btn-primary btn-sm', () => {
      logDebugMain('VERIFY2 (F8) clicked.');
      verify2Clicked = true;
      verify1Clicked = false;
      checkAllVisibleBoxesWithoutDuplicates();
      setTimeout(clickCompleteMedicalVerificationButton, 500); // Delay for checkbox processing
    }, 'verify2-btn');

    const verify1Icon = createToggleIcon('verify1Icon', verify1Toggle, () => handleVerifyToggle('verify1'));
    const verify2Icon = createToggleIcon('verify2Icon', verify2Toggle, () => handleVerifyToggle('verify2'));

    const credit = document.createElement('span');
    credit.textContent = "Modded by: Hamad AlShegifi";
    credit.style.cssText = `
      font-size: 11px !important;
      font-weight: bold !important;
      color: #e60000 !important;
      margin-left: 15px !important;
      border: 1px solid #e60000 !important;
      border-radius: 5px !important;
      padding: 3px 6px !important;
      background-color: #ffffff !important;
      vertical-align: middle !important;
    `;

    buttonDiv.append(verify1Button, verify1Icon, verify2Button, verify2Icon, credit);
    // Insert after the next button's parent container if needed, adjust as per actual DOM
    nextButton.parentNode.insertBefore(buttonDiv, nextButton.nextSibling);
    logDebugMain("Custom buttons added.");
  }

  /*** CORE ACTIONS ***/
  function checkAllVisibleBoxesWithoutDuplicates() {
    logDebugMain("Checking all visible boxes...");
    const selectedTests = new Set();
    const boxes = document.querySelectorAll(CONFIG_MAIN.SELECTORS.UNCHECKED_BOX);
    let count = 0;
    boxes.forEach(box => {
      const row = box.closest(CONFIG_MAIN.SELECTORS.CHECKBOX_ROW);
      // Ensure the row is actually visible (offsetParent check)
      if (row && row.offsetParent !== null) {
        const testDescElement = row.querySelector(CONFIG_MAIN.SELECTORS.TEST_DESC_CELL);
        const descText = testDescElement?.textContent.trim().toLowerCase();

        if (descText) {
            const isExcluded = CONFIG_MAIN.EXCLUDE_WORDS.some(word => descText.includes(word));
            if (!selectedTests.has(descText) && !isExcluded) {
                selectedTests.add(descText);
                box.click();
                count++;
                // logDebugMain(`Clicked box for test: ${descText}`);
             } else if (selectedTests.has(descText)) {
                // logDebugMain(`Skipping duplicate test: ${descText}`);
             } else if (isExcluded) {
                // logDebugMain(`Skipping excluded test: ${descText}`);
             }
        } else {
            logDebugMain("Found checkbox in row without valid test description.");
        }
      }
    });
    logDebugMain(`Clicked ${count} checkboxes.`);
  }

  const clickButton = (selector, callback) => {
      const btn = document.querySelector(selector);
      if (btn && !btn.disabled) {
          logDebugMain(`Clicking button: ${selector}`);
          btn.click();
          if (callback) {
              // Add delay before executing next step (e.g., final verify)
              setTimeout(callback, 500);
          }
          return true;
      } else if (btn && btn.disabled) {
          logDebugMain(`Button found but disabled: ${selector}`);
          // Show alert for disabled button
          const buttonName = selector.includes('CompleteTechnicalVerification') ? 'Complete Technical Verification' :
                           selector.includes('CompleteMedicalVerification') ? 'Complete Medical Verification' :
                           selector.includes('Verify') ? 'Final Verify' : 'Button';
          showDisabledButtonAlert(`${buttonName} button is disabled. Please check if you have selected all required tests or if verification is already done.`);
      } else {
          logDebugMain(`Button not found: ${selector}`);
      }
      return false;
  };

  const clickCompleteTechnicalVerificationButton = () => {
      clickButton(CONFIG_MAIN.SELECTORS.COMPLETE_TECH, clickFinalVerifyButton);
  };

  const clickCompleteMedicalVerificationButton = () => {
      clickButton(CONFIG_MAIN.SELECTORS.COMPLETE_MED, clickFinalVerifyButton);
  };

  const clickFinalVerifyButton = () => {
      clickButton(CONFIG_MAIN.SELECTORS.FINAL_VERIFY);
  };

  // Function to periodically check for disabled buttons
  function checkForDisabledButtons() {
    if (!isCorrectPage()) return;

    // Check if any verification buttons are disabled when they shouldn't be
    const verify1Btn = document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN);
    const verify2Btn = document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY2_BTN);

    if (verify1Btn && verify1Btn.disabled) {
      logDebugMain("VERIFY1 button is disabled");
      showDisabledButtonAlert("varification is disabled. Make sure tests not already verified.");
    }

    if (verify2Btn && verify2Btn.disabled) {
      logDebugMain("VERIFY2 button is disabled");
      showDisabledButtonAlert("varification is disabled. Make sure tests not already verified.");
    }
  }

  // Handles the '/undefined' URL issue by closing toasts
  function checkUrlAndTriggerClickForUndefined() {
    if (window.location.href.endsWith('/undefined')) {
        logDebugMain("Detected '/undefined' URL. Attempting to close toast.");
        const closeBtn = document.querySelector(`${CONFIG_MAIN.SELECTORS.TOAST.CONTAINER} ${CONFIG_MAIN.SELECTORS.TOAST.CLOSE_BTN}`);
        if (closeBtn) {
            closeBtn.click();
            logDebugMain("Clicked toast close button.");
            // Optionally, navigate back or to a known safe page
            // window.history.back();
        }
    }
  }

  // Monitors for 'Ordered' status and updates VERIFY1 button/scrolls
  function monitorOrderedStatus() {
    const rows = document.querySelectorAll('div[role="row"]');
    let hasOrdered = false;
    let firstOrderedRow = null;

    rows.forEach(row => {
        // Ensure row is potentially visible before querying deeper
        if (row.offsetParent !== null) {
            const cell = row.querySelector(CONFIG_MAIN.SELECTORS.ORDERED_STATUS_CELL);
            if (cell?.textContent.includes('Ordered')) {
                if (!firstOrderedRow) firstOrderedRow = row;
                hasOrdered = true;
            }
        }
    });

    const btn = document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN);
    if (btn) {
      if (hasOrdered) {
        // Update button style only if it's not already in warning state
        if (!btn.classList.contains('btn-warning')) {
            logDebugMain("Found 'Ordered' status, updating VERIFY1 button.");
            btn.className = 'btn btn-warning verify1-btn'; // Ensure base btn class remains
            btn.innerText = 'VERIFY1 (F7)';
            btn.style.setProperty('background-color', '#fab641', 'important'); // Orange/Yellow
            btn.style.setProperty('color', '#050505', 'important'); // Dark text for contrast
        }

        // Scroll the first ordered row into view (centered) only if we haven't scrolled yet for this page view
        if (firstOrderedRow && !hasScrolledToOrderedRow) {
          logDebugMain("Scrolling to first 'Ordered' row.");
          // Temporarily set smooth scroll for the action
          document.documentElement.style.scrollBehavior = 'smooth';
          firstOrderedRow.scrollIntoView({ behavior: 'auto', block: 'center' }); // 'auto' behavior uses thedocumentElement style

          hasScrolledToOrderedRow = true; // Mark as scrolled to prevent re-scrolling
          // Reset scroll behavior after a delay to allow scroll to finish and permit manual scrolling
          setTimeout(() => {
            document.documentElement.style.scrollBehavior = 'auto';
            logDebugMain("Scroll complete, scroll behavior reset.");
          }, 1000); // 1 second delay
        }
      } else {
        // Reset button style only if it's currently in warning state
         if (btn.classList.contains('btn-warning')) {
            logDebugMain("No 'Ordered' status found, resetting VERIFY1 button.");
            btn.className = 'btn btn-success btn-sm verify1-btn'; // Ensure base classes remain
            btn.innerText = 'VERIFY1 (F7)';
            btn.style.setProperty('background-color', '#28a745', 'important'); // Default green
            btn.style.setProperty('color', '#ffffff', 'important'); // Default white text
         }
         // Reset scroll flag if page no longer has ordered items
         hasScrolledToOrderedRow = false;
      }
    }
  }

  // Use setInterval for periodic checks
  setInterval(monitorOrderedStatus, CONFIG_MAIN.CHECK_INTERVALS.ORDERED_SCAN);


  /*** TOAST OBSERVER (for Auto-Back) ***/
  const toastObserver = new MutationObserver(mutations => {
    mutations.forEach(mutation => {
      mutation.addedNodes.forEach(node => {
        // Check if the added node itself is a success toast or contains one
        if (node.nodeType === Node.ELEMENT_NODE) {
            let successToast = null;
            if (node.matches(CONFIG_MAIN.SELECTORS.TOAST.SUCCESS)) {
                successToast = node;
            } else if (node.querySelector) {
                 successToast = node.querySelector(CONFIG_MAIN.SELECTORS.TOAST.SUCCESS);
            }

            if (successToast) {
                logDebugMain("Success toast detected.");
                // Add listener to the specific toast that appeared
                successToast.addEventListener('click', () => {
                    logDebugMain("Success toast clicked.");
                    if ((verify1Clicked && verify1Toggle) || (verify2Clicked && verify2Toggle)) {
                        logDebugMain("Auto-back triggered.");
                        window.history.back();
                    }
                    // Reset flags regardless of whether back navigation occurred
                    verify1Clicked = false;
                    verify2Clicked = false;
                 }, { once: true }); // Use { once: true } so the listener removes itself after firing
            }
        }
      });
    });
  });

  // Observe the body for toasts appearing (might need a more specific container if available)
  toastObserver.observe(document.body, { childList: true, subtree: true });


  // --- Start of Integrated Alerts Scanner Code ---
  (function () {
    'use strict'; // Inner scope strict mode

    const CONFIG_ALERTS = {
        SCAN_INTERVAL: 150, // Faster scan for critical alerts
        FLASH_COLOR: "pink",
        FLASH_INTERVAL: 500,
        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', // Note: Original script had typo "critial-alret" - keeping it if that's the actual class name
        CHECKBOX_PARENT_ROW_SELECTOR: '.ag-row', // Re-using main config selector concept
        NO_RESULT_MESSAGE: "NO-RESULT DETECTED!!",

    };

    let isScanningActive = false;
    let issueScanIntervalId = null;
    let alertAudio = null; // Reuse audio object

    function logDebugAlerts(message) {
        console.log(`[Alerts Scanner] ${message}`);
    }

    function playAlertSound() {
        if (!alertAudio) {
            alertAudio = new Audio(CONFIG_ALERTS.SOUND_URL);
            alertAudio.loop = true; // Loop the sound
        }
        alertAudio.play().catch(e => console.warn("[Alerts Scanner] Sound playback blocked by browser:", e));
        // Stop the sound after a few seconds
        setTimeout(() => {
            if (alertAudio) {
                alertAudio.pause();
                alertAudio.currentTime = 0; // Reset for next play
            }
        }, 3000); // Play for 3 seconds
    }

    // function isVisible(element) { // Already checked by offsetParent in main script, might not be needed here if checks use row context
    //     return !!(element && (element.offsetWidth || element.offsetHeight || element.getClientRects().length));
    // }

    function applyFlashingEffect(row) {
        if (!row || row.dataset.flashing === 'true') return; // Already flashing or invalid row

        row.dataset.flashing = 'true';
        const originalBg = row.style.backgroundColor || 'transparent'; // Get current or default
        row.dataset.originalBg = originalBg;
        row.style.transition = "background-color 0.5s ease"; // Smooth transition
        let isPink = false;

        // Store interval ID on the element itself to manage it
        const intervalId = setInterval(() => {
            // Check if the row still exists and is still meant to be flashing
            if (!document.body.contains(row) || row.dataset.flashing === 'false') {
                clearInterval(intervalId);
                row.style.transition = ''; // Remove transition before resetting color
                row.style.setProperty("background-color", row.dataset.originalBg || 'transparent', "important");
                // Clean up data attributes
                delete row.dataset.flashing;
                delete row.dataset.originalBg;
                delete row.dataset.flashIntervalId;
                return;
            }
            isPink = !isPink;
            row.style.setProperty("background-color", isPink ? CONFIG_ALERTS.FLASH_COLOR : originalBg, "important");
        }, CONFIG_ALERTS.FLASH_INTERVAL);

        row.dataset.flashIntervalId = intervalId; // Store reference for stopping
    }

    function stopFlashingEffect(row) {
        if (row && row.dataset.flashing === 'true') {
            row.dataset.flashing = 'false'; // Signal the interval to stop and clean up
            // The interval itself handles clearing and resetting style
        }
    }

    // Use sessionStorage to track notifications *per browser session* for specific issues
    function getNotificationSessionKey(type, identifier = 'general') {
        // Include pathname to make keys specific to the current view/page if URL changes within the SPA
        return `labAlertNotified_${window.location.pathname}_${type}_${identifier}`;
    }

    function hasAlreadyNotified(type, identifier = 'general') {
        return sessionStorage.getItem(getNotificationSessionKey(type, identifier)) === 'true';
    }

    function setNotificationFlag(type, identifier = 'general') {
        sessionStorage.setItem(getNotificationSessionKey(type, identifier), 'true');
    }

    // Creates a custom modal alert dialog
    function createCustomAlert(message, callback) {
        // Prevent duplicate alerts if one is already showing
        if (document.getElementById('custom-alert-modal-overlay')) return;

        logDebugAlerts(`Displaying custom alert: ${message}`);
        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'custom-alert-modal-overlay'; // ID to prevent duplicates
        // Basic styling for overlay
        Object.assign(modalOverlay.style, {
            position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh',
            backgroundColor: 'rgba(0, 0, 0, 0.6)', zIndex: '10000', display: 'flex',
            justifyContent: 'center', alignItems: 'center'
        });

        const modalBox = document.createElement('div');
         // Basic styling for modal box
        Object.assign(modalBox.style, {
            backgroundColor: '#fff', padding: '30px', borderRadius: '8px',
            boxShadow: '0 5px 15px rgba(0,0,0,0.2)', width: 'auto', minWidth:'300px', maxWidth: '90%',
            textAlign: 'center', borderTop: '5px solid #f0ad4e' // Warning color top border
        });

        const messageElem = document.createElement('p');
        messageElem.textContent = message;
        messageElem.style.fontSize = '16px';
        messageElem.style.marginBottom = '20px';
        modalBox.appendChild(messageElem);

        const okButton = document.createElement('button');
        okButton.textContent = 'OK';
         // Basic styling for button
         Object.assign(okButton.style, {
            padding: '10px 25px', borderRadius: '5px', backgroundColor: '#5cb85c', // Green color
            color: '#fff', border: 'none', cursor: 'pointer', fontSize: '16px'
        });
        okButton.onclick = () => {
            logDebugAlerts("User acknowledged alert.");
            document.body.removeChild(modalOverlay);
            if (callback) callback();
        };
        modalBox.appendChild(okButton);
        modalOverlay.appendChild(modalBox);

        document.body.appendChild(modalOverlay);
        okButton.focus(); // Focus the OK button
    }

    function checkForIssues() {
        const resultDivs = document.querySelectorAll(CONFIG_ALERTS.RESULT_CELL_SELECTOR);
        const criticalDivs = document.querySelectorAll(CONFIG_ALERTS.CRITICAL_FLAG_SELECTOR);
        const potentialAlerts = []; // Collect potential new alerts for this scan cycle

        function scrollToRowNearest(element) {
            const row = element.closest(CONFIG_ALERTS.CHECKBOX_PARENT_ROW_SELECTOR);
            if (row) {
                logDebugAlerts("Scrolling to issue row.");
                document.documentElement.style.scrollBehavior = 'smooth'; // Ensure smooth scroll for alerts too
                try {
                    // Center is often better for drawing attention than 'nearest'
                    row.scrollIntoView({ behavior: 'auto', block: 'center' });
                } catch (e) { // Fallback
                     row.scrollIntoView();
                }
                 // Reset scroll behavior after a short delay
                setTimeout(() => { document.documentElement.style.scrollBehavior = 'auto'; }, 500);
                return true;
            }
            return false;
        }

        // --- Check for Critical Flags ---
        criticalDivs.forEach((div, index) => {
            const rowElement = div.closest(CONFIG_ALERTS.CHECKBOX_PARENT_ROW_SELECTOR);
            if (rowElement) { // Always trigger, even if offscreen
                 const text = div.textContent?.trim() || '';
                 if (text === "CL" || text === "CH") {
                    const specificRowKey = rowElement.getAttribute('row-id') || `${text}_${index}`; // Unique identifier
                    if (!hasAlreadyNotified('critical', specificRowKey)) {
                        const message = text === "CL" ? "CRITICAL LOW RESULT DETECTED !!" : "CRITICAL HIGH RESULT DETECTED !!";
                        logDebugAlerts(`Found potential critical alert: ${message} (Key: ${specificRowKey})`);
                        potentialAlerts.push({
                            element: div, rowElement: rowElement, type: 'critical',
                            message: message, rowId: specificRowKey
                        });
                    } else {
                         // Apply flashing even if notified, as it might be a persistent issue
                         applyFlashingEffect(rowElement);
                    }
                 }
            }
        });

        // --- Check for No Results and Dilution ('>') ---
        resultDivs.forEach((div, index) => {
            const rowElement = div.closest(CONFIG_ALERTS.CHECKBOX_PARENT_ROW_SELECTOR);
             if (rowElement) { // Always trigger, even if offscreen
                const text = div.textContent?.trim().toLowerCase() || '';
                const isNoResultType = (text === "no result" || text === "no-xresult" || text === "x-noresult"); // Add other variations if needed
                const needsDilution = text.includes(">");

                // Handle No Result
                if (isNoResultType) {
                    const specificRowKey = rowElement.getAttribute('row-id') || `noresult_${index}`;
                     if (!hasAlreadyNotified('noresult', specificRowKey)) {
                        logDebugAlerts(`Found potential no-result alert. (Key: ${specificRowKey})`);
                        potentialAlerts.push({
                             element: div, rowElement: rowElement, type: 'noresult',
                             message: CONFIG_ALERTS.NO_RESULT_MESSAGE, rowId: specificRowKey
                         });
                    } else {
                        applyFlashingEffect(rowElement);
                    }
                }

                 // Handle Dilution (>)
                 if (needsDilution) {
                     const specificRowKey = rowElement.getAttribute('row-id') || `dilution_${index}`;
                     if (!hasAlreadyNotified('greaterThan', specificRowKey)) {
                        logDebugAlerts(`Found potential dilution alert. (Key: ${specificRowKey})`);
                         potentialAlerts.push({
                             element: div, rowElement: rowElement, type: 'greaterThan',
                             message: "Dilution is required for this sample (> detected)!", rowId: specificRowKey
                         });
                     } else {
                         // Always flash if dilution is needed and already notified
                         applyFlashingEffect(rowElement);
                     }
                 }
            }
        });

        // --- Process Alerts ---
        if (potentialAlerts.length > 0) {
            // Find the *first* new alert in the list to display and scroll to
            const alertToShow = potentialAlerts.find(alert => !hasAlreadyNotified(alert.type, alert.rowId));

            if (alertToShow) {
                logDebugAlerts(`Triggering alert for type: ${alertToShow.type}, rowId: ${alertToShow.rowId}`);
                playAlertSound();
                setNotificationFlag(alertToShow.type, alertToShow.rowId); // Mark as notified *before* showing alert
                applyFlashingEffect(alertToShow.rowElement); // Apply flashing effect
                scrollToRowNearest(alertToShow.element); // Scroll to the problematic element/row
                createCustomAlert(alertToShow.message); // Show the custom modal
            }

             // Apply flashing to all potential alerts found in this cycle, even if not the primary one shown
             potentialAlerts.forEach(alert => {
                 applyFlashingEffect(alert.rowElement);
             });

            return true; // Indicates an issue was found/processed
        }
        return false; // No new issues found this cycle
    }


    function startContinuousScanning() {
        if (isScanningActive) return;
        logDebugAlerts("Starting continuous issue scanning...");
        isScanningActive = true;
        // Clear any previous interval just in case
        if (issueScanIntervalId) clearInterval(issueScanIntervalId);

        // Initial check
        checkForIssues();

        // Start interval
        issueScanIntervalId = setInterval(checkForIssues, CONFIG_ALERTS.SCAN_INTERVAL);
    }

    function stopContinuousScanning() {
        if (!isScanningActive) return; // Only stop if active
        logDebugAlerts("Stopping continuous issue scanning.");
        if (issueScanIntervalId) {
            clearInterval(issueScanIntervalId);
            issueScanIntervalId = null;
        }
         // Stop flashing on all rows that might still be flashing
        document.querySelectorAll(CONFIG_ALERTS.CHECKBOX_PARENT_ROW_SELECTOR + '[data-flashing="true"]').forEach(stopFlashingEffect);
        isScanningActive = false;

        // Stop and reset audio if it's playing
         if (alertAudio && !alertAudio.paused) {
             alertAudio.pause();
             alertAudio.currentTime = 0;
         }
    }

    // Use MutationObserver to automatically start/stop scanning when results grid appears/disappears
    const observer = new MutationObserver((mutationsList, observer) => {
        // Check if the relevant grid elements exist now
        const resultsGridExists = document.querySelector(CONFIG_ALERTS.RESULT_CELL_SELECTOR) || document.querySelector(CONFIG_ALERTS.CRITICAL_FLAG_SELECTOR);

        if (resultsGridExists) {
            if (!isScanningActive) {
                 // Delay slightly before starting scan after elements appear, allowing grid to fully render
                setTimeout(startContinuousScanning, 300);
            }
        } else {
            if (isScanningActive) {
                stopContinuousScanning();
            }
        }
    });

     // Observe the body for broad changes, necessary for SPA navigation
    observer.observe(document.body, { childList: true, subtree: true });

    // Initial check in case the grid is already present on script load
    if (document.querySelector(CONFIG_ALERTS.RESULT_CELL_SELECTOR) || document.querySelector(CONFIG_ALERTS.CRITICAL_FLAG_SELECTOR)) {
        startContinuousScanning();
    }

  })(); // End of Integrated Alerts Scanner IIFE
  // --- End of Integrated Alerts Scanner Code ---


  /*** HOTKEYS ***/
  document.addEventListener('keydown', e => {
    // Ignore keydowns if focus is inside an input/textarea/select
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
      return;
    }

    if (e.key === 'F7') {
      e.preventDefault();
      logDebugMain("F7 key pressed.");
      document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN)?.click();
    } else if (e.key === 'F8') {
      e.preventDefault();
      logDebugMain("F8 key pressed.");
      document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY2_BTN)?.click();
    }
  });

  /*** MAIN PAGE WATCHER (for SPA Navigation) ***/
  const pageObserver = new MutationObserver(() => {
    if (isCorrectPage()) {
      // If on the correct page, ensure setup is complete
      addFontAwesome(); // Ensure FontAwesome is loaded
      if (!document.getElementById('custom-script-buttons')) {
           // Buttons not present, try adding them (handles SPA navigation where elements might be removed/re-added)
           addButtons();
           // Reset scroll flag on navigation to the correct page
           hasScrolledToOrderedRow = false;
      }
    } else {
      // If not on the correct page, remove custom elements
      const customButtons = document.getElementById('custom-script-buttons');
      if (customButtons) {
        logDebugMain("Navigated away from edit page, removing custom buttons.");
        customButtons.remove();
      }
      // Stop alert scanner if navigating away (though the alert scanner's own observer should also handle this)
      // Consider calling stopContinuousScanning() here if needed, but it might be redundant.
    }
  });

   // Observe the body for major changes that might indicate page navigation in an SPA
  pageObserver.observe(document.body, { childList: true, subtree: true });

  /*** INITIAL BOOTSTRAP ***/
  logDebugMain("KAAUH Lab Suite script initializing...");
  if (isCorrectPage()) {
    addFontAwesome();
    addButtons(); // Initial attempt to add buttons
  }
  // Start the interval check for the '/undefined' URL issue immediately
  setInterval(checkUrlAndTriggerClickForUndefined, CONFIG_MAIN.CHECK_INTERVALS.UNDEFINED_URL);
  // The 'monitorOrderedStatus' interval is started within its own definition block
  // The 'Alerts Scanner' starts based on its MutationObserver detecting the grid
  // Start disabled button checker
  setInterval(checkForDisabledButtons, CONFIG_MAIN.CHECK_INTERVALS.DISABLED_BTN_CHECK);

})(); // End of Main UserScript IIFE