您需要先安装一个扩展,例如 篡改猴、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 Enhancement Suite 1 (Buttons, Alerts, Auto-Actions) - Modified Alert Handling // @namespace Violentmonkey Scripts // @version 5.7 // @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; } (function () { 'use strict'; function waitForElement(selector, callback, interval = 500, timeout = 10000) { const startTime = Date.now(); const check = () => { const element = document.querySelector(selector); if (element) return callback(element); if (Date.now() - startTime > timeout) return; setTimeout(check, interval); }; check(); } function checkForOrdered() { const cells = document.querySelectorAll('div[col-id="ResultStatus"]'); let hasOrdered = false; cells.forEach(cell => { if (cell.textContent.toLowerCase().includes('ordered')) { hasOrdered = true; } }); if (hasOrdered) { const buttons = document.querySelectorAll('button.btn.btn-success'); buttons.forEach(btn => { if (btn.textContent.trim() === 'VERIFY1 (F7)') { btn.style.setProperty('background-color', 'orange', 'important'); } }); } } waitForElement('div[col-id="ResultStatus"]', () => { checkForOrdered(); const observer = new MutationObserver(checkForOrdered); observer.observe(document.body, { childList: true, subtree: true }); }); })(); // --- 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
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址