// ==UserScript==
// @name Enhanced AG Grid Functionality
// @version 4.3
// @description Adds vibrant row hover effect (overriding all cell colors), conditional status cell styling for test status and sample status, and static red background for emergency rows in AG Grid.
// @match https://his.kaauh.org/lab/*
// @author Hamad AlShegifi
// @grant GM_addStyle
// @namespace http://tampermonkey.net/
// ==/UserScript==
(function () {
'use strict';
// Inject CSS for hover effect, status cell styling, and emergency highlighting
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 */
}
`);
// 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
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(); // Call highlightEmergencyRows on every DOM change
if (isSpecificPage()) {
hasRunOnce = false; // Reset run flag for repeated executions
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. Falling back to body observer.");
observer.observe(document.body, { childList: true, subtree: true });
}
window.addEventListener('load', () => {
applyHoverEffect();
applyStatusCellStyle();
if (isSpecificPage()) {
initColumnToggle();
}
});
// Function to set the dropdown value (keeping this separate as it seems independent)
(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
});
})();
})();
(function() {
'use strict';
// Add CSS styles
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;
}
#ordered-tests-tooltip {
/* existing styles */
min-width: 300px;
}
.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 {
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 {
padding: 15px;
text-align: center;
color: #666;
font-style: italic;
background: #f8faf8;
white-space: nowrap;
}
`);
// Configuration
const CONFIG = {
POLL_INTERVAL: 1000,
MAX_WAIT_TIME: 30000,
SCAN_INTERVAL: 3000,
COLUMN_PATTERNS: {
TEST_DESC: ['desc', 'testdesc', 'name'],
STATUS: ['status', 'resultstatus', 'state']
}
};
// Create the tooltip element
function createTooltip() {
const tooltip = document.createElement('div');
tooltip.id = 'ordered-tests-tooltip';
return tooltip;
}
// Find cells by column patterns
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;
}
// Get all tests with "Ordered" status
function getOrderedTests() {
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 = [];
for (let i = 0; i < leftRows.length; i++) {
const leftRow = leftRows[i];
const centerRow = centerRows[i] || {};
const rightRow = rightRows[i] || {};
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 && status.includes('ordered')) {
orderedTests.push(testName);
}
}
return orderedTests;
}
// Update tooltip content with ordered tests (sorted alphabetically)
// Update the tooltip content generation function
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'});
// Get all tests and categorize them
const { orderedTests, resultedTests } = getAllTests();
// Create a Set to remove duplicates, then sort alphabetically (case-insensitive)
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 to get all tests and categorize them
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 = [];
for (let i = 0; i < leftRows.length; i++) {
const leftRow = leftRows[i];
const centerRow = centerRows[i] || {};
const rightRow = rightRows[i] || {};
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 };
}
// Main function to attach tooltip to VERIFY1 button
function attachTooltipToVerifyButton() {
const verifyButton = document.querySelector('.verify1-btn');
if (!verifyButton) return false;
if (verifyButton.querySelector('#ordered-tests-tooltip')) return true;
const tooltip = createTooltip();
verifyButton.style.position = 'relative';
verifyButton.appendChild(tooltip);
verifyButton.addEventListener('mouseenter', () => {
updateTooltipContent(tooltip);
tooltip.style.display = 'block';
});
verifyButton.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
});
return true;
}
// Wait for button to appear
function waitForButton() {
const startTime = Date.now();
const checkInterval = setInterval(() => {
if (attachTooltipToVerifyButton() || Date.now() - startTime > CONFIG.MAX_WAIT_TIME) {
clearInterval(checkInterval);
}
}, CONFIG.POLL_INTERVAL);
}
// Start the script
if (document.readyState === 'complete') {
waitForButton();
} else {
window.addEventListener('load', waitForButton);
}
// Watch for SPA navigation
const observer = new MutationObserver(() => {
if (!document.querySelector('.verify1-btn #ordered-tests-tooltip')) {
attachTooltipToVerifyButton();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();