// ==UserScript==
// @name KAAUH Lab Suite - Verification, Alerts & Enhancements
// @namespace Violentmonkey Scripts
// @version 7.0.20
// @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,
// @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();
})();
// 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',
},
CHECK_INTERVALS: {
UNDEFINED_URL: 200,
ORDERED_SCAN: 500,
DISABLED_BTN_CHECK: 1000,
},
EXCLUDE_WORDS: [
'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;
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'
});
const title = document.createElement('h3');
title.textContent = 'Button Disabled';
title.style.color = '#d9534f';
title.style.marginTop = '0';
modalBox.appendChild(title);
const messageElem = document.createElement('p');
messageElem.textContent = message;
messageElem.style.fontSize = '16px';
messageElem.style.marginBottom = '20px';
modalBox.appendChild(messageElem);
const okButton = document.createElement('button');
okButton.textContent = 'OK';
Object.assign(okButton.style, {
padding: '8px 20px', borderRadius: '5px', backgroundColor: '#5cb85c',
color: '#fff', border: 'none', cursor: 'pointer', fontSize: '16px'
});
okButton.onclick = () => document.body.removeChild(modalOverlay);
modalBox.appendChild(okButton);
modalOverlay.appendChild(modalBox);
document.body.appendChild(modalOverlay);
okButton.focus();
}
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;
if (head) head.appendChild(link);
}
}
function createVerifyButton(label, className, onClick, id) {
const button = document.createElement('button');
button.type = 'button';
button.className = className;
button.classList.add(id);
button.innerText = label;
const styles = {
'font-family': 'Arial, sans-serif',
'font-size': '14px',
'font-weight': 'normal',
'color': '#ffffff',
'background-color': className.includes('success') ? '#28a745' : '#2594d9',
'padding': '8px 16px',
'border': 'none',
'border-radius': '5px',
'text-shadow': 'none',
'cursor': 'pointer',
'margin-right': '5px',
'line-height': '1.5',
'vertical-align': 'middle',
};
for (const [prop, value] of Object.entries(styles)) {
button.style.setProperty(prop, value, 'important');
}
button.onclick = onClick;
return button;
}
function createToggleIcon(id, isActive, onClick) {
const icon = document.createElement('span');
icon.id = id;
icon.innerHTML = `<i class="fas fa-arrow-circle-left" 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';
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);
const icon = document.querySelector(`#${type}Icon i`);
if (icon) icon.style.setProperty('color', toggle ? '#008000' : '#d1cfcf', 'important');
if (type === 'verify1') verify1Toggle = toggle;
else verify2Toggle = toggle;
}
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);
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);
}, 'verify1-btn');
const verify2Button = createVerifyButton('VERIFY2 (F8)', 'btn btn-primary btn-sm', () => {
verify2Clicked = true;
verify1Clicked = false;
checkAllVisibleBoxesWithoutDuplicates();
setTimeout(clickCompleteMedicalVerificationButton, 500);
}, 'verify2-btn');
const verify1Icon = createToggleIcon('verify1Icon', verify1Toggle, () => handleVerifyToggle('verify1'));
const verify2Icon = createToggleIcon('verify2Icon', verify2Toggle, () => handleVerifyToggle('verify2'));
const credit = document.createElement('span');
credit.textContent = "Modded by: Hamad AlShegifi";
credit.style.cssText =
`font-size: 11px !important;
font-weight: bold !important;
color: #e60000 !important;
margin-left: 15px !important;
border: 1px solid #e60000 !important;
border-radius: 5px !important;
padding: 3px 6px !important;
background-color: #ffffff !important;
vertical-align: middle !important;
`;
buttonDiv.append(verify1Button, verify1Icon, verify2Button, verify2Icon, credit);
nextButton.parentNode.insertBefore(buttonDiv, nextButton.nextSibling);
}
function checkAllVisibleBoxesWithoutDuplicates() {
const selectedTests = new Set();
const boxes = document.querySelectorAll(CONFIG_MAIN.SELECTORS.UNCHECKED_BOX);
boxes.forEach(box => {
const row = box.closest(CONFIG_MAIN.SELECTORS.CHECKBOX_ROW);
if (row && row.offsetParent !== null) {
const testDescElement = row.querySelector(CONFIG_MAIN.SELECTORS.TEST_DESC_CELL);
const descText = testDescElement?.textContent.trim().toLowerCase();
if (descText) {
const isExcluded = CONFIG_MAIN.EXCLUDE_WORDS.some(word => descText.includes(word));
if (!selectedTests.has(descText) && !isExcluded) {
selectedTests.add(descText);
box.click();
}
}
}
});
}
const clickButton = (selector, callback) => {
const btn = document.querySelector(selector);
if (btn && !btn.disabled) {
btn.click();
if (callback) setTimeout(callback, 500);
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);
if (verify1Btn && verify1Btn.disabled) {
showDisabledButtonAlert("Verification is disabled. Make sure tests not already verified.");
}
if (verify2Btn && verify2Btn.disabled) {
showDisabledButtonAlert("Verification is disabled. Make sure tests not already verified.");
}
}
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) closeBtn.click();
}
}
function monitorOrderedStatus() {
if (!isCorrectPage()) return;
const rows = document.querySelectorAll('div[role="row"]');
let hasOrdered = false;
let firstOrderedRow = null;
rows.forEach(row => {
if (row.offsetParent !== null) {
const cell = row.querySelector(CONFIG_MAIN.SELECTORS.ORDERED_STATUS_CELL);
if (cell?.textContent.includes('Ordered')) {
if (!firstOrderedRow) firstOrderedRow = row;
hasOrdered = true;
}
}
});
const btn = document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN);
if (btn) {
if (hasOrdered) {
if (!btn.classList.contains('btn-warning')) {
btn.className = 'btn btn-warning verify1-btn';
btn.innerText = 'VERIFY1 (F7)';
btn.style.setProperty('background-color', '#fab641', 'important');
btn.style.setProperty('color', '#050505', 'important');
}
if (firstOrderedRow && !hasScrolledToOrderedRow) {
document.documentElement.style.scrollBehavior = 'smooth';
firstOrderedRow.scrollIntoView({ behavior: 'auto', block: 'center' });
hasScrolledToOrderedRow = true;
setTimeout(() => document.documentElement.style.scrollBehavior = 'auto', 1000);
}
} else {
if (btn.classList.contains('btn-warning')) {
btn.className = 'btn btn-success btn-sm verify1-btn';
btn.innerText = 'VERIFY1 (F7)';
btn.style.setProperty('background-color', '#28a745', 'important');
btn.style.setProperty('color', '#ffffff', 'important');
}
hasScrolledToOrderedRow = false;
}
}
}
setInterval(monitorOrderedStatus, CONFIG_MAIN.CHECK_INTERVALS.ORDERED_SCAN);
const toastObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
let successToast = null;
if (node.matches(CONFIG_MAIN.SELECTORS.TOAST.SUCCESS)) {
successToast = node;
} else if (node.querySelector) {
successToast = node.querySelector(CONFIG_MAIN.SELECTORS.TOAST.SUCCESS);
}
if (successToast) {
successToast.addEventListener('click', () => {
if ((verify1Clicked && verify1Toggle) || (verify2Clicked && verify2Toggle)) {
window.history.back();
}
verify1Clicked = false;
verify2Clicked = false;
}, { once: true });
}
}
});
});
});
toastObserver.observe(document.body, { childList: true, subtree: true });
document.addEventListener('keydown', e => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
return;
}
if (e.key === 'F7') {
e.preventDefault();
document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY1_BTN)?.click();
} else if (e.key === 'F8') {
e.preventDefault();
document.querySelector(CONFIG_MAIN.SELECTORS.VERIFY2_BTN)?.click();
}
});
// --- Integrated Alerts Scanner Code (Critical, Dilution, No-Result) ---
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,
FLASH_COLOR: "pink",
FLASH_INTERVAL: 700,
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"]',
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',
PATIENT_LOCATION_SELECTOR: 'span[title*="UNIT/"]',
USER_NAME_SELECTOR: 'div.profile-wrapper span.csi-dropdown-btn-text',
SAMPLE_BARCODE_SELECTOR: 'div[style*="font-size: 13px; color: rgb(68, 68, 68);"]',
LOCAL_SERVER_SAVE_URL: 'http://localhost:5000/save-alerts'
};
let isScanningActive = false;
let issueScanIntervalId = null;
let isAlertModalOpen = false;
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';
row.dataset.originalBg = originalBg;
row.style.transition = "background-color 0.5s ease";
let isPink = false;
const intervalId = setInterval(() => {
if (!document.body.contains(row) || row.dataset.flashing === 'false') {
clearInterval(intervalId);
row.style.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();
}
function stopFlashingEffect(row) {
if (row && row.dataset.flashing === 'true') {
row.dataset.flashing = 'false';
const intervalId = parseInt(row.dataset.flashIntervalId, 10);
if (!isNaN(intervalId)) {
clearInterval(intervalId);
}
}
}
function getNotificationSessionKey(type, identifier = 'general') {
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;
}
const notifiedPersonNameInput = document.getElementById('notifiedPersonNameInput');
const notifiedPersonTelExtInput = document.getElementById('notifiedPersonTelExtInput');
const readBackCheckbox = document.getElementById('readBackCheckbox');
const userIdInput = document.getElementById('userIdInput');
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;
const time = `${formattedHours}:${minutes} ${ampm}`;
const entryID = getNextEntryID();
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'),
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,
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);
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) {
const parts = alert.rowId.split('_');
const type = parts[0];
const baseIdentifier = parts.slice(1).join('_');
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);
}
}
overlay.style.opacity = '0';
if (modalContent) {
modalContent.style.opacity = '0';
modalContent.style.transform = 'translateY(-30px) scale(0.95)';
}
overlay.addEventListener('transitionend', () => {
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
isAlertModalOpen = false;
document.body.style.overflow = '';
logAlertDebug("Alert modal overlay removed, isAlertModalOpen = false");
}
}, { once: true });
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);
} else {
isAlertModalOpen = false;
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)',
zIndex: '10001',
display: 'flex', justifyContent: 'center', alignItems: 'center',
opacity: '0', transition: 'opacity 0.3s ease-out',
padding: '20px'
});
document.body.style.overflow = 'hidden';
const modalBox = document.createElement('div');
modalBox.id = 'custom-alert-modal-content';
Object.assign(modalBox.style, {
backgroundColor: '#ffffff',
borderRadius: '12px',
boxShadow: '0 16px 32px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.08)',
width: 'calc(100% - 40px)',
maxWidth: '1100px',
maxHeight: '90vh',
overflowY: 'auto',
padding: '32px',
position: 'relative',
opacity: '0', transform: 'translateY(-20px) scale(0.98)',
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',
display: 'flex', flexDirection: 'column', gap: '16px'
});
const closeButton = document.createElement('button');
closeButton.innerHTML = '×';
Object.assign(closeButton.style, {
position: 'absolute', top: '18px', right: '18px',
fontSize: '30px', lineHeight: '1', cursor: 'pointer', color: '#adb5bd',
background: 'transparent', border: 'none', padding: '0', width: '30px', height: '30px',
display: 'flex', alignItems: 'center', justifyContent: 'center'
});
closeButton.onmouseover = () => closeButton.style.color = '#495057';
closeButton.onmouseout = () => closeButton.style.color = '#adb5bd';
closeButton.onclick = closeAlertModalAction;
modalBox.appendChild(closeButton);
const headerDiv = document.createElement('div');
Object.assign(headerDiv.style, {
display: 'flex', alignItems: 'center',
gap: '12px',
paddingBottom: '18px', borderBottom: '1px solid #dee2e6', marginBottom: '16px'
});
const alertIcon = document.createElement('i');
alertIcon.className = 'fas fa-exclamation-triangle';
let iconColor = '#17a2b8';
let alertTitleText = 'Lab Alert Detected';
if (alertData.overallSeverity === 'critical') {
alertTitleText = 'Critical Lab Alert!'; iconColor = '#d9534f';
} else if (alertData.overallSeverity === 'noresult') {
alertTitleText = 'No Result Detected'; iconColor = '#ffc107';
} else if (alertData.overallSeverity === 'greaterThan') {
alertTitleText = 'Dilution Required'; iconColor = '#ffc107';
}
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' });
headerDiv.appendChild(dateTime);
modalBox.appendChild(headerDiv);
const allPatientInfoContainer = document.createElement('div');
Object.assign(allPatientInfoContainer.style, {
display: 'flex',
flexDirection: 'column',
gap: '18px',
paddingBottom: '18px',
borderBottom: '1px solid #dee2e6',
marginBottom: '16px'
});
const createInfoItem = (label, value, iconClass) => {
if (!value) value = 'N/A';
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' });
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'
});
itemDiv.appendChild(valueElem);
return itemDiv;
};
const pNameItem = createInfoItem('Patient Name', alertData.patientName, 'fa-user');
if (pNameItem) {
allPatientInfoContainer.appendChild(pNameItem);
}
const otherPatientInfoRow = document.createElement('div');
Object.assign(otherPatientInfoRow.style, {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
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);
}
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',
});
modalBox.appendChild(alertsListTitle);
const alertsContainer = document.createElement('div');
Object.assign(alertsContainer.style, {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
gap: '18px',
padding: '5px 0'
});
alertData.alertsToList.forEach(alert => {
const alertCard = document.createElement('div');
let borderColor = '#6c757d', bgColor = '#f8f9fa', textColor = '#343a40';
if (alert.type === 'critical') {
if (alert.flag === 'CL') {
borderColor = '#007bff'; bgColor = '#e7f3ff';
} else if (alert.flag === 'CH') {
borderColor = '#dc3545'; bgColor = '#f8d7da';
} else {
borderColor = '#d9534f'; bgColor = '#fdf7f7';
}
} else if (alert.type === 'greaterThan') {
borderColor = '#ffc107'; bgColor = '#fff9e6';
} else if (alert.type === 'noresult') {
borderColor = '#6c757d'; bgColor = '#f8f9fa';
}
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) {
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) {
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);
}
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' });
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');
if (userIdInputElem && alertData.userId && alertData.userId !== 'N/A') {
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);
}
const buttonContainer = document.createElement('div');
Object.assign(buttonContainer.style, {
display: 'flex', justifyContent: 'flex-end',
paddingTop: '24px', marginTop: 'auto',
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') {
button.style.backgroundColor = '#28a745'; button.style.color = 'white';
button.onmouseover = () => button.style.backgroundColor = '#218838';
button.onmouseout = () => button.style.backgroundColor = '#28a745';
} else {
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)';
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);
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);
} else {
const okButton = createModalButton('OK', 'primary');
okButton.onclick = closeAlertModalAction;
buttonContainer.appendChild(okButton);
}
modalBox.appendChild(buttonContainer);
modalBox.dataset.alerts = JSON.stringify(alertsDisplayedInModal.map(a => ({ type: a.type, rowId: a.rowId })));
overlay.appendChild(modalBox);
document.body.appendChild(overlay);
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);
overlay.addEventListener('click', (event) => {
if (event.target === overlay) {
closeAlertModalAction();
}
});
const escapeKeyListener = (event) => {
if (event.key === 'Escape' && document.body.contains(overlay)) {
closeAlertModalAction();
document.removeEventListener('keydown', escapeKeyListener);
}
};
document.addEventListener('keydown', escapeKeyListener);
}
function scrollToRowNearest(row) {
if (!row) return false;
let rowToScroll = row;
if (row.closest('.ag-center-cols-container')) {
const rowIndex = row.getAttribute('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' });
setTimeout(() => document.documentElement.style.scrollBehavior = 'auto', 500);
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' };
function elementsPresent() {
return document.querySelector(selectors.name) &&
document.querySelector(selectors.mrn) &&
document.querySelector(selectors.location);
}
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) {
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.`);
}
const nameEl = document.querySelector(selectors.name);
patientData.patientName = nameEl ? nameEl.textContent.trim().replace(/\s+/g, ' ') : 'N/A';
const mrnEl = document.querySelector(selectors.mrn);
patientData.patientId = mrnEl ? mrnEl.textContent.trim() : 'N/A';
const locElement = document.querySelector(selectors.location);
patientData.patientLocation = locElement ? (locElement.title?.trim() || locElement.textContent?.trim()) : 'N/A';
if (patientData.patientLocation === '') patientData.patientLocation = 'N/A';
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;
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];
if (!centerRow || !pinnedRow || centerRow.offsetParent === null) return;
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);
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, '');
const flagText = flagSpan?.textContent?.trim()?.toUpperCase() || '';
const isNoResult = ["noresult", "noxresult", "x-noresult"].includes(normalizedResultText);
const isDilution = resultText.includes(">");
const isCritical = (flagSpan && (flagText === "CL" || flagText === "CH"));
let alertType = null;
let alertMessage = "";
let alertComment = null;
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";
}
if (alertType) {
if (!hasAlreadyNotified(alertType, baseRowIdentifier)) {
potentialAlerts.push({
rowElement: centerRow, type: alertType,
rowId: `${alertType}_${baseRowIdentifier}`,
baseIdentifier: baseRowIdentifier,
message: alertMessage,
comment: alertComment,
testName: testDesc, result: resultText, uom: uomText, flag: flagText
});
}
applyFlashingEffect(centerRow);
applyFlashingEffect(pinnedRow);
} else {
stopFlashingEffect(centerRow);
stopFlashingEffect(pinnedRow);
}
});
if (potentialAlerts.length > 0 && !isAlertModalOpen && !document.getElementById('custom-alert-modal-overlay')) {
const firstSignificantAlert =
potentialAlerts.find(a => a.type === 'critical') ||
potentialAlerts.find(a => a.type === 'noresult') ||
potentialAlerts.find(a => a.type === 'greaterThan') ||
potentialAlerts[0];
scrollToRowNearest(firstSignificantAlert.rowElement);
const modalData = {
alertsToList: potentialAlerts.map(a => ({
testName: a.testName, result: a.result, uom: a.uom, flag: a.flag, type: a.type,
rowId: a.rowId,
comment: a.comment
})),
overallSeverity: firstSignificantAlert.type,
primaryMessage: firstSignificantAlert.message,
patientName: patientName,
patientId: patientId,
patientLocation: patientLocation,
sampleBarcode: sampleBarcode,
userId: currentUserId
};
createCustomAlert(modalData, potentialAlerts);
// Pause scanner after showing alerts for this patient
logAlertDebug("Alerts shown for patient. Pausing scanner.");
if (window.stopAlertsScanner) window.stopAlertsScanner();
return true;
} else if (potentialAlerts.length === 0) {
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;
}
function startAlertsScannerInternal() {
logAlertDebug("startAlertsScannerInternal called.");
if (!isScanningActive && issueScanIntervalId === null) {
logAlertDebug("Alert Scanner starting/resuming...");
isScanningActive = true;
if (issueScanIntervalId) clearInterval(issueScanIntervalId);
checkForIssues();
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() {
logAlertDebug("stopAlertsScannerInternal called (pause).");
if (isScanningActive) {
logAlertDebug("Alert Scanner pausing...");
clearInterval(issueScanIntervalId);
issueScanIntervalId = null; // Keep isScanningActive true, but clear interval
// Do not set isScanningActive = false here if we want it to resume on new patient
// Flashing rows will stop naturally or when modal is closed / page changes.
logAlertDebug("Scanner interval cleared for pause. isScanningActive remains true.");
} else {
logAlertDebug("Alert Scanner not active, no action taken by stop/pause.");
}
}
window.startAlertsScanner = startAlertsScannerInternal;
window.stopAlertsScanner = stopAlertsScannerInternal; // This is now more of a "pause"
let previousObservedUrl = window.location.href;
let currentPatientProcessed = false; // Flag to manage scanner pause/resume per patient
const pageUrlObserver = new MutationObserver(() => {
const newObservedUrl = window.location.href;
if (newObservedUrl !== previousObservedUrl) {
logDebugMain(`URL changed from: ${previousObservedUrl} to: ${newObservedUrl}.`);
currentPatientProcessed = false; // Reset flag for new URL
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.`);
isScanningActive = false; // Set to false to truly stop
if (window.stopAlertsScanner) window.stopAlertsScanner();
// Clear session storage flags for the *specific page* being left
try {
const oldUrlObject = new URL(previousObservedUrl, window.location.origin);
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}`);
}
}
if (nowOnEditPage) {
logDebugMain(`Now on a new edit page (${newObservedUrl}). Setting up features and scanner.`);
hasScrolledToOrderedRow = false;
isAlertModalOpen = false;
setTimeout(() => {
// Only start scanner if not already paused for this new patient (currentPatientProcessed is false)
if (!currentPatientProcessed && window.startAlertsScanner) {
window.startAlertsScanner();
}
addButtons();
addFontAwesome();
monitorOrderedStatus();
}, 700);
} else {
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; // Ensure it's marked as not active
window.stopAlertsScanner();
}
}
previousObservedUrl = newObservedUrl;
} else {
const stillOnEditPage = newObservedUrl.startsWith(CONFIG_MAIN.URLS.EDIT_PAGE_PREFIX);
if (stillOnEditPage) {
if (!document.getElementById('custom-script-buttons')) {
logDebugMain("Buttons missing on edit page (no URL change), re-adding.");
addButtons();
}
addFontAwesome();
// If scanner was paused 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 stopped and needs restart on same URL
logDebugMain("Scanner not active on current edit page, attempting restart.");
window.startAlertsScanner();
}
}
}
});
pageUrlObserver.observe(document.body, { childList: true, subtree: true });
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);
} else {
logDebugMain("Initial page load not on correct page. No features started.");
}
setInterval(checkUrlAndTriggerClickForUndefined, CONFIG_MAIN.CHECK_INTERVALS.UNDEFINED_URL);
setInterval(checkForDisabledButtons, CONFIG_MAIN.CHECK_INTERVALS.DISABLED_BTN_CHECK);
})();
// User's SAMPLES COUNT Logic (IIFE) - Integrated and Styles Updated
(function () {
'use strict';
function createCounterElement(id) {
const wrapper = document.createElement('span');
wrapper.id = id;
Object.assign(wrapper.style, {
display: 'inline-flex',
alignItems: 'center',
padding: '6px 12px',
backgroundColor: '#e9ecef',
borderRadius: '20px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)',
marginRight: 'auto',
marginLeft: '10px',
fontSize: '14px',
fontWeight: '500',
color: '#495057',
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';
badge.textContent = '0';
Object.assign(badge.style, {
backgroundColor: '#6c757d',
color: '#ffffff',
padding: '4px 10px',
borderRadius: '12px',
fontSize: '14px',
fontWeight: 'bold',
minWidth: '26px',
textAlign: 'center',
lineHeight: '1'
});
wrapper.appendChild(label);
wrapper.appendChild(badge);
return wrapper;
}
function updateSpecificCounter(modalElement, counterElement, inputSelector) {
const badge = counterElement.querySelector('.sample-count-badge');
if (!badge || !modalElement || !document.body.contains(modalElement) || !modalElement.contains(counterElement)) {
return;
}
const inputs = modalElement.querySelectorAll(inputSelector);
const count = inputs.length;
badge.textContent = count;
badge.style.backgroundColor = count > 0 ? '#28a745' : '#6c757d';
}
function observeModals() {
const activeModalIntervals = new Map();
const observer = new MutationObserver(() => {
const originalModal = document.querySelector('modal-container.show');
// Selectors for WB Modal (Original Modal)
const wbModalFooterSelector = '.modal-footer'; // Target the footer
// **IMPORTANT**: Verify this input selector for the WB modal.
// Inspect the input fields in the WB modal that should be counted.
const wbInputSelectorForCount = 'td input[formcontrolname="PatientID"]';
let originalModalFooter = null;
if (originalModal) {
originalModalFooter = originalModal.querySelector(wbModalFooterSelector);
}
const originalCounterId = 'inline-patientid-counter-original-wb'; // Unique ID for WB counter
// Check if this is the "WB" modal (heuristic: contains app-sample-receive but not app-sample-receive-receptionist)
const isLikelyWBModal = originalModal && originalModal.querySelector('app-sample-receive') && !originalModal.querySelector('app-sample-receive-receptionist');
if (isLikelyWBModal && originalModalFooter && !originalModalFooter.querySelector('#' + originalCounterId)) {
console.debug("[SAMPLES COUNT WB DEBUG] WB Modal identified and Footer found:", originalModalFooter);
const counter = createCounterElement(originalCounterId);
originalModalFooter.insertBefore(counter, originalModalFooter.firstChild); // Insert at the beginning of the footer
console.log("[SAMPLES COUNT] Original modal (WB) counter element created and inserted into footer.");
const interval = setInterval(() => {
const currentFooterForCheck = document.getElementById(originalCounterId)?.closest(wbModalFooterSelector) || (originalModal ? originalModal.querySelector(wbModalFooterSelector) : null);
if (!document.body.contains(originalModal) || (currentFooterForCheck && !currentFooterForCheck.contains(counter))) {
clearInterval(activeModalIntervals.get(originalModal));
activeModalIntervals.delete(originalModal);
const existingCounter = document.getElementById(originalCounterId);
if (existingCounter) existingCounter.remove();
console.log("[SAMPLES COUNT] Original modal (WB) counter cleaned up.");
return;
}
updateSpecificCounter(originalModal, counter, wbInputSelectorForCount);
}, 500);
activeModalIntervals.set(originalModal, interval);
} else if (isLikelyWBModal && !originalModalFooter && !document.getElementById(originalCounterId)) {
console.warn("[SAMPLES COUNT WB DEBUG] WB Modal found, but Footer NOT identified using selector '" + wbModalFooterSelector + "'. Counter cannot be placed.");
console.debug("[SAMPLES COUNT WB DEBUG] WB Modal (for context):", originalModal);
}
// --- Observe New Modal (Sample Receive by CPL/Receptionist) --- (This part seems to be working)
const newModalApp = document.querySelector('.modal-dialog.modal-xlg-fullwidth app-sample-receive-receptionist');
const newModal = newModalApp ? newModalApp.closest('.modal-dialog') : null;
const newModalFooter = newModalApp ? newModalApp.querySelector('.modal-footer') : null;
const newCounterId = 'inline-patientid-counter-new-cpl'; // Unique ID for CPL counter
if (newModal && newModalFooter && !newModalFooter.querySelector('#' + newCounterId)) {
const counter = createCounterElement(newCounterId);
newModalFooter.insertBefore(counter, newModalFooter.firstChild);
console.debug("[SAMPLES COUNT] New modal (CPL) counter created.");
const interval = setInterval(() => {
if (!document.body.contains(newModal) || !newModalFooter.contains(counter)) {
clearInterval(activeModalIntervals.get(newModal));
activeModalIntervals.delete(newModal);
const existingCounter = document.getElementById(newCounterId);
if (existingCounter) existingCounter.remove();
console.debug("[SAMPLES COUNT] New modal (CPL) counter cleaned up.");
} else {
const inputSelector = 'tbody[formarrayname="TubeTypeList"] input[formcontrolname="PatientID"]';
updateSpecificCounter(newModal, counter, inputSelector);
}
}, 500);
activeModalIntervals.set(newModal, interval);
}
});
observer.observe(document.body, { childList: true, subtree: true });
console.debug("[SAMPLES COUNT] Modal observer started.");
}
window.addEventListener('load', () => {
console.debug("[SAMPLES COUNT] Window loaded, initializing modal observer.");
observeModals();
});
})();