您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Combines verification buttons (F7/F8), dynamic alerts (>, NO RESULT, X-NORESULT, CL/CH) showing alerts once per trigger per page visit, checkbox automation, toggle back-nav, and inline sample counters.
当前为
// ==UserScript== // @name KAAUH Lab Suite - Verification, Alerts & Enhancements // @namespace Violentmonkey Scripts // @version 7.1 // @description Combines verification buttons (F7/F8), dynamic alerts (>, NO RESULT, X-NORESULT, CL/CH) showing alerts once per trigger per page visit, checkbox automation, toggle back-nav, and inline sample counters. // @match *://his.kaauh.org/lab/* // @grant none // @author Hamad AlShegifi // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Styling Adjustments (Global) --- 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, ''); } }; simplifySpan('span.to-do'); simplifySpan('span.pending-orders'); } function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } const debouncedStyleUpdater = debounce(applySingleLineStyles, 300); const styleObserver = new MutationObserver(debouncedStyleUpdater); styleObserver.observe(document.body, { childList: true, subtree: true }); applySingleLineStyles(); // Initial application })(); // Main Userscript Logic (Alerts, F7/F8 buttons, etc.) (function () { 'use strict'; 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"]', ORDERED_STATUS_CELL: 'div[col-id="ResultStatus"]', TOAST: { CONTAINER: '#toast-container', CLOSE_BTN: 'button.toast-close-button', SUCCESS: '.toast-success', }, SAMPLE_RECEIVE_MODAL: 'modal-container.show', // Used by the sample counter part }, CHECK_INTERVALS: { UNDEFINED_URL: 200, ORDERED_SCAN: 500, DISABLED_BTN_CHECK: 1000, }, EXCLUDE_WORDS: [ // Words to exclude from auto-checkbox selection 'culture', "gram's stain", 'stain', 'bacterial', 'fungal', 'pcr', 'meningitis', 'mrsa', 'mid', 'stream', 'cryptococcus' ] }; let verify1Clicked = false; let verify2Clicked = false; let verify1Toggle = localStorage.getItem('verify1Toggle') === 'true'; let verify2Toggle = localStorage.getItem('verify2Toggle') === 'true'; let hasScrolledToOrderedRow = false; let lastDisabledButtonAlertTime = 0; const DISABLED_ALERT_COOLDOWN = 30000; // 30 seconds const logDebugMain = msg => console.debug(`[LabScript Main] ${msg}`); const isCorrectPage = () => window.location.href.startsWith(CONFIG_MAIN.URLS.EDIT_PAGE_PREFIX); function showDisabledButtonAlert(message) { const now = Date.now(); if (now - lastDisabledButtonAlertTime < DISABLED_ALERT_COOLDOWN) return; lastDisabledButtonAlertTime = now; 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'; // Danger 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', // Success color for button 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(); // Focus on OK button for accessibility } function addFontAwesome() { const fontAwesomeLink = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'; if (!document.querySelector(`link[href="${fontAwesomeLink}"]`)) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = fontAwesomeLink; const head = document.head || document.body; // Prefer document.head if (head) head.appendChild(link); } } 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; // Common styles, ensuring they override existing ones if necessary const styles = { 'font-family': 'Arial, sans-serif', 'font-size': '14px', 'font-weight': 'normal', // Bootstrap buttons often have bolder weight 'color': '#ffffff', 'background-color': className.includes('success') ? '#28a745' : '#2594d9', // Green for success, blue for primary 'padding': '8px 16px', 'border': 'none', 'border-radius': '5px', 'text-shadow': 'none', // Remove any default text shadow 'cursor': 'pointer', 'margin-right': '5px', 'line-height': '1.5', // Standard line height 'vertical-align': 'middle', // Align with other inline elements }; 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; // Using FontAwesome icon for toggle icon.innerHTML = `<i class="fas fa-arrow-circle-left" 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'; // Slight adjustment for alignment icon.title = "Toggle: Go back automatically after this verification success toast is clicked"; icon.onclick = onClick; return icon; } function handleVerifyToggle(type) { const toggle = type === 'verify1' ? !verify1Toggle : !verify2Toggle; localStorage.setItem(type + 'Toggle', toggle); // Save preference const icon = document.querySelector(`#${type}Icon i`); if (icon) icon.style.setProperty('color', toggle ? '#008000' : '#d1cfcf', 'important'); // Green for active, grey for inactive if (type === 'verify1') verify1Toggle = toggle; else verify2Toggle = toggle; } function addButtons() { if (document.getElementById('custom-script-buttons') || !isCorrectPage()) return; const nextButton = document.querySelector(CONFIG_MAIN.SELECTORS.NEXT_BTN); if (!nextButton || !nextButton.parentNode) { setTimeout(addButtons, 500); // Retry if next button not found yet return; } 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', () => { verify1Clicked = true; verify2Clicked = false; checkAllVisibleBoxesWithoutDuplicates(); setTimeout(clickCompleteTechnicalVerificationButton, 500); // Delay to allow checkboxes to register }, 'verify1-btn'); const verify2Button = createVerifyButton('VERIFY2 (F8)', 'btn btn-primary btn-sm', () => { verify2Clicked = true; verify1Clicked = false; checkAllVisibleBoxesWithoutDuplicates(); setTimeout(clickCompleteMedicalVerificationButton, 500); // Delay }, 'verify2-btn'); const verify1Icon = createToggleIcon('verify1Icon', verify1Toggle, () => handleVerifyToggle('verify1')); const verify2Icon = createToggleIcon('verify2Icon', verify2Toggle, () => handleVerifyToggle('verify2')); // Credit span 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; /* Bright red for visibility */ margin-left: 15px !important; border: 1px solid #e60000 !important; border-radius: 5px !important; padding: 3px 6px !important; background-color: #ffffff !important; /* White background for contrast */ vertical-align: middle !important; `; buttonDiv.append(verify1Button, verify1Icon, verify2Button, verify2Icon, credit); nextButton.parentNode.insertBefore(buttonDiv, nextButton.nextSibling); logDebugMain("Custom buttons added."); } function checkAllVisibleBoxesWithoutDuplicates() { const selectedTests = new Set(); // To track selected test descriptions and avoid duplicates const boxes = document.querySelectorAll(CONFIG_MAIN.SELECTORS.UNCHECKED_BOX); boxes.forEach(box => { const row = box.closest(CONFIG_MAIN.SELECTORS.CHECKBOX_ROW); // Check if the row is visible (offsetParent is not null) if (row && row.offsetParent !== null) { const testDescElement = row.querySelector(CONFIG_MAIN.SELECTORS.TEST_DESC_CELL); const descText = testDescElement?.textContent.trim().toLowerCase(); if (descText) { // Check if any part of the description text includes an excluded word const isExcluded = CONFIG_MAIN.EXCLUDE_WORDS.some(word => descText.includes(word)); if (!selectedTests.has(descText) && !isExcluded) { selectedTests.add(descText); // Add to set to prevent re-checking if description is identical box.click(); } } } }); logDebugMain(`Checked ${selectedTests.size} unique, non-excluded, visible boxes.`); } const clickButton = (selector, callback) => { const btn = document.querySelector(selector); if (btn && !btn.disabled) { btn.click(); if (callback) setTimeout(callback, 500); // Short delay for UI updates return true; } else if (btn && btn.disabled) { 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.`); } 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 checkForDisabledButtons() { if (!isCorrectPage()) return; const verify1Btn = document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN); const verify2Btn = document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY2_BTN); // This function is more for proactive checks if needed, showDisabledButtonAlert is called on actual click attempt. // Could be used to visually indicate disabled state if desired. if (verify1Btn && verify1Btn.disabled) { // logDebugMain("VERIFY1 button is currently disabled on page."); } if (verify2Btn && verify2Btn.disabled) { // logDebugMain("VERIFY2 button is currently disabled on page."); } } function checkUrlAndTriggerClickForUndefined() { if (window.location.href.endsWith('/undefined')) { const closeBtn = document.querySelector(`${CONFIG_MAIN.SELECTORS.TOAST.CONTAINER} ${CONFIG_MAIN.SELECTORS.TOAST.CLOSE_BTN}`); if (closeBtn) { logDebugMain("URL ends with /undefined, attempting to close toast."); closeBtn.click(); } } } function monitorOrderedStatus() { if (!isCorrectPage()) return; const rows = document.querySelectorAll('div[role="row"]'); // More generic row selector let hasOrdered = false; let firstOrderedRow = null; rows.forEach(row => { // Ensure row is visible if (row.offsetParent !== null) { // Check if element is visible on the page const cell = row.querySelector(CONFIG_MAIN.SELECTORS.ORDERED_STATUS_CELL); if (cell?.textContent.includes('Ordered')) { if (!firstOrderedRow) firstOrderedRow = row; // Get the first visible ordered row hasOrdered = true; } } }); const btn = document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN); if (btn) { if (hasOrdered) { if (!btn.classList.contains('btn-warning')) { // Check if already warning btn.className = 'btn btn-warning verify1-btn'; // Change class for styling btn.innerText = 'VERIFY1 (F7)'; // Keep text consistent btn.style.setProperty('background-color', '#fab641', 'important'); // Orange/yellow btn.style.setProperty('color', '#050505', 'important'); // Dark text for contrast logDebugMain("Ordered status detected. VERIFY1 button changed to warning."); } if (firstOrderedRow && !hasScrolledToOrderedRow) { document.documentElement.style.scrollBehavior = 'smooth'; // Smooth scroll firstOrderedRow.scrollIntoView({ behavior: 'auto', block: 'center' }); hasScrolledToOrderedRow = true; setTimeout(() => document.documentElement.style.scrollBehavior = 'auto', 1000); // Reset scroll behavior logDebugMain("Scrolled to first 'Ordered' row."); } } else { // No 'Ordered' status found if (btn.classList.contains('btn-warning')) { // Check if it was warning btn.className = 'btn btn-success btn-sm verify1-btn'; // Revert to success btn.innerText = 'VERIFY1 (F7)'; btn.style.setProperty('background-color', '#28a745', 'important'); // Green btn.style.setProperty('color', '#ffffff', 'important'); // White text logDebugMain("No ordered status. VERIFY1 button reverted to success."); } hasScrolledToOrderedRow = false; // Reset scroll flag } } } setInterval(monitorOrderedStatus, CONFIG_MAIN.CHECK_INTERVALS.ORDERED_SCAN); // --- Toast Observer for Auto Back Navigation --- const toastObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { // Check if it's an element node let successToast = null; if (node.matches(CONFIG_MAIN.SELECTORS.TOAST.SUCCESS)) { successToast = node; } else if (node.querySelector) { // Check if it's a container with the toast inside successToast = node.querySelector(CONFIG_MAIN.SELECTORS.TOAST.SUCCESS); } if (successToast) { logDebugMain("Success toast detected."); successToast.addEventListener('click', () => { if ((verify1Clicked && verify1Toggle) || (verify2Clicked && verify2Toggle)) { logDebugMain("Navigating back due to toast click and toggle state."); window.history.back(); } // Reset click flags after any toast interaction verify1Clicked = false; verify2Clicked = false; }, { once: true }); // Ensure listener is added only once } } }); }); }); // Observe the body for toast container additions, common for many toast libraries toastObserver.observe(document.body, { childList: true, subtree: true }); // --- Keyboard Shortcuts (F7/F8) --- document.addEventListener('keydown', e => { // Ignore keydown events if focus is on an input, textarea, or select element if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') { return; } if (e.key === 'F7') { e.preventDefault(); // Prevent default F7 browser action document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN)?.click(); } else if (e.key === 'F8') { e.preventDefault(); // Prevent default F8 browser action document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY2_BTN)?.click(); } }); // --- Integrated Alerts Scanner Code (Critical, Dilution, No-Result) --- // Ensure SheetJS (XLSX) is available if local saving is intended (not included in this script) if (typeof XLSX === 'undefined') { console.warn("[Alerts Scanner] SheetJS library (XLSX) not found. Local XLSX saving will not work."); } const CONFIG_ALERTS = { SCAN_INTERVAL: 750, // ms FLASH_COLOR: "pink", FLASH_INTERVAL: 700, // ms for flashing RESULT_CELL_SELECTOR: 'div[role="gridcell"][col-id="TestResult"] app-result-value-render div', UOM_CELL_SELECTOR: 'div[col-id="UomValue"]', CRITICAL_FLAG_SELECTOR: 'div[role="gridcell"][col-id="LTFlag"] app-ref-high-low div span.critial-alret-indication', TEST_DESC_PINNED_SELECTOR: '.ag-pinned-left-cols-container .ag-row div[col-id="TestDesc"]', // For AG-Grid pinned columns NO_RESULT_MESSAGE: "NO-RESULT DETECTED!", DILUTION_MESSAGE: "DILUTION REQUIRED!", PATIENT_NAME_SELECTOR: 'div.patient-name-full', PATIENT_MRN_SELECTOR: 'div.mid.renal-demography span', // More specific MRN selector PATIENT_LOCATION_SELECTOR: 'span[title*="UNIT/"]', // Assuming location is in a title attribute USER_NAME_SELECTOR: 'div.profile-wrapper span.csi-dropdown-btn-text', // User ID/Name SAMPLE_BARCODE_SELECTOR: 'div[style*="font-size: 13px; color: rgb(68, 68, 68);"]', // Example selector, adjust if needed LOCAL_SERVER_SAVE_URL: 'http://localhost:5000/save-alerts' // Example URL for local server }; let isScanningActive = false; let issueScanIntervalId = null; let isAlertModalOpen = false; // Flag to prevent multiple modals const logAlertDebug = msg => console.debug(`[Alerts Scanner DEBUG] ${msg}`); const logAlertError = msg => console.error(`[Alerts Scanner ERROR] ${msg}`); function applyFlashingEffect(row) { if (!row || row.dataset.flashing === 'true') return; row.dataset.flashing = 'true'; const originalBg = row.style.backgroundColor || 'transparent'; // Store original row.dataset.originalBg = originalBg; row.style.transition = "background-color 0.5s ease"; // Smooth transition let isPink = false; const intervalId = setInterval(() => { if (!document.body.contains(row) || row.dataset.flashing === 'false') { // Stop if row removed or flashing stopped clearInterval(intervalId); row.style.transition = ''; // Remove transition row.style.setProperty("background-color", row.dataset.originalBg || 'transparent', "important"); 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.toString(); // Store interval ID to clear it } function stopFlashingEffect(row) { if (row && row.dataset.flashing === 'true') { row.dataset.flashing = 'false'; // Signal to stop flashing const intervalId = parseInt(row.dataset.flashIntervalId, 10); if (!isNaN(intervalId)) { clearInterval(intervalId); } // The interval itself will handle reverting the style } } function getNotificationSessionKey(type, identifier = 'general') { // Sanitize identifier to prevent issues with special characters in sessionStorage keys const safeIdentifier = String(identifier).replace(/[^a-zA-Z0-9_-]/g, ''); return `labAlertNotified_${window.location.pathname}${window.location.hash}_${type}_${safeIdentifier}`; } function hasAlreadyNotified(type, identifier = 'general') { const key = getNotificationSessionKey(type, identifier); return sessionStorage.getItem(key) === 'true'; } function setNotificationFlag(type, identifier = 'general') { const key = getNotificationSessionKey(type, identifier); logAlertDebug(`Setting notification flag for key: "${key}"`); sessionStorage.setItem(key, 'true'); } function getNextEntryID() { const counterKey = 'labAlertEntryCounter'; let currentID = parseInt(localStorage.getItem(counterKey), 10) || 0; currentID++; localStorage.setItem(counterKey, String(currentID)); logAlertDebug(`Next Entry ID: ${currentID}`); return currentID; } async function sendAlertDataToServer(alertData) { logAlertDebug("Attempting to send data to local server."); if (!CONFIG_ALERTS.LOCAL_SERVER_SAVE_URL) { logAlertError("LOCAL_SERVER_SAVE_URL is not configured."); console.error("Local server save URL is not configured for alerts."); return false; } // Get values from the modal inputs const notifiedPersonNameInput = document.getElementById('notifiedPersonNameInput'); const notifiedPersonTelExtInput = document.getElementById('notifiedPersonTelExtInput'); const readBackCheckbox = document.getElementById('readBackCheckbox'); const userIdInput = document.getElementById('userIdInput'); // User ID from modal const now = new Date(); const date = `${String(now.getDate()).padStart(2, '0')}/${String(now.getMonth() + 1).padStart(2, '0')}/${now.getFullYear()}`; const hours = now.getHours(); const minutes = String(now.getMinutes()).padStart(2, '0'); const ampm = hours >= 12 ? 'pm' : 'am'; const formattedHours = hours % 12 || 12; // Convert 0 to 12 for 12 AM/PM const time = `${formattedHours}:${minutes} ${ampm}`; const entryID = getNextEntryID(); // Get a unique ID for this entry const dataToSend = { entryID: entryID, date: date, time: time, patientName: alertData.patientName || 'N/A', patientMRN: alertData.patientId || 'N/A', patientLocation: alertData.patientLocation || 'N/A', sampleBarcode: alertData.sampleBarcode || 'N/A', userName: userIdInput ? userIdInput.value : (alertData.userId || 'N/A'), // Use modal input if available notifiedPersonName: notifiedPersonNameInput ? notifiedPersonNameInput.value : '', notifiedPersonTelExt: notifiedPersonTelExtInput ? notifiedPersonTelExtInput.value : '', readBack: readBackCheckbox ? readBackCheckbox.checked : false, alerts: alertData.alertsToList.map(alert => ({ testName: alert.testName, result: alert.result, uom: alert.uom || '', flag: alert.flag, type: alert.type, // e.g., 'critical', 'noresult' comment: alert.comment || '' })), }; try { const response = await fetch(CONFIG_ALERTS.LOCAL_SERVER_SAVE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataToSend) }); if (response.ok) { logAlertDebug("Data successfully sent to local server."); return true; } else { const errorText = await response.text(); logAlertError(`Failed to send data to local server: ${response.status} ${response.statusText} - ${errorText}`); console.error(`Failed to save alert data to local server. Status: ${response.status}. Details: ${errorText}`); return false; } } catch (error) { logAlertError("Error sending data to local server:", error); // This usually means the server is not running or not accessible (CORS, network issue) console.error("An error occurred while trying to send data to the local server. Ensure the server is running and accessible."); return false; } } function closeAlertModalAction() { logAlertDebug("closeAlertModalAction triggered."); const overlay = document.getElementById('custom-alert-modal-overlay'); if (overlay) { const modalContent = overlay.querySelector('#custom-alert-modal-content'); if (modalContent && modalContent.dataset.alerts) { try { const alertsToMark = JSON.parse(modalContent.dataset.alerts); alertsToMark.forEach(alert => { if (alert.type && alert.rowId) { // Ensure type and rowId exist // The rowId is already prefixed like 'critical_ag-row-id-123' // We need to extract the type and the base identifier const parts = alert.rowId.split('_'); // e.g., ['critical', 'ag-row-id-123'] or ['noresult', 'idx', '0'] const type = parts[0]; const baseIdentifier = parts.slice(1).join('_'); // Reconstruct identifier if it had underscores setNotificationFlag(type, baseIdentifier); logAlertDebug(`Marked alert as notified: Type=${type}, Identifier=${baseIdentifier} (from full key ${alert.rowId})`); } }); } catch (e) { console.error("Error parsing alerts data from modal dataset:", e); } } // Smooth fade out overlay.style.opacity = '0'; if (modalContent) { modalContent.style.opacity = '0'; modalContent.style.transform = 'translateY(-30px) scale(0.95)'; } // Remove after transition overlay.addEventListener('transitionend', () => { if (overlay.parentNode) { overlay.parentNode.removeChild(overlay); isAlertModalOpen = false; // Reset flag document.body.style.overflow = ''; // Restore body scroll logAlertDebug("Alert modal overlay removed, isAlertModalOpen = false"); } }, { once: true }); // Fallback removal if transitionend doesn't fire (e.g., element removed abruptly) setTimeout(() => { if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); isAlertModalOpen = false; document.body.style.overflow = ''; logAlertDebug("Alert modal overlay removed by fallback, isAlertModalOpen = false"); } }, 400); // Slightly longer than transition } else { isAlertModalOpen = false; // Ensure flag is reset even if overlay wasn't found document.body.style.overflow = ''; logAlertDebug("closeAlertModalAction called but overlay not found, isAlertModalOpen = false"); } } function createCustomAlert(alertData, alertsDisplayedInModal) { logAlertDebug(`Attempting to create alert modal. isAlertModalOpen: ${isAlertModalOpen}, Existing overlay: ${document.getElementById('custom-alert-modal-overlay')}`); if (document.getElementById('custom-alert-modal-overlay') || isAlertModalOpen) { logAlertDebug("Alert modal already open or overlay element exists. Aborting creation."); return; } isAlertModalOpen = true; logDebugMain("Alert modal creation initiated. isAlertModalOpen set to true."); const overlay = document.createElement('div'); overlay.id = 'custom-alert-modal-overlay'; Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.65)', // Darker overlay zIndex: '10001', // Ensure it's on top display: 'flex', justifyContent: 'center', alignItems: 'center', opacity: '0', transition: 'opacity 0.3s ease-out', // Fade-in transition padding: '20px' // Padding for smaller screens so modal isn't edge-to-edge }); document.body.style.overflow = 'hidden'; // Prevent background scrolling const modalBox = document.createElement('div'); modalBox.id = 'custom-alert-modal-content'; Object.assign(modalBox.style, { backgroundColor: '#ffffff', borderRadius: '12px', // Softer corners boxShadow: '0 16px 32px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.08)', // Enhanced shadow width: 'calc(100% - 40px)', // Responsive width with padding maxWidth: '1100px', // Max width for large screens maxHeight: '90vh', // Max height to prevent overflow on short screens overflowY: 'auto', // Scroll for content overflow padding: '32px', // Generous padding position: 'relative', // For absolute positioning of close button opacity: '0', transform: 'translateY(-20px) scale(0.98)', // Entry animation transition: 'opacity 0.3s ease-out, transform 0.3s ease-out', fontFamily: "'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif", color: '#343a40', // Dark grey for text display: 'flex', flexDirection: 'column', gap: '16px' // Use flex for layout }); // Close Button (X) const closeButton = document.createElement('button'); closeButton.innerHTML = '×'; // HTML entity for 'X' Object.assign(closeButton.style, { position: 'absolute', top: '18px', right: '18px', fontSize: '30px', lineHeight: '1', cursor: 'pointer', color: '#adb5bd', // Light grey background: 'transparent', border: 'none', padding: '0', width: '30px', height: '30px', display: 'flex', alignItems: 'center', justifyContent: 'center' }); closeButton.onmouseover = () => closeButton.style.color = '#495057'; // Darken on hover closeButton.onmouseout = () => closeButton.style.color = '#adb5bd'; closeButton.onclick = closeAlertModalAction; modalBox.appendChild(closeButton); // Header Section (Icon, Title, DateTime) const headerDiv = document.createElement('div'); Object.assign(headerDiv.style, { display: 'flex', alignItems: 'center', gap: '12px', // Space between icon and title paddingBottom: '18px', borderBottom: '1px solid #dee2e6', marginBottom: '16px' }); const alertIcon = document.createElement('i'); // FontAwesome icon alertIcon.className = 'fas fa-exclamation-triangle'; // Default icon let iconColor = '#17a2b8'; // Info blue as default let alertTitleText = 'Lab Alert Detected'; if (alertData.overallSeverity === 'critical') { alertTitleText = 'Critical Lab Alert!'; iconColor = '#d9534f'; // Red for critical } else if (alertData.overallSeverity === 'noresult') { alertTitleText = 'No Result Detected'; iconColor = '#ffc107'; // Yellow for warning/no-result } else if (alertData.overallSeverity === 'greaterThan') { alertTitleText = 'Dilution Required'; iconColor = '#ffc107'; // Yellow for dilution } alertIcon.style.fontSize = '26px'; alertIcon.style.color = iconColor; headerDiv.appendChild(alertIcon); const title = document.createElement('h2'); title.textContent = alertTitleText; Object.assign(title.style, { fontSize: '26px', fontWeight: '600', color: iconColor, margin: '0' }); headerDiv.appendChild(title); const dateTime = document.createElement('p'); dateTime.id = 'currentDateTime'; const now = new Date(); dateTime.textContent = now.toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }); Object.assign(dateTime.style, { fontSize: '14px', color: '#6c757d', margin: '0', marginLeft: 'auto' }); // Align to right headerDiv.appendChild(dateTime); modalBox.appendChild(headerDiv); // Patient Information Section const allPatientInfoContainer = document.createElement('div'); Object.assign(allPatientInfoContainer.style, { display: 'flex', flexDirection: 'column', // Stack items vertically gap: '18px', // Space between info groups paddingBottom: '18px', borderBottom: '1px solid #dee2e6', marginBottom: '16px' }); const createInfoItem = (label, value, iconClass) => { if (!value) value = 'N/A'; // Handle undefined/empty values const itemDiv = document.createElement('div'); Object.assign(itemDiv.style, { backgroundColor: '#f8f9fa', borderRadius: '8px', padding: '16px', border: '1px solid #e9ecef', boxShadow: '0 3px 6px rgba(0,0,0,0.06)', display: 'flex', flexDirection: 'column', gap: '10px' }); const header = document.createElement('div'); Object.assign(header.style, { display: 'flex', alignItems: 'center', gap: '12px', color: '#495057' }); if (iconClass) { const iconElem = document.createElement('i'); iconElem.className = `fas ${iconClass}`; Object.assign(iconElem.style, { fontSize: '1.4em', color: '#007bff' }); // Blue icon header.appendChild(iconElem); } const labelElem = document.createElement('strong'); labelElem.textContent = `${label}:`; labelElem.style.fontSize = '15px'; labelElem.style.fontWeight = '600'; header.appendChild(labelElem); itemDiv.appendChild(header); const valueElem = document.createElement('span'); valueElem.textContent = value; Object.assign(valueElem.style, { fontSize: '16px', color: '#212529', wordBreak: 'break-word', paddingLeft: iconClass ? '32px' : '0' // Indent value if icon present }); itemDiv.appendChild(valueElem); return itemDiv; }; // Patient Name (full width) const pNameItem = createInfoItem('Patient Name', alertData.patientName, 'fa-user'); if (pNameItem) { allPatientInfoContainer.appendChild(pNameItem); } // Other Patient Info (Grid for MRN, Location, Barcode) const otherPatientInfoRow = document.createElement('div'); Object.assign(otherPatientInfoRow.style, { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', // Responsive grid gap: '18px', }); const pIdElem = createInfoItem('MRN', alertData.patientId, 'fa-id-card'); if (pIdElem) { otherPatientInfoRow.appendChild(pIdElem); } const pLocElem = createInfoItem('Location', alertData.patientLocation, 'fa-hospital-alt'); if (pLocElem) { otherPatientInfoRow.appendChild(pLocElem); } const pBarcodeElem = createInfoItem('Sample Barcode', alertData.sampleBarcode, 'fa-barcode'); if (pBarcodeElem) { otherPatientInfoRow.appendChild(pBarcodeElem); } if (otherPatientInfoRow.hasChildNodes()){ allPatientInfoContainer.appendChild(otherPatientInfoRow); } if (allPatientInfoContainer.hasChildNodes()){ modalBox.appendChild(allPatientInfoContainer); } // Alerts List Section if (alertData.alertsToList && alertData.alertsToList.length > 0) { const alertsListTitle = document.createElement('h3'); alertsListTitle.textContent = 'Alert Details:'; Object.assign(alertsListTitle.style, { fontSize: '19px', fontWeight: '600', color: '#343a40', margin: '0 0 12px 0', // Spacing }); modalBox.appendChild(alertsListTitle); const alertsContainer = document.createElement('div'); Object.assign(alertsContainer.style, { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', // Responsive grid for alert cards gap: '18px', padding: '5px 0' // Small padding for internal spacing }); alertData.alertsToList.forEach(alert => { const alertCard = document.createElement('div'); let borderColor = '#6c757d', bgColor = '#f8f9fa', textColor = '#343a40'; // Default styling // Customize card style based on alert type/flag if (alert.type === 'critical') { if (alert.flag === 'CL') { // Critical Low borderColor = '#007bff'; bgColor = '#e7f3ff'; // Blue theme } else if (alert.flag === 'CH') { // Critical High borderColor = '#dc3545'; bgColor = '#f8d7da'; // Red theme } else { // General critical borderColor = '#d9534f'; bgColor = '#fdf7f7'; } } else if (alert.type === 'greaterThan') { // Dilution borderColor = '#ffc107'; bgColor = '#fff9e6'; // Yellow theme } else if (alert.type === 'noresult') { // No Result borderColor = '#6c757d'; bgColor = '#f8f9fa'; // Grey theme } Object.assign(alertCard.style, { borderLeft: `6px solid ${borderColor}`, padding: '18px', borderRadius: '10px', backgroundColor: bgColor, color: textColor, boxShadow: '0 5px 10px rgba(0,0,0,0.08)', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: '130px' }); const testNameElem = document.createElement('p'); testNameElem.innerHTML = `<strong style="font-size: 17px; color: #212529;">${alert.testName}</strong>`; testNameElem.style.margin = '0 0 10px 0'; alertCard.appendChild(testNameElem); const resultElem = document.createElement('p'); const resultText = alert.result || 'N/A'; const uomText = alert.uom ? ` ${alert.uom}` : ''; resultElem.textContent = `Result: ${resultText}${uomText}`; resultElem.style.margin = '0 0 10px 0'; resultElem.style.fontSize = '15px'; alertCard.appendChild(resultElem); if (alert.flag) { const flagElem = document.createElement('p'); let flagDescription = `Flag: ${alert.flag}`; if (alert.flag === 'CL') flagDescription = 'Flag: CL (Critical Low)'; else if (alert.flag === 'CH') flagDescription = 'Flag: CH (Critical High)'; flagElem.textContent = flagDescription; Object.assign(flagElem.style, {margin: '0', fontSize: '15px', fontWeight: '600', color: borderColor }); alertCard.appendChild(flagElem); } if (alert.comment) { // Display comment if present const commentElem = document.createElement('p'); commentElem.textContent = alert.comment; Object.assign(commentElem.style, { fontSize: '13px', fontStyle: 'italic', color: '#555', marginTop: '8px', paddingTop: '8px', borderTop: '1px dashed #ccc' }); alertCard.appendChild(commentElem); } alertsContainer.appendChild(alertCard); }); modalBox.appendChild(alertsContainer); } else if (alertData.primaryMessage) { // Fallback for single message if no list const primaryMessageElem = document.createElement('p'); primaryMessageElem.textContent = alertData.primaryMessage; Object.assign(primaryMessageElem.style, { fontSize: '16px', fontWeight: '500', margin: '12px 0', color: '#555' }); modalBox.appendChild(primaryMessageElem); } // Notification Details Form (for Critical Alerts) if (alertData.overallSeverity === 'critical') { const notifiedDetailsDiv = document.createElement('div'); Object.assign(notifiedDetailsDiv.style, { marginTop: '24px', paddingTop: '20px', borderTop: '1px solid #dee2e6' }); const h3 = document.createElement('h3'); h3.textContent = 'Notification Details (Critical)'; Object.assign(h3.style, { fontSize: '17px', fontWeight: '600', marginBottom: '16px', color: '#343a40'}); notifiedDetailsDiv.appendChild(h3); const formGrid = document.createElement('div'); Object.assign(formGrid.style, { display: 'grid', gridTemplateColumns: '1fr', gap: '14px' }); // Single column for simplicity const createFormGroup = (label, inputId, placeholder, type = 'text') => { const formGroup = document.createElement('div'); const formLabel = document.createElement('label'); formLabel.textContent = label; formLabel.htmlFor = inputId; Object.assign(formLabel.style, { display: 'block', fontSize: '14px', fontWeight: '500', marginBottom: '6px', color: '#495057' }); formGroup.appendChild(formLabel); const formInput = document.createElement('input'); formInput.type = type; formInput.id = inputId; formInput.placeholder = placeholder; Object.assign(formInput.style, { width: '100%', padding: '12px', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '14px', boxSizing: 'border-box', transition: 'border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out' }); formInput.onfocus = () => { formInput.style.borderColor = '#80bdff'; formInput.style.boxShadow = '0 0 0 0.2rem rgba(0,123,255,.25)';}; formInput.onblur = () => { formInput.style.borderColor = '#ced4da'; formInput.style.boxShadow = 'none';}; formGroup.appendChild(formInput); return formGroup; }; const userIdGroup = createFormGroup('User ID', 'userIdInput', 'Enter your User ID'); formGrid.appendChild(userIdGroup); const userIdInputElem = userIdGroup.querySelector('#userIdInput'); // Get the input element if (userIdInputElem && alertData.userId && alertData.userId !== 'N/A') { // Pre-fill if available userIdInputElem.value = alertData.userId; } formGrid.appendChild(createFormGroup('Notified Person Name', 'notifiedPersonNameInput', 'Name of person notified')); formGrid.appendChild(createFormGroup('Extension / Contact', 'notifiedPersonTelExtInput', 'Phone or extension')); const readBackGroup = document.createElement('div'); Object.assign(readBackGroup.style, { display: 'flex', alignItems: 'center', marginTop: '10px'}); const readBackCheckbox = document.createElement('input'); readBackCheckbox.type = 'checkbox'; readBackCheckbox.id = 'readBackCheckbox'; Object.assign(readBackCheckbox.style, { marginRight: '10px', height: '18px', width: '18px', cursor: 'pointer', accentColor: '#007bff'}); const readBackLabel = document.createElement('label'); readBackLabel.textContent = 'Read-Back Confirmed'; readBackLabel.htmlFor = 'readBackCheckbox'; Object.assign(readBackLabel.style, { fontSize: '14px', fontWeight: '500', color: '#495057', cursor: 'pointer'}); readBackGroup.appendChild(readBackCheckbox); readBackGroup.appendChild(readBackLabel); formGrid.appendChild(readBackGroup); notifiedDetailsDiv.appendChild(formGrid); modalBox.appendChild(notifiedDetailsDiv); } // Button Container const buttonContainer = document.createElement('div'); Object.assign(buttonContainer.style, { display: 'flex', justifyContent: 'flex-end', // Align buttons to the right paddingTop: '24px', marginTop: 'auto', // Push to bottom if content is short gap: '12px', borderTop: '1px solid #dee2e6' }); const createModalButton = (text, styleType = 'primary') => { const button = document.createElement('button'); button.textContent = text; Object.assign(button.style, { padding: '12px 24px', borderRadius: '8px', fontWeight: '600', fontSize: '15px', border: 'none', cursor: 'pointer', transition: 'background-color 0.2s ease, box-shadow 0.2s ease, transform 0.1s ease' }); if (styleType === 'primary') { button.style.backgroundColor = '#007bff'; button.style.color = 'white'; button.onmouseover = () => button.style.backgroundColor = '#0069d9'; button.onmouseout = () => button.style.backgroundColor = '#007bff'; } else if (styleType === 'success') { // For Acknowledge & Save button.style.backgroundColor = '#28a745'; button.style.color = 'white'; button.onmouseover = () => button.style.backgroundColor = '#218838'; button.onmouseout = () => button.style.backgroundColor = '#28a745'; } else { // Default/secondary button.style.backgroundColor = '#6c757d'; button.style.color = 'white'; button.onmouseover = () => button.style.backgroundColor = '#5a6268'; button.onmouseout = () => button.style.backgroundColor = '#6c757d'; } button.onmousedown = () => button.style.transform = 'scale(0.98)'; // Click effect button.onmouseup = () => button.style.transform = 'scale(1)'; return button; }; if (alertData.overallSeverity === 'critical') { const acknowledgeButton = createModalButton('Acknowledge & Save', 'success'); acknowledgeButton.onclick = async () => { logAlertDebug("Acknowledge & Save button clicked."); const success = await sendAlertDataToServer(alertData); if (success) { logAlertDebug("Data sent successfully. Closing modal."); closeAlertModalAction(); } else { logAlertDebug("Data send failed. Modal remains open."); const errorMsgElem = document.getElementById('sendErrorMsg'); if (errorMsgElem) errorMsgElem.textContent = 'Failed to save. Check server or try again.'; } }; buttonContainer.appendChild(acknowledgeButton); // Error message placeholder for send failure const sendErrorMsg = document.createElement('p'); sendErrorMsg.id = 'sendErrorMsg'; Object.assign(sendErrorMsg.style, {color: 'red', fontSize: '13px', margin: '0 auto 0 0', flexGrow: 1, alignSelf: 'center'}); buttonContainer.insertBefore(sendErrorMsg, acknowledgeButton); // Place error message before buttons } else { // For non-critical, just an OK button const okButton = createModalButton('OK', 'primary'); okButton.onclick = closeAlertModalAction; buttonContainer.appendChild(okButton); } modalBox.appendChild(buttonContainer); // Store a list of alerts that were shown in this modal instance for later marking as notified modalBox.dataset.alerts = JSON.stringify(alertsDisplayedInModal.map(a => ({ type: a.type, rowId: a.rowId }))); overlay.appendChild(modalBox); document.body.appendChild(overlay); // Trigger fade-in and animation setTimeout(() => { overlay.style.opacity = '1'; modalBox.style.opacity = '1'; modalBox.style.transform = 'translateY(0) scale(1)'; logAlertDebug("Alert modal fade-in and animation triggered."); }, 10); // Small delay to ensure styles are applied before transition starts // Close modal on Escape key or click outside overlay.addEventListener('click', (event) => { if (event.target === overlay) { // Clicked on overlay itself closeAlertModalAction(); } }); const escapeKeyListener = (event) => { if (event.key === 'Escape' && document.body.contains(overlay)) { // Check if modal is still in DOM closeAlertModalAction(); document.removeEventListener('keydown', escapeKeyListener); // Clean up listener } }; document.addEventListener('keydown', escapeKeyListener); } function scrollToRowNearest(row) { if (!row) return false; let rowToScroll = row; // If the row is in the center container, try to find its corresponding pinned row for scrolling // This helps if the test description (usually pinned) is what needs to be visible if (row.closest('.ag-center-cols-container')) { const rowIndex = row.getAttribute('row-index'); // AG-Grid often uses row-index if (rowIndex) { const pinnedRow = document.querySelector(`.ag-pinned-left-cols-container .ag-row[row-index="${rowIndex}"]`); if (pinnedRow) rowToScroll = pinnedRow; } } document.documentElement.style.scrollBehavior = 'smooth'; rowToScroll.scrollIntoView({ behavior: 'auto', block: 'center' }); // 'auto' for behavior if smooth is too slow setTimeout(() => document.documentElement.style.scrollBehavior = 'auto', 500); // Reset scroll behavior return true; } async function getPatientDataWithRetry(selectors, maxWaitTime = 2000, checkInterval = 100) { let elapsedTime = 0; let patientData = { patientName: 'N/A', patientId: 'N/A', patientLocation: 'N/A', sampleBarcode: 'N/A' }; // Helper to check if essential elements are present function elementsPresent() { return document.querySelector(selectors.name) && document.querySelector(selectors.mrn) && document.querySelector(selectors.location); // Barcode is optional for this check } // Wait for essential elements to be available while (elapsedTime < maxWaitTime) { if (elementsPresent()) { logAlertDebug("Essential patient data elements detected."); break; } await new Promise(resolve => setTimeout(resolve, checkInterval)); elapsedTime += checkInterval; if (elapsedTime % 500 === 0) { // Log progress occasionally logAlertDebug(`Waiting for patient data elements... ${elapsedTime}ms`); } } if (!elementsPresent() && elapsedTime >= maxWaitTime) { logAlertDebug(`Essential patient data elements not found after ${maxWaitTime}ms. Using N/A for some fields.`); } // Retrieve data, providing 'N/A' if elements are not found or empty const nameEl = document.querySelector(selectors.name); patientData.patientName = nameEl ? nameEl.textContent.trim().replace(/\s+/g, ' ') : 'N/A'; // Normalize whitespace const mrnEl = document.querySelector(selectors.mrn); patientData.patientId = mrnEl ? mrnEl.textContent.trim() : 'N/A'; const locElement = document.querySelector(selectors.location); // Prefer title attribute for location if available, otherwise textContent patientData.patientLocation = locElement ? (locElement.title?.trim() || locElement.textContent?.trim()) : 'N/A'; if (patientData.patientLocation === '') patientData.patientLocation = 'N/A'; // Ensure not empty string const barcodeEl = document.querySelector(selectors.barcode); patientData.sampleBarcode = barcodeEl ? barcodeEl.textContent.trim() : 'N/A'; if (patientData.sampleBarcode === '') patientData.sampleBarcode = 'N/A'; return patientData; } async function checkForIssues() { if (!isScanningActive || isAlertModalOpen) return false; // Don't scan if paused or modal is already open const patientDataSelectors = { name: CONFIG_ALERTS.PATIENT_NAME_SELECTOR, mrn: CONFIG_ALERTS.PATIENT_MRN_SELECTOR, location: CONFIG_ALERTS.PATIENT_LOCATION_SELECTOR, barcode: CONFIG_ALERTS.SAMPLE_BARCODE_SELECTOR }; const patientDataSource = await getPatientDataWithRetry(patientDataSelectors); const patientName = patientDataSource.patientName; const patientId = patientDataSource.patientId; const patientLocation = patientDataSource.patientLocation; const sampleBarcode = patientDataSource.sampleBarcode; const userIdElement = document.querySelector(CONFIG_ALERTS.USER_NAME_SELECTOR); const currentUserId = userIdElement ? userIdElement.textContent.trim() : 'N/A'; const centerRows = document.querySelectorAll('.ag-center-cols-container .ag-row'); const pinnedRows = document.querySelectorAll('.ag-pinned-left-cols-container .ag-row'); const potentialAlerts = []; centerRows.forEach((centerRow, index) => { const pinnedRow = pinnedRows[index]; // Assumes matching indices for pinned and center rows if (!centerRow || !pinnedRow || centerRow.offsetParent === null) return; // Skip if not visible or no matching pinned row const testDescElement = pinnedRow.querySelector('div[col-id="TestDesc"]'); const testDesc = testDescElement?.textContent?.trim() || 'Unknown Test'; const resultDiv = centerRow.querySelector(CONFIG_ALERTS.RESULT_CELL_SELECTOR); const flagSpan = centerRow.querySelector(CONFIG_ALERTS.CRITICAL_FLAG_SELECTOR); const uomCell = centerRow.querySelector(CONFIG_ALERTS.UOM_CELL_SELECTOR); // Create a unique identifier for the row for sessionStorage tracking const baseRowIdentifier = centerRow.getAttribute('row-id') || centerRow.getAttribute('row-index') || `idx_${index}`; const resultText = resultDiv?.textContent?.trim() || ''; const uomText = uomCell?.textContent?.trim() || ''; const normalizedResultText = resultText.toLowerCase().replace(/[- ]/g, ''); // Normalize for "no-result" checks const flagText = flagSpan?.textContent?.trim()?.toUpperCase() || ''; // CL, CH // Determine alert type const isNoResult = ["noresult", "noxresult", "x-noresult"].includes(normalizedResultText); const isDilution = resultText.includes(">"); // Simple check for dilution needed const isCritical = (flagSpan && (flagText === "CL" || flagText === "CH")); let alertType = null; let alertMessage = ""; let alertComment = null; // For additional context like "Sample needs dilution" if (isCritical) { alertType = "critical"; alertMessage = `CRITICAL ${flagText === "CL" ? "LOW" : "HIGH"} RESULT!`; } else if (isNoResult) { alertType = "noresult"; alertMessage = CONFIG_ALERTS.NO_RESULT_MESSAGE; } else if (isDilution) { alertType = "greaterThan"; alertMessage = CONFIG_ALERTS.DILUTION_MESSAGE; alertComment = "Sample needs dilution"; // Add specific comment for dilution } if (alertType) { if (!hasAlreadyNotified(alertType, baseRowIdentifier)) { potentialAlerts.push({ rowElement: centerRow, type: alertType, rowId: `${alertType}_${baseRowIdentifier}`, // Composite key for session storage baseIdentifier: baseRowIdentifier, // Store base for easier reference message: alertMessage, comment: alertComment, testName: testDesc, result: resultText, uom: uomText, flag: flagText }); } applyFlashingEffect(centerRow); // Flash the center row applyFlashingEffect(pinnedRow); // Flash the corresponding pinned row } else { stopFlashingEffect(centerRow); stopFlashingEffect(pinnedRow); } }); if (potentialAlerts.length > 0 && !isAlertModalOpen && !document.getElementById('custom-alert-modal-overlay')) { // Prioritize showing critical alerts first if multiple types are present const firstSignificantAlert = potentialAlerts.find(a => a.type === 'critical') || potentialAlerts.find(a => a.type === 'noresult') || potentialAlerts.find(a => a.type === 'greaterThan') || potentialAlerts[0]; // Fallback to the first alert scrollToRowNearest(firstSignificantAlert.rowElement); // Scroll to the most significant alert const modalData = { alertsToList: potentialAlerts.map(a => ({ // Pass all details for display testName: a.testName, result: a.result, uom: a.uom, flag: a.flag, type: a.type, rowId: a.rowId, // Pass the full rowId used for notification tracking comment: a.comment })), overallSeverity: firstSignificantAlert.type, // Used for modal title and icon primaryMessage: firstSignificantAlert.message, // Main message for the modal patientName: patientName, patientId: patientId, patientLocation: patientLocation, sampleBarcode: sampleBarcode, userId: currentUserId }; createCustomAlert(modalData, potentialAlerts); // Pass all potential alerts for marking // Pause scanner after showing alerts for this patient to avoid repeated modals for same data logAlertDebug("Alerts shown for patient. Pausing scanner."); if (window.stopAlertsScanner) window.stopAlertsScanner(); // Call the global pause function return true; // Alerts were found and processed } else if (potentialAlerts.length === 0) { // If no alerts, ensure any residual flashing is stopped document.querySelectorAll('.ag-center-cols-container .ag-row[data-flashing="true"]').forEach(stopFlashingEffect); document.querySelectorAll('.ag-pinned-left-cols-container .ag-row[data-flashing="true"]').forEach(stopFlashingEffect); } return false; // No new alerts to show } function startAlertsScannerInternal() { logAlertDebug("startAlertsScannerInternal called."); if (!isScanningActive && issueScanIntervalId === null) { // Only start if not already active and no interval ID logAlertDebug("Alert Scanner starting/resuming..."); isScanningActive = true; if (issueScanIntervalId) clearInterval(issueScanIntervalId); // Clear any old interval just in case checkForIssues(); // Initial check issueScanIntervalId = setInterval(checkForIssues, CONFIG_ALERTS.SCAN_INTERVAL); logAlertDebug(`Alert Scanner interval set: ${issueScanIntervalId}`); } else { logAlertDebug(`Alert Scanner already active or interval set. Active: ${isScanningActive}, IntervalID: ${issueScanIntervalId}`); } } function stopAlertsScannerInternal() { // This acts more like a "pause" logAlertDebug("stopAlertsScannerInternal called (pause)."); if (isScanningActive) { // Only act if it was active logAlertDebug("Alert Scanner pausing..."); clearInterval(issueScanIntervalId); issueScanIntervalId = null; // Clear interval ID, but keep isScanningActive = true // This indicates it's paused but should resume on new patient/page // Flashing rows will stop naturally when modal is closed or page changes. logAlertDebug("Scanner interval cleared for pause. isScanningActive remains true (paused state)."); } else { logAlertDebug("Alert Scanner not active, no action taken by stop/pause."); } } // Expose scanner controls globally for external management (e.g., on page change) window.startAlertsScanner = startAlertsScannerInternal; window.stopAlertsScanner = stopAlertsScannerInternal; // This is now a "pause" // --- Page Navigation Observer (for starting/stopping scanner and features) --- let previousObservedUrl = window.location.href; let currentPatientProcessed = false; // Flag to manage scanner pause/resume per patient view const pageUrlObserver = new MutationObserver(() => { const newObservedUrl = window.location.href; if (newObservedUrl !== previousObservedUrl) { logDebugMain(`URL changed from: ${previousObservedUrl} to: ${newObservedUrl}.`); currentPatientProcessed = false; // Reset flag, indicating a new patient/context const wasOnEditPage = previousObservedUrl.startsWith(CONFIG_MAIN.URLS.EDIT_PAGE_PREFIX); const nowOnEditPage = newObservedUrl.startsWith(CONFIG_MAIN.URLS.EDIT_PAGE_PREFIX); if (wasOnEditPage && !nowOnEditPage) { // Navigated AWAY from an edit page logDebugMain(`Left an edit page (${previousObservedUrl}). Fully stopping scanner and cleaning UI.`); isScanningActive = false; // Truly stop the scanner if (window.stopAlertsScanner) window.stopAlertsScanner(); // Call the pause, but isScanningActive=false prevents restart // Clear session storage flags for the *specific page* being left try { const oldUrlObject = new URL(previousObservedUrl, window.location.origin); // Ensure base URL for relative paths const oldPageKeyPrefix = `labAlertNotified_${oldUrlObject.pathname}${oldUrlObject.hash}_`; logAlertDebug(`Attempting to clear session flags with prefix: "${oldPageKeyPrefix}"`); Object.keys(sessionStorage) .filter(key => key.startsWith(oldPageKeyPrefix)) .forEach(key => { sessionStorage.removeItem(key); logAlertDebug(`Cleared session flag: ${key}`); }); logAlertDebug(`Session flags for prefix "${oldPageKeyPrefix}" cleared.`); } catch (e) { logAlertError(`Error processing old URL (${previousObservedUrl}) for flag clearing: ${e}`); } // Remove buttons if they exist const customButtons = document.getElementById('custom-script-buttons'); if (customButtons) customButtons.remove(); } if (nowOnEditPage) { logDebugMain(`Now on a new edit page (${newObservedUrl}). Setting up features and (re)starting scanner.`); hasScrolledToOrderedRow = false; // Reset for new page isAlertModalOpen = false; // Reset modal flag // Close any existing alert modal forcefully if navigation happened while it was open const existingAlertOverlay = document.getElementById('custom-alert-modal-overlay'); if (existingAlertOverlay) existingAlertOverlay.remove(); setTimeout(() => { // Start scanner only if not already "processed" (i.e., paused after showing alerts for this patient) if (!currentPatientProcessed && window.startAlertsScanner) { window.startAlertsScanner(); } addButtons(); // Add F7/F8 buttons addFontAwesome(); // Ensure icons are available monitorOrderedStatus(); // Initial check for 'Ordered' status }, 700); // Delay to allow page elements to load } else { // Not on an edit page logDebugMain(`Not on an edit page (${newObservedUrl}). Ensuring UI cleanup.`); const customButtons = document.getElementById('custom-script-buttons'); if (customButtons) customButtons.remove(); // If truly leaving edit pages, ensure scanner is fully stopped if (isScanningActive && window.stopAlertsScanner) { isScanningActive = false; // Mark as not active window.stopAlertsScanner(); // Call the pause/clear interval } } previousObservedUrl = newObservedUrl; } else { // URL has not changed, but mutations occurred const stillOnEditPage = newObservedUrl.startsWith(CONFIG_MAIN.URLS.EDIT_PAGE_PREFIX); if (stillOnEditPage) { // Check if buttons are missing (e.g., due to SPA re-render without URL change) if (!document.getElementById('custom-script-buttons')) { logDebugMain("Buttons missing on edit page (no URL change), re-adding."); addButtons(); } addFontAwesome(); // Ensure icons are still there // If scanner was paused (isScanningActive=true, but intervalId=null) and it's still the same patient page, // it should remain paused. // If it's a page refresh for a new patient (somehow URL didn't change initially), // startAlertsScanner will handle it if currentPatientProcessed is false. if (!isScanningActive && !currentPatientProcessed && window.startAlertsScanner) { // This case might be rare, but handles if scanner was fully stopped and needs restart on same URL logDebugMain("Scanner not active on current edit page (no URL change), attempting restart."); window.startAlertsScanner(); } } } }); pageUrlObserver.observe(document.body, { childList: true, subtree: true }); // --- Initial Setup on Load --- if (isCorrectPage()) { logDebugMain("Initial page load on correct page. Setting up features."); currentPatientProcessed = false; // Reset for initial load setTimeout(() => { if (window.startAlertsScanner) window.startAlertsScanner(); addFontAwesome(); addButtons(); monitorOrderedStatus(); }, 700); // Delay for page elements } else { logDebugMain("Initial page load not on correct page. No features started."); } // --- Periodic Checks --- setInterval(checkUrlAndTriggerClickForUndefined, CONFIG_MAIN.CHECK_INTERVALS.UNDEFINED_URL); setInterval(checkForDisabledButtons, CONFIG_MAIN.CHECK_INTERVALS.DISABLED_BTN_CHECK); // Less frequent check for disabled buttons })(); // User's SAMPLES COUNT Logic (IIFE) - Integrated and Styles Updated // This is the logic from the original "1.js" script, integrated here. (function () { 'use strict'; const SCRIPT_PREFIX_SAMPLES = "[SAMPLES COUNT 통합]"; // Changed prefix for clarity const logSampleCountDebug = msg => console.debug(`${SCRIPT_PREFIX_SAMPLES} ${msg}`); // const logSampleCountError = msg => console.error(`${SCRIPT_PREFIX_SAMPLES} ${msg}`); // Already defined in main script function createCounterElementSamples(id) { // Renamed to avoid conflict if any global function existed const wrapper = document.createElement('span'); wrapper.id = id; Object.assign(wrapper.style, { display: 'inline-flex', alignItems: 'center', padding: '6px 12px', backgroundColor: '#e9ecef', // Light grey background borderRadius: '20px', // Rounded corners boxShadow: '0 2px 5px rgba(0,0,0,0.1)', // Subtle shadow marginRight: 'auto', // Pushes to the left if in a flex container marginLeft: '10px', // Margin from other elements fontSize: '14px', fontWeight: '500', color: '#495057', // Dark grey text verticalAlign: 'middle' }); const label = document.createElement('span'); label.textContent = 'SAMPLES COUNT: '; label.style.marginRight = '8px'; const badge = document.createElement('span'); badge.className = 'sample-count-badge'; // For potential specific styling via CSS badge.textContent = '0'; // Initialize with 0 Object.assign(badge.style, { backgroundColor: '#6c757d', // Default grey for 0 color: '#ffffff', // White text padding: '4px 10px', borderRadius: '12px', // Rounded badge fontSize: '14px', fontWeight: 'bold', minWidth: '26px', // Ensure badge has some width even for single digit textAlign: 'center', lineHeight: '1' // Ensure text is centered vertically }); wrapper.appendChild(label); wrapper.appendChild(badge); return wrapper; } function updateSpecificCounterSamples(modalElementForInputs, counterElement, inputSelector) { const badge = counterElement.querySelector('.sample-count-badge'); // Ensure all elements are valid and connected to the DOM if (!badge || !modalElementForInputs || !document.body.contains(modalElementForInputs) || !counterElement.isConnected) { return; } const inputs = modalElementForInputs.querySelectorAll(inputSelector); const count = inputs.length; badge.textContent = count; badge.style.backgroundColor = count > 0 ? '#28a745' : '#6c757d'; // Green if > 0, else grey } // This function sets up a counter for a specific modal type function setupModalCounterSamples(modalConfig) { const { modalKeyElement, targetFooter, counterId, inputSelector, activeIntervalsMap, buttonIdForLog } = modalConfig; logSampleCountDebug(`Setup for ${buttonIdForLog}: ModalKeyElement found: ${!!modalKeyElement}, TargetFooter found: ${!!targetFooter}`); if (targetFooter.querySelector('#' + counterId)) { logSampleCountDebug(`Setup for ${buttonIdForLog}: Counter (ID: ${counterId}) already exists in footer. Skipping.`); return false; // Counter already exists } logSampleCountDebug(`Setup for ${buttonIdForLog}: Creating counter (ID: ${counterId}).`); const counter = createCounterElementSamples(counterId); targetFooter.insertBefore(counter, targetFooter.firstChild); // Insert at the beginning of the footer logSampleCountDebug(`Setup for ${buttonIdForLog}: Counter (ID: ${counterId}) CREATED and INSERTED.`); // Determine the element within the modal to search for inputs const modalElementForInputs = modalKeyElement.querySelector('.modal-body') || modalKeyElement.querySelector('.modal-content') || modalKeyElement; const interval = setInterval(() => { const modalStillTracked = document.body.contains(modalKeyElement); const counterStillPresent = counter.isConnected; // Check modal visibility (common classes and style checks) const modalVisible = modalStillTracked && ( modalKeyElement.classList.contains('show') || modalKeyElement.classList.contains('modal-open') || (modalKeyElement.style.display && modalKeyElement.style.display !== 'none') || (!modalKeyElement.style.display && modalKeyElement.offsetParent !== null) // Check if rendered ); if (!modalStillTracked || !counterStillPresent || !modalVisible) { logSampleCountDebug(`Interval for ${buttonIdForLog} (Counter ID: ${counterId}): Cleaning up. ModalTracked: ${modalStillTracked}, CounterPresent: ${counterStillPresent}, ModalVisible: ${modalVisible}`); clearInterval(interval); activeIntervalsMap.delete(modalKeyElement); // Remove from active tracking if (counter.isConnected) { // Remove counter from DOM if it's still there counter.remove(); } return; } updateSpecificCounterSamples(modalElementForInputs, counter, inputSelector); }, 500); // Update interval activeIntervalsMap.set(modalKeyElement, { interval, counterId }); // Track active interval return true; } function observeModalsSamples() { // Renamed to avoid conflict const activeModalIntervals = new Map(); // Tracks modals and their counter intervals logSampleCountDebug("Sample Count Modal observer callback function defined."); const getModalKeyDetails = (el) => { // Helper for logging if (!el) return "null"; const tagName = el.tagName || "UNKNOWN_TAG"; let className = ""; if (el.className && typeof el.className === 'string') { className = el.className.trim().replace(/\s+/g, '.'); } else if (el.className && typeof el.className.baseVal === 'string') { // For SVG elements className = el.className.baseVal.trim().replace(/\s+/g, '.'); } return `${tagName}${className ? '.' + className : ''}`; }; const observer = new MutationObserver((mutationsList) => { // --- Target Modal Type 1 (Sample Collection / CPL Receptionist Modal) --- // Usually identified by a button like 'btnclose-smplcollection' or specific app component const buttonSmplCollection = document.querySelector('button#btnclose-smplcollection'); // Or other unique element in this modal if (buttonSmplCollection) { const footerSmplCollection = buttonSmplCollection.closest('.modal-footer'); if (footerSmplCollection) { // The modalKeyElement should be the main modal container that stays in the DOM while the modal is open // and is removed or hidden when closed. '.modal' or '.modal-dialog' are common. const modalKeyElement = footerSmplCollection.closest('.modal.show, .modal-dialog, modal-container.show'); // More robust selector if (modalKeyElement && !activeModalIntervals.has(modalKeyElement)) { logSampleCountDebug(`Found potential modal for 'btnclose-smplcollection' (modal key: ${getModalKeyDetails(modalKeyElement)}). Attempting setup.`); setupModalCounterSamples({ buttonIdForLog: 'btnclose-smplcollection', counterId: 'inline-counter-smplcollection', inputSelector: 'tbody[formarrayname="TubeTypeList"] input[formcontrolname="PatientID"]', // Specific to this modal modalKeyElement: modalKeyElement, targetFooter: footerSmplCollection, activeIntervalsMap: activeModalIntervals }); } } } // --- Target Modal Type 2 (Sample Receive / WB Modal) --- // Usually identified by a button like 'closebtn-smplrecieve' or specific app component const buttonSmplRecieve = document.querySelector('button#closebtn-smplrecieve'); // Or other unique element if (buttonSmplRecieve) { const footerSmplRecieve = buttonSmplRecieve.closest('.modal-footer'); if (footerSmplRecieve) { const modalKeyElement = footerSmplRecieve.closest('.modal.show, .modal-dialog, modal-container.show'); if (modalKeyElement && !activeModalIntervals.has(modalKeyElement)) { logSampleCountDebug(`Found potential modal for 'closebtn-smplrecieve' (modal key: ${getModalKeyDetails(modalKeyElement)}). Attempting setup.`); setupModalCounterSamples({ buttonIdForLog: 'closebtn-smplrecieve', counterId: 'inline-counter-smplrecieve', inputSelector: 'td input[formcontrolname="PatientID"]', // Specific to this modal's table structure modalKeyElement: modalKeyElement, targetFooter: footerSmplRecieve, activeIntervalsMap: activeModalIntervals }); } } } // Fallback cleanup for modals that might have been missed by interval cleanup activeModalIntervals.forEach((data, modalElemKey) => { const modalStillPresent = document.body.contains(modalElemKey); const counterElem = document.getElementById(data.counterId); const modalStillVisible = modalStillPresent && ( modalElemKey.classList.contains('show') || modalElemKey.classList.contains('modal-open') || (modalElemKey.style.display && modalElemKey.style.display !== 'none') || (!modalElemKey.style.display && modalElemKey.offsetParent !== null) ); if (!modalStillPresent || !modalStillVisible) { logSampleCountDebug(`Fallback Cleanup: Modal for counter ID ${data.counterId} (key: ${getModalKeyDetails(modalElemKey)}) no longer present/visible. Cleaning up.`); clearInterval(data.interval); if (counterElem && counterElem.isConnected) { counterElem.remove(); } activeModalIntervals.delete(modalElemKey); } else if (counterElem && !counterElem.isConnected) { // Counter removed but modal still there (e.g. by other script) logSampleCountDebug(`Fallback Cleanup: Counter ID ${data.counterId} (key: ${getModalKeyDetails(modalElemKey)}) is disconnected but modal still present. Cleaning interval.`); clearInterval(data.interval); activeModalIntervals.delete(modalElemKey); } }); }); observer.observe(document.body, { childList: true, subtree: true }); logSampleCountDebug("Sample Count Modal observer INSTANCE started watching document.body."); } window.addEventListener('load', () => { logSampleCountDebug("Window 'load' event fired for SAMPLES COUNT. Initializing modal observer."); try { observeModalsSamples(); } catch (e) { console.error(`${SCRIPT_PREFIX_SAMPLES} Error during observeModalsSamples initialization: ${e.message} \n${e.stack}`); } }); logSampleCountDebug("UserScript SAMPLES COUNT logic loaded and IIFE executed."); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址