Alerts to User (Dynamic Scanning with Notification Persistence)

Continuously scans for specific events and ensures the user gets notifications again if they return to the same event.

// ==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}`);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址