您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
当前为
// ==UserScript== // @name KAAUH Lab Suite - Verification, Alerts & Enhancements // @namespace Violentmonkey Scripts // @version 6.3 // @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)🚩incomplete!!'; 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 && rowElement.offsetParent !== null) { // Check visibility 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 && rowElement.offsetParent !== null) { // Check visibility 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
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址