Enhanced AG Grid Functionality

Adds vibrant row hover effect, conditional status styling, emergency row highlighting, and 1-min green highlight on row click.

目前為 2025-05-21 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Enhanced AG Grid Functionality
// @version      4.5.0
// @description  Adds vibrant row hover effect, conditional status styling, emergency row highlighting, and 1-min green highlight on row click.
// @match        https://his.kaauh.org/lab/*
// @author       Hamad AlShegifi (updated by AI)
// @grant        GM_addStyle
// @namespace    http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    // Constants for row click highlight
    const CLICKED_ROW_EXPIRY_PREFIX = 'clicked_row_expiry_';
    const CLICK_DURATION_MS = 60 * 1000; // 1 minute

    // Inject CSS
    GM_addStyle(`
        .ag-row {
            transition: background-color 0.3s ease;
        }
        .vibrant-hover {
            /* Background color and text color are now applied to cells within the hovered row */
        }
        .ag-row.vibrant-hover .ag-cell {
            background-color: #87dced !important; /* Light blue color */
            color: black !important; /* Black text color */
            font-weight: bold !important; /* Bold font */
        }
        .ag-cell[col-id="testStatus"].status-verified-level2,
        .ag-cell[col-id="testStatus"].status-verified-level1 {
            background-color: #90EE90 !important; /* Light green color */
        }
        .ag-cell[col-id="testStatus"].status-ordered {
            background-color: #FFFFE0 !important; /* Light yellow color */
        }
        .ag-cell[col-id="testStatus"].status-resulted {
            background-color: #FFA500 !important; /* Orange color */
            color: black !important; /* Ensure text is readable on orange */
        }
        .emergency-row {
            background-color: #ffe0e0 !important; /* Light red */
        }
        .ag-cell[col-id="sampleStatus"].status-received {
            background-color: #90EE90 !important; /* Light green color for Received sample status */
        }
        /* Styles for clicked row highlight */
        .ag-row.clicked-row-green .ag-cell {
            background-color: #A0ECA0 !important; /* A distinct, pleasant green for clicked row */
            /* color: black !important; */ /* Optional: if text color needs to change for contrast */
        }
    `);

    // List of col-id attributes to target for hover effect
    const targetColumnIdsForHover = [
        "orderNo", "testId", "testDescription", "clusterMrn",
        "hospitalMrn", "patientName", "dob", "nationalIqamaId",
        "department", "clinic", "doctor", "analyzer",
        "orderDateAndTime", "lastUpdatedDate", "sampleStatus",
        "referenceLab", "accessionNo", "barcode", "sequenceNo",
        "primaryPatientId", "referenceLabDesc", "testStatus",
        "orderLastModifiedOnEpoch", "orderCreatedOnEpoch", "equipmentName", "doctorName", "localMrn",
        "dateOfBirth", "idNumber"
    ];

    const columnsToUncheck = [
        'Lab Order No', 'Hospital MRN', 'DOB', 'Test ID', 'National/Iqama Id',
        'Department', 'Doctor', 'Analyzer', 'Reference Lab',
        'Accession No', 'Sequence No','Age','Container Type','Storage Condition'
    ];

    let hasRunOnce = false; // Prevents running the column unchecking code more than once

    // --- START: New functions for persistent green row highlight ---
    function handleRowClickForPersistentGreen(event) {
        // Ensure the click is on a cell, then find the row
        const cellElement = event.target.closest('.ag-cell');
        if (!cellElement) return;

        const rowElement = cellElement.closest('.ag-row[role="row"]');
        if (!rowElement) return;

        const barcodeCell = rowElement.querySelector('div[col-id="barcode"]');
        if (!barcodeCell || !barcodeCell.textContent) {
            // console.log("Clicked row has no barcode cell or content for persistent green.");
            return;
        }

        const barcode = barcodeCell.textContent.trim();
        if (!barcode) {
            // console.log("Barcode is empty for persistent green.");
            return;
        }

        const expiryTimestamp = Date.now() + CLICK_DURATION_MS;
        try {
            localStorage.setItem(CLICKED_ROW_EXPIRY_PREFIX + barcode, expiryTimestamp.toString());
            // console.log(`[PersistentGreen] Set ${CLICKED_ROW_EXPIRY_PREFIX + barcode} to ${expiryTimestamp}`);
        } catch (e) {
            console.error("[PersistentGreen] Error saving to localStorage:", e);
        }

        applyPersistentRowStyles(); // Re-apply styles to all rows to reflect the change
    }

    function applyPersistentRowStyles() {
        const rows = document.querySelectorAll('.ag-center-cols-container div[role="row"], .ag-pinned-left-cols-container div[role="row"], .ag-pinned-right-cols-container div[role="row"]');
        const now = Date.now();
        // console.log(`[PersistentGreen] Applying styles. Found ${rows.length} rows. Time: ${now}`);

        rows.forEach(row => {
            const barcodeCell = row.querySelector('div[col-id="barcode"]');
            let rowBarcode = null;
            if (barcodeCell && barcodeCell.textContent) {
                rowBarcode = barcodeCell.textContent.trim();
            }

            if (rowBarcode) {
                const expiryKey = CLICKED_ROW_EXPIRY_PREFIX + rowBarcode;
                const expiryTimestampStr = localStorage.getItem(expiryKey);

                if (expiryTimestampStr) {
                    const expiryTimestamp = parseInt(expiryTimestampStr, 10);
                    if (now < expiryTimestamp) {
                        row.classList.add('clicked-row-green');
                        // console.log(`[PersistentGreen] Row ${rowBarcode} is active green. Expires at ${expiryTimestamp}`);
                    } else {
                        // console.log(`[PersistentGreen] Row ${rowBarcode} expired. Removing highlight and localStorage item.`);
                        localStorage.removeItem(expiryKey);
                        row.classList.remove('clicked-row-green');
                    }
                } else {
                    // Not in storage, ensure not green
                    row.classList.remove('clicked-row-green');
                }
            } else {
                // Row has no barcode, ensure it's not green from this mechanism
                row.classList.remove('clicked-row-green');
            }
        });

        // General cleanup for expired items in localStorage (for barcodes not currently in DOM)
        try {
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                if (key && key.startsWith(CLICKED_ROW_EXPIRY_PREFIX)) {
                    const expiryTimestampStr = localStorage.getItem(key);
                    if (expiryTimestampStr) { // Check if item still exists
                        const expiryTimestamp = parseInt(expiryTimestampStr, 10);
                        if (now >= expiryTimestamp) {
                            // console.log(`[PersistentGreen] Cleaning up expired localStorage key: ${key}`);
                            localStorage.removeItem(key);
                        }
                    }
                }
            }
        } catch (e) {
            console.error("[PersistentGreen] Error during localStorage cleanup:", e);
        }
    }

    function setupPersistentRowStylesListener() {
        const gridRoot = document.querySelector('.ag-root-wrapper'); // Preferred target
        let listenerTarget = null;

        if (gridRoot) {
            listenerTarget = gridRoot;
        } else {
            const bodyViewport = document.querySelector('.ag-body-viewport');
            if (bodyViewport) {
                listenerTarget = bodyViewport;
            } else {
                // console.warn("[PersistentGreen] AG Grid root/viewport not found. Attaching listener to document.body as a fallback.");
                listenerTarget = document.body; // Fallback to body
            }
        }

        if (listenerTarget && !listenerTarget.dataset.persistentClickListenerAttached) {
            // console.log(`[PersistentGreen] Attaching click listener to ${listenerTarget.tagName || 'document.body'}`);
            listenerTarget.addEventListener('click', handleRowClickForPersistentGreen, true); // Using capture phase
            listenerTarget.dataset.persistentClickListenerAttached = 'true';
        }
    }
    // --- END: New functions for persistent green row highlight ---

    function isSpecificPage() {
        return window.location.href.endsWith('/#/lab-orders/lab-test-analyzer');
    }

    function areColumnsChecked() {
        return columnsToUncheck.some(column => isColumnChecked(column));
    }

    function isColumnChecked(labelText) {
        const labels = document.querySelectorAll('.ag-column-tool-panel-column-label');
        for (const label of labels) {
            if (label.textContent.trim() === labelText) {
                const checkbox = label.parentElement.querySelector('.ag-icon-checkbox-checked');
                if (checkbox) return true; // Column is checked
            }
        }
        return false; // Column is not checked
    }

    function ensureColumnsUnchecked() {
        if (hasRunOnce || !isSpecificPage()) return;
        if (!areColumnsChecked()) return; // Do nothing if columns are not checked

        hasRunOnce = true;
        // console.log("Unchecking checked columns...");

        setTimeout(() => {
            columnsToUncheck.forEach(column => clickColumnLabel(column));
        }, 1000);
    }

    function ensureOtherColumnsChecked() {
        if (!isSpecificPage()) return;
        // console.log("Ensuring all other columns are checked...");

        const allLabels = document.querySelectorAll('.ag-column-tool-panel-column-label');
        allLabels.forEach(label => {
            const labelText = label.textContent.trim();
            if (!columnsToUncheck.includes(labelText)) {
                const checkbox = label.parentElement.querySelector('.ag-icon-checkbox-unchecked');
                if (checkbox) {
                    label.click(); // Click to check the column if unchecked
                }
            }
        });
    }

    function clickColumnLabel(labelText) {
        if (!isSpecificPage()) return;
        const labels = document.querySelectorAll('.ag-column-tool-panel-column-label');
        labels.forEach(label => {
            if (label.textContent.trim() === labelText) {
                const checkbox = label.parentElement.querySelector('.ag-icon-checkbox-checked');
                if (checkbox) {
                    label.click(); // Click only if checked
                }
            }
        });
    }

    function highlightEmergencyRows() {
        const rows = document.querySelectorAll('div[role="row"]');

        rows.forEach(row => {
            const clinicCell = row.querySelector('div[col-id="clinic"]');
            if (clinicCell && clinicCell.textContent.trim() === 'EMERGENCY') {
                row.classList.add('emergency-row');
            } else {
                row.classList.remove('emergency-row'); // Remove the class if the condition is no longer met
            }
        });
    }

    function initColumnToggle() {
        if (!isSpecificPage()) return;
        // console.log("Checking if columns need to be unchecked and highlighting emergency rows...");

        let attempts = 0;
        const interval = setInterval(() => {
            if (document.querySelector('.ag-side-buttons')) {
                ensureColumnsUnchecked();
                ensureOtherColumnsChecked();
                highlightEmergencyRows(); // Call the highlighting function here
                clearInterval(interval);
            }
            if (++attempts > 10) {
                highlightEmergencyRows(); // Ensure highlighting runs even if sidebar isn't found quickly
                clearInterval(interval);
            }
        }, 500);

        // Also run highlighting on initial load attempt
        highlightEmergencyRows();
    }

    // Function to apply hover effects to the entire row when any target cell is hovered
    function applyHoverEffect() {
        document.querySelectorAll('.ag-cell').forEach(cell => {
            const colId = cell.getAttribute('col-id');
            if (colId && targetColumnIdsForHover.includes(colId)) {
                const row = cell.closest('.ag-row'); // Find the parent row
                if (row) {
                    // Add hover effect to the entire row on mouseenter
                    cell.addEventListener('mouseenter', () => {
                        row.classList.add('vibrant-hover'); // Add hover effect to the row
                    });
                    // Remove hover effect on mouseleave
                    cell.addEventListener('mouseleave', () => {
                        row.classList.remove('vibrant-hover'); // Remove hover effect from the row
                    });
                }
            }
        });
    }

    // Function to apply conditional background color to the "Status" column
    function applyStatusCellStyle() {
        document.querySelectorAll('.ag-cell[col-id="testStatus"]').forEach(cell => {
            const statusValue = cell.textContent.trim();
            cell.classList.remove('status-verified-level2', 'status-verified-level1', 'status-ordered', 'status-resulted'); // Remove previous styles

            if (statusValue === "VerifiedLevel2" || statusValue === "VerifiedLevel1") {
                cell.classList.add('status-verified-level2');
            } else if (statusValue === "Ordered") {
                cell.classList.add('status-ordered');
            } else if (statusValue === "Resulted") {
                cell.classList.add('status-resulted');
            }
        });

        // Apply style for Sample Status = Received
        document.querySelectorAll('.ag-cell[col-id="sampleStatus"]').forEach(cell => {
            if (cell.textContent.trim() === "Received") {
                cell.classList.add('status-received');
            } else {
                cell.classList.remove('status-received'); // Ensure the class is removed if the value changes
            }
        });
    }

    const observer = new MutationObserver(() => {
        applyHoverEffect();
        applyStatusCellStyle();
        highlightEmergencyRows();
        applyPersistentRowStyles(); // ADDED
        setupPersistentRowStylesListener(); // ADDED: Ensure listener is active/reattached

        if (isSpecificPage()) {
            hasRunOnce = false;
            initColumnToggle();
        }
    });

    // **Important: Observing the correct AG Grid body viewport**
    const gridBodyViewport = document.querySelector('.ag-body-viewport'); // Observe the grid body viewport
    if (gridBodyViewport) {
        observer.observe(gridBodyViewport, { childList: true, subtree: false }); // subtree should be false here as we are observing direct children (rows)
    } else {
        console.warn("AG Grid body viewport not found for observer. Falling back to body observer.");
        observer.observe(document.body, { childList: true, subtree: true });
    }


    window.addEventListener('load', () => {
        applyHoverEffect();
        applyStatusCellStyle();
        highlightEmergencyRows(); // Ensure consistency with observer
        applyPersistentRowStyles(); // ADDED: Apply on initial load
        setupPersistentRowStylesListener(); // ADDED: Setup listener on initial load

        if (isSpecificPage()) {
            initColumnToggle();
        }
    });
})();


// == The following IIFEs are from the original script and remain unchanged ==

// IIFE for Barcode display box functionality
(function () {
  'use strict';

  const BARCODE_KEY = 'selectedBarcode';
  let currentUrl = location.href;

  // Load JsBarcode if needed
  function loadJsBarcode(callback) {
    if (window.JsBarcode) return callback();
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/JsBarcode.all.min.js';
    script.onload = callback;
    document.head.appendChild(script);
  }

  // Insert barcode box
  function insertBarcodeBox(barcode) {
    if (!barcode) return;

    const btnArea = document.querySelector('.btn-area.stickey-btnset');
    if (!btnArea || document.getElementById('barcode-svg')) return;

    const box = document.createElement('div');
    box.style = 'margin-top:10px;padding:8px 12px;background:#f7f7f7;border-radius:8px;display:flex;align-items:center;gap:10px;border:1px solid #ccc;';

    const label = document.createElement('div');
    label.textContent = 'Sample Barcode:';
    label.style = 'font-weight:bold;font-size:14px;color:#333;';

    const text = document.createElement('div');
    text.textContent = barcode;
    text.style = 'font-size:13px;color:#444;';

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.id = "barcode-svg";
    svg.style = 'height:40px;width:120px;border:1px solid #ccc;border-radius:4px;padding:2px;';

    box.append(label, text, svg);
    btnArea.appendChild(box);

    loadJsBarcode(() => {
      try {
        JsBarcode(svg, barcode, {
          format: "CODE128",
          displayValue: false,
          height: 40,
          width: 2,
          margin: 0
        });
        // console.log('[✓] Barcode rendered');
      } catch (err) {
        console.warn('Barcode render error:', err);
      }
    });
  }

  // Watch for AG-Grid clicks to collect barcode for the display box
  function watchGridClicksForBarcodeBox() {
    document.body.addEventListener('click', e => {
      const row = e.target.closest('.ag-row');
      const cell = row?.querySelector('[col-id="barcode"]');
      if (cell) {
        const barcode = cell.textContent.trim();
        if (barcode) {
          localStorage.setItem(BARCODE_KEY, barcode);
          // console.log('[✓] Barcode saved for display box:', barcode);
        }
      }
    });
  }

  // Wait until button area is ready
  function waitAndShowBarcode() {
    const barcode = localStorage.getItem(BARCODE_KEY);
    if (!barcode || !/\/0\/[^/]+\/undefined$/.test(location.href)) return;

    const interval = setInterval(() => {
      const ready = document.querySelector('.btn-area.stickey-btnset');
      if (ready) {
        clearInterval(interval);
        insertBarcodeBox(barcode);
      }
    }, 300);
  }

  // Handle SPA navigation
  function observeSPA() {
    const checkUrl = () => {
      if (location.href !== currentUrl) {
        currentUrl = location.href;
        // console.log('[URL] Changed (for barcode box):', currentUrl);
        waitAndShowBarcode();
      }
    };

    ['pushState', 'replaceState'].forEach(type => {
      const original = history[type];
      history[type] = function () {
        const result = original.apply(this, arguments);
        setTimeout(checkUrl, 100);
        return result;
      };
    });

    window.addEventListener('popstate', checkUrl);
    new MutationObserver(checkUrl).observe(document.body, { childList: true, subtree: true });
  }

  // Init all for barcode box
  function initBarcodeBox() {
    watchGridClicksForBarcodeBox();
    observeSPA();
    waitAndShowBarcode();
  }

  window.addEventListener('load', initBarcodeBox);
})();

// IIFE for Dropdown pagination
(function() {
    'use strict';

    // Function to set the dropdown value
    function setDropdownValue() {
        const dropdown = document.getElementById("dropdownPaginationPageSize");
        if (dropdown && dropdown.value !== "100") {
            dropdown.value = "100"; // Set the value to "100"

            // Trigger the 'change' event
            const event = new Event('change', { bubbles: true });
            dropdown.dispatchEvent(event);

            // console.log("Dropdown value set to 100");
        }
    }

    // Function to observe changes in the DOM for the dropdown
    function observeDOMForDropdown() {
        const observer = new MutationObserver(() => {
            setDropdownValue(); // Check and set the dropdown value when changes are detected
        });

        // Observe the entire document for changes
        observer.observe(document.body, {
            childList: true,
            subtree: true,
        });

        // console.log("MutationObserver is active for dropdown");
    }

    // Run the function when the page is fully loaded
    window.addEventListener('load', () => {
        setDropdownValue(); // Initial check
        observeDOMForDropdown(); // Start observing for dynamic changes
    });
})();

// IIFE for Ordered Tests Tooltip
(function() {
    'use strict';

    // Add CSS styles (ensure GM_addStyle is available or manage styles differently if this IIFE is truly isolated)
    // Since GM_addStyle is typically called once, this might need adjustment if it's meant to be fully standalone.
    // For this integration, assuming GM_addStyle in the main IIFE covers all styles.
    // If not, this IIFE would need its own GM_addStyle call or another way to inject CSS.
    // For simplicity, I'll assume the main GM_addStyle is sufficient. If tooltip styles are missing,
    // they would need to be added to the main GM_addStyle block or injected here.
    // The original script had this CSS block here, so I'll keep it, but be mindful of GM_addStyle usage.
    GM_addStyle(`
        #ordered-tests-tooltip {
            position: absolute;
            bottom: -10px;
            left: 0;
            transform: translateY(100%);
            background: white;
            border: 1px solid #e0e0e0;
            border-radius: 6px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10000;
            font-family: 'Segoe UI', Arial, sans-serif;
            display: none;
            pointer-events: none;
            transition: opacity 0.2s ease;
            white-space: nowrap;
            max-width: none;
            overflow: visible;
            min-width: 300px; /* from original */
        }
        .tooltip-header.resulted-header {
            background: #f0fff0;
            border-bottom: 1px solid #d0ffd0;
            color: #2e7d32;
        }
        .test-item.completed {
            color: #2e7d32;
        }
        .test-item.completed .test-icon {
            color: #4caf50;
            font-weight: bold;
            margin-right: 8px;
        }
        .test-item.pending {
            color: #d32f2f;
        }
        .no-tests {
            padding: 8px 0;
            color: #666;
            font-style: italic;
            text-align: center;
        }
        .verify1-btn:hover #ordered-tests-tooltip { /* Corrected selector from original if verify1-btn is the hover target */
            display: block;
            opacity: 1;
        }
        .tooltip-header {
            background: #fff1f1;
            border-bottom: 1px solid #ffd6d6;
            padding: 12px 15px;
            font-weight: 600;
            color: #d32f2f;
            display: flex;
            align-items: center;
            gap: 10px;
            white-space: nowrap;
        }
        .tooltip-content {
            padding: 10px 15px;
            white-space: nowrap;
        }
        .test-item {
            display: flex;
            align-items: center;
            gap: 6px;
            margin: 2px 0;
            font-size: 13px;
        }
        .test-item:last-child {
            border-bottom: none;
        }
        .test-bullet {
            color: #ff5252;
            font-size: 14px;
        }
        .tooltip-footer {
            background: #f9f9f9;
            border-top: 1px solid #eee;
            padding: 8px 15px;
            font-size: 11px;
            color: #757575;
            text-align: right;
            white-space: nowrap;
        }
        .no-ordered-tests { /* This class was in the original CSS block but not used in the HTML structure provided */
            padding: 15px;
            text-align: center;
            color: #666;
            font-style: italic;
            background: #f8faf8;
            white-space: nowrap;
        }
    `);

    const CONFIG = {
        POLL_INTERVAL: 1000,
        MAX_WAIT_TIME: 30000,
        SCAN_INTERVAL: 3000, // This was in original but not obviously used in the provided snippet for this IIFE
        COLUMN_PATTERNS: {
            TEST_DESC: ['desc', 'testdesc', 'name'],
            STATUS: ['status', 'resultstatus', 'state']
        }
    };

    function createTooltip() {
        const tooltip = document.createElement('div');
        tooltip.id = 'ordered-tests-tooltip';
        return tooltip;
    }

    function findCellByPatterns(row, patterns) {
        const cells = row.querySelectorAll('[col-id]');
        for (const cell of cells) {
            const colId = cell.getAttribute('col-id')?.toLowerCase();
            if (patterns.some(pattern => colId?.includes(pattern))) {
                return cell;
            }
        }
        return null;
    }

    // getAllTests was defined in the original script for the tooltip
    function getAllTests() {
        const leftRows = Array.from(document.querySelectorAll('.ag-pinned-left-cols-container .ag-row'));
        const centerRows = Array.from(document.querySelectorAll('.ag-center-cols-container .ag-row'));
        const rightRows = Array.from(document.querySelectorAll('.ag-pinned-right-cols-container .ag-row'));

        const orderedTests = [];
        const resultedTests = [];

        // Assuming leftRows is the primary guide for row count
        for (let i = 0; i < leftRows.length; i++) {
            const leftRow = leftRows[i];
            // Gracefully handle cases where center/right rows might not align perfectly (though they should in AG Grid)
            const centerRow = centerRows[i] || document.createElement('div'); // Dummy div if not found
            const rightRow = rightRows[i] || document.createElement('div');   // Dummy div if not found

            const testDescCell = findCellByPatterns(leftRow, CONFIG.COLUMN_PATTERNS.TEST_DESC);
            const statusCell = findCellByPatterns(rightRow, CONFIG.COLUMN_PATTERNS.STATUS) ||
                             findCellByPatterns(centerRow, CONFIG.COLUMN_PATTERNS.STATUS);

            const testName = testDescCell?.textContent?.trim();
            const status = statusCell?.textContent?.trim().toLowerCase() || '';

            if (testName) {
                if (status.includes('ordered')) {
                    orderedTests.push(testName);
                } else if (status.includes('resulted')) {
                    resultedTests.push(testName);
                }
            }
        }
        return { orderedTests, resultedTests };
    }


    function updateTooltipContent(tooltip) {
        const now = new Date();
        const timeString = now.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
        const dateString = now.toLocaleDateString([], {month: 'short', day: 'numeric'});

        const { orderedTests, resultedTests } = getAllTests();

        const uniqueOrderedTests = [...new Set(orderedTests)].sort((a, b) =>
            a.localeCompare(b, undefined, {sensitivity: 'base'})
        );
        const uniqueResultedTests = [...new Set(resultedTests)].sort((a, b) =>
            a.localeCompare(b, undefined, {sensitivity: 'base'})
        );

        tooltip.innerHTML = `
            <div class="tooltip-header">
                <span>⏳ Pending Tests (${uniqueOrderedTests.length})</span>
            </div>
            <div class="tooltip-content">
                ${uniqueOrderedTests.length > 0 ?
                    uniqueOrderedTests.map(test => `
                        <div class="test-item pending">
                            <span class="test-bullet">•</span>
                            <span>${test}</span>
                        </div>
                    `).join('') : `
                    <div class="no-tests">No pending tests</div>`
                }
            </div>

            <div class="tooltip-header resulted-header">
                <span>✅ Tests ready for verification (${uniqueResultedTests.length})</span>
            </div>
            <div class="tooltip-content">
                ${uniqueResultedTests.length > 0 ?
                    uniqueResultedTests.map(test => `
                        <div class="test-item completed">
                            <span class="test-icon">✓</span>
                            <span>${test}</span>
                        </div>
                    `).join('') : `
                    <div class="no-tests">No resulted tests</div>`
                }
            </div>

            <div class="tooltip-footer">
                Updated: ${dateString} ${timeString}
            </div>
        `;
    }

    function attachTooltipToVerifyButton() {
        const verifyButton = document.querySelector('.verify1-btn');
        if (!verifyButton) return false;

        if (verifyButton.querySelector('#ordered-tests-tooltip')) return true; // Already attached

        const tooltip = createTooltip();
        verifyButton.style.position = 'relative'; // Ensure proper positioning of absolute tooltip
        verifyButton.appendChild(tooltip);

        verifyButton.addEventListener('mouseenter', () => {
            updateTooltipContent(tooltip);
            tooltip.style.display = 'block'; // Show on hover
        });

        verifyButton.addEventListener('mouseleave', () => {
            tooltip.style.display = 'none'; // Hide when not hovering
        });
        return true;
    }

    function waitForButton() {
        const startTime = Date.now();
        const checkInterval = setInterval(() => {
            if (attachTooltipToVerifyButton() || Date.now() - startTime > CONFIG.MAX_WAIT_TIME) {
                clearInterval(checkInterval);
            }
        }, CONFIG.POLL_INTERVAL);
    }

    if (document.readyState === 'complete') {
        waitForButton();
    } else {
        window.addEventListener('load', waitForButton);
    }

    // Watch for SPA navigation to re-attach if button is re-rendered
    const tooltipObserver = new MutationObserver(() => {
        if (!document.querySelector('.verify1-btn #ordered-tests-tooltip')) {
            attachTooltipToVerifyButton();
        }
    });
    tooltipObserver.observe(document.body, { childList: true, subtree: true });
})();

QingJ © 2025

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