// ==UserScript==
// @name Alerts to User (Dynamic Scanning with Notification Persistence)
// @namespace Violentmonkey Scripts
// @match *://his.kaauh.org/lab/*
// @grant none
// @version 3.5
// @author Hamad AlShegifi
// @description Continuously scans for specific events and ensures the user gets notifications again if they return to the same event.
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Configuration Options
const CONFIG = {
SCAN_INTERVAL: 100, // Interval for scanning (in ms)
FLASH_COLOR: "pink", // Flashing color for rows with ">"
FLASH_INTERVAL: 500, // Interval for flashing effect (in ms)
MODAL_TIMEOUT: 10000, // Timeout for modal auto-dismissal (in ms)
DEBUG_MODE: true, // Enable/disable debug logs
};
// Debug Logging
function logDebug(message) {
if (CONFIG.DEBUG_MODE) console.log(`[Alerts to User] ${message}`);
}
// Function to create a custom modal dialog
function showModal(message, onConfirm) {
// Create overlay
const overlay = document.createElement("div");
overlay.style.position = "fixed";
overlay.style.top = "0";
overlay.style.left = "0";
overlay.style.width = "100%";
overlay.style.height = "100%";
overlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
overlay.style.zIndex = "999";
document.body.appendChild(overlay);
// Create modal
const modal = document.createElement("div");
modal.style.position = "fixed";
modal.style.top = "50%";
modal.style.left = "50%";
modal.style.transform = "translate(-50%, -50%) scale(0.5)";
modal.style.backgroundColor = "#f4f4f9";
modal.style.padding = "30px";
modal.style.boxShadow = "0px 10px 30px rgba(0, 0, 0, 0.15)";
modal.style.zIndex = "1000";
modal.style.borderRadius = "15px";
modal.style.textAlign = "center";
modal.style.transition = "transform 0.3s ease, opacity 0.3s ease";
modal.style.opacity = "0";
// Add heading
const heading = document.createElement("h2");
heading.textContent = "Attention!";
heading.style.fontFamily = "'Arial', sans-serif";
heading.style.color = "#333";
heading.style.marginBottom = "10px";
heading.style.fontSize = "24px";
modal.appendChild(heading);
// Add message
const content = document.createElement("p");
content.textContent = message;
content.style.fontFamily = "'Arial', sans-serif";
content.style.color = "#555";
content.style.marginBottom = "20px";
content.style.fontSize = "16px";
content.style.lineHeight = "1.5";
modal.appendChild(content);
// Add input field for user comment (if onConfirm is provided)
if (onConfirm) {
const input = document.createElement("input");
input.type = "text";
input.placeholder = "Enter your comment";
input.style.padding = "10px";
input.style.width = "100%";
input.style.marginBottom = "20px";
input.style.border = "1px solid #ccc";
input.style.borderRadius = "5px";
input.style.fontSize = "16px";
modal.appendChild(input);
// Add Confirm button
const confirmButton = createButton("Confirm", "#ff4081", () => {
const comment = input.value.trim();
if (comment) {
onConfirm(comment);
closeModal(modal, overlay);
} else {
alert("Please enter a comment!");
}
});
modal.appendChild(confirmButton);
} else {
// Add OK button (for alerts without input)
const okButton = createButton("OK", "#ff4081", () => {
closeModal(modal, overlay);
});
modal.appendChild(okButton);
}
// Append modal to the body
document.body.appendChild(modal);
// Animate modal appearance
setTimeout(() => {
modal.style.transform = "translate(-50%, -50%) scale(1)";
modal.style.opacity = "1";
}, 10);
// Auto-dismiss modal after timeout
setTimeout(() => {
closeModal(modal, overlay);
}, CONFIG.MODAL_TIMEOUT);
}
// Helper function to create styled buttons
function createButton(text, backgroundColor, onClick) {
const button = document.createElement("button");
button.textContent = text;
button.style.padding = "10px 20px";
button.style.border = "none";
button.style.backgroundColor = backgroundColor;
button.style.color = "white";
button.style.borderRadius = "30px";
button.style.cursor = "pointer";
button.style.fontSize = "16px";
button.style.transition = "background-color 0.3s ease, transform 0.2s ease";
// Hover effects
button.addEventListener("mouseenter", () => {
button.style.backgroundColor = darkenColor(backgroundColor, 20);
button.style.transform = "scale(1.05)";
});
button.addEventListener("mouseleave", () => {
button.style.backgroundColor = backgroundColor;
button.style.transform = "scale(1)";
});
// Click handler
button.addEventListener("click", onClick);
return button;
}
// Helper function to darken a color
function darkenColor(color, percent) {
const num = parseInt(color.slice(1), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) - amt;
const G = ((num >> 8) & 0x00ff) - amt;
const B = (num & 0x0000ff) - amt;
return `#${(0x1000000 + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
(B < 255 ? (B < 1 ? 0 : B) : 255))
.toString(16)
.slice(1)}`;
}
// Function to close a modal
function closeModal(modal, overlay) {
modal.style.transform = "translate(-50%, -50%) scale(0.5)";
modal.style.opacity = "0";
overlay.style.opacity = "0";
setTimeout(() => {
document.body.removeChild(modal);
document.body.removeChild(overlay);
}, 300); // Match transition duration
}
// Function to apply flashing effect to rows
function applyFlashingEffect(rows) {
rows.forEach(row => {
row.style.transition = "background-color 0.5s ease";
let isPink = false;
const intervalId = setInterval(() => {
isPink = !isPink;
row.style.backgroundColor = isPink ? CONFIG.FLASH_COLOR : "transparent";
}, CONFIG.FLASH_INTERVAL);
// Stop flashing if the row is removed from the DOM
const observer = new MutationObserver(() => {
if (!document.contains(row)) {
clearInterval(intervalId);
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
// Function to replace "NO RESULT" or "NO-XRESULT" with user comment
function replacePlaceholderWithComment(divElement, comment) {
if (divElement) {
divElement.innerText = comment;
}
}
// Function to check for issues in the data
function checkForIssues() {
const resultDivs = document.querySelectorAll(
'div[role="gridcell"][col-id="TestResult"] app-result-value-render div'
);
const criticalDivs = document.querySelectorAll(
'div[role="gridcell"][col-id="LTFlag"] app-ref-high-low div span.critial-alret-indication'
);
let hasMatch = false;
// Check for ">", "NO RESULT", and "NO-XRESULT"
resultDivs.forEach(div => {
const text = div.textContent.trim().toLowerCase();
if (text.includes(">") && !hasAlreadyNotified("greaterThan")) {
const row = div.closest("div[role='row']");
if (row) {
applyFlashingEffect([row]);
showModal("Dilution is required for this sample (> detected)!");
setNotificationFlag("greaterThan");
hasMatch = true;
}
}
if ((text === "no result" || text === "no-xresult") && !hasAlreadyNotified(text)) {
showModal(`Please enter a comment to replace '${text.toUpperCase()}':`, (comment) => {
replacePlaceholderWithComment(div, comment);
setNotificationFlag(text);
});
hasMatch = true;
}
});
// Check for critical results (CL/CH)
criticalDivs.forEach(div => {
const text = div.textContent.trim();
if (text === "CL" && !hasAlreadyNotified("CL")) {
showModal("CRITICAL LOW RESULT DETECTED !!");
setNotificationFlag("CL");
hasMatch = true;
} else if (text === "CH" && !hasAlreadyNotified("CH")) {
showModal("CRITICAL HIGH RESULT DETECTED !!");
setNotificationFlag("CH");
hasMatch = true;
}
});
return hasMatch;
}
// Function to check if a user has already been notified about a specific event
function hasAlreadyNotified(eventKey) {
return localStorage.getItem(eventKey) !== null;
}
// Function to set a notification flag in localStorage
function setNotificationFlag(eventKey) {
localStorage.setItem(eventKey, true);
}
// Continuous scanning function
function startContinuousScanning() {
logDebug("Starting continuous scanning...");
const intervalId = setInterval(() => {
const hasMatch = checkForIssues();
if (hasMatch) {
clearInterval(intervalId);
logDebug("Match found. Stopping continuous scanning.");
}
}, CONFIG.SCAN_INTERVAL);
}
// Use MutationObserver to detect dynamic content loading
function observeDOMChanges() {
const targetNode = document.body;
const config = { childList: true, subtree: true };
const observer = new MutationObserver(() => {
const resultDivs = document.querySelectorAll(
'div[role="gridcell"][col-id="TestResult"] app-result-value-render div'
);
if (resultDivs.length > 0) {
logDebug("Relevant elements detected. Starting continuous scanning...");
startContinuousScanning();
observer.disconnect(); // Stop observing once elements are found
}
});
observer.observe(targetNode, config);
}
// Main execution
try {
window.addEventListener('load', () => {
logDebug("Page fully loaded. Observing DOM changes...");
observeDOMChanges();
});
} catch (error) {
logDebug(`An error occurred: ${error.message}`);
}
})();