// ==UserScript==
// @name Enhanced AG Grid
// @version 13.9.1.0
// @description Adds a unified summary table, improves UI styling, and adds QoL features.
// @match https://his.kaauh.org/lab/*
// @author Hamad AlShegifi
// @grant GM_addStyle
// @namespace http://tampermonkey.net/
// ==/UserScript==
(function () {
'use strict';
// Create a global namespace for inter-IIFE communication
window.enhancedGrid = window.enhancedGrid || {};
// --- Global Constants ---
const GRID_WIDTH = '54vw'; // Target width for main content areas (AG-Grids, static tables, card headers)
const RESIZED_FLAG = 'data-ag-resized'; // Custom attribute to mark AG-Grids as already resized
const INTERVAL = 300; // Interval (ms) for periodic scanning and resizing
const CLICKED_ROW_EXPIRY_PREFIX = 'clicked_row_expiry_'; // Prefix for localStorage keys for row highlights
const CLICK_DURATION_MS = 60 * 1000; // Duration (ms) for row highlight persistence (1 minute)
// --- Utility Functions ---
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// --- Inject CSS Styles ---
GM_addStyle(`
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
/* --- General Layout & Resizing for Main Content Area --- */
.results .ag-theme-balham,
.accd-details,
.accd-details-table-static,
.card-header {
width: ${GRID_WIDTH} !important;
margin-left: auto !important;
margin-right: 0 !important;
box-sizing: border-box;
}
.accd-details table,
.accd-details-table-static table {
width: 100% !important;
table-layout: fixed !important;
border-collapse: collapse !important;
}
.accd-details table th, .accd-details table td,
.accd-details-table-static table th, .accd-details-table-static table td {
padding: 8px !important;
word-wrap: break-word !important;
}
.results .ag-theme-balham {
height: 220px !important;
}
/* --- AG-Grid Row and Cell Specific Styles --- */
.ag-row {
transition: background-color 0.3s ease;
}
.ag-row.clicked-row-green .ag-cell {
background-color: #A0ECA0 !important;
}
`);
// --- AG-Grid Column ID Definitions ---
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;
// --- AG-Grid Row Highlight and Persistence Logic ---
function handleRowClickForPersistentGreen(event) {
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) return;
const barcode = barcodeCell.textContent.trim();
if (!barcode) return;
const expiryTimestamp = Date.now() + CLICK_DURATION_MS;
try { localStorage.setItem(CLICKED_ROW_EXPIRY_PREFIX + barcode, expiryTimestamp.toString()); }
catch (e) { console.error("Error saving to localStorage:", e); }
applyPersistentRowStyles();
}
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();
rows.forEach(row => {
const barcodeCell = row.querySelector('div[col-id="barcode"]');
let rowBarcode = barcodeCell?.textContent?.trim();
if (rowBarcode) {
const expiryKey = CLICKED_ROW_EXPIRY_PREFIX + rowBarcode;
const expiryTimestampStr = localStorage.getItem(expiryKey);
if (expiryTimestampStr) {
if (now < parseInt(expiryTimestampStr, 10)) row.classList.add('clicked-row-green');
else {
localStorage.removeItem(expiryKey);
row.classList.remove('clicked-row-green');
}
} else row.classList.remove('clicked-row-green');
} else row.classList.remove('clicked-row-green');
});
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key?.startsWith(CLICKED_ROW_EXPIRY_PREFIX)) {
if (now >= parseInt(localStorage.getItem(key), 10)) localStorage.removeItem(key);
}
}
} catch (e) { console.error("Error during localStorage cleanup:", e); }
}
function setupPersistentRowStylesListener() {
const gridRoot = document.querySelector('.ag-root-wrapper');
const listenerTarget = gridRoot || document.querySelector('.ag-body-viewport') || document.body;
if (listenerTarget && !listenerTarget.dataset.persistentClickListenerAttached) {
listenerTarget.addEventListener('click', handleRowClickForPersistentGreen, true);
listenerTarget.dataset.persistentClickListenerAttached = 'true';
}
}
// --- AG-Grid Column Visibility Toggle Logic ---
function isSpecificPageForColumns() {
return window.location.href.endsWith('/#/lab-orders/lab-test-analyzer');
}
function areColumnsChecked() {
return columnsToUncheck.some(column => isColumnChecked(column));
}
function isColumnChecked(labelText) {
for (const label of document.querySelectorAll('.ag-column-tool-panel-column-label')) {
if (label.textContent.trim() === labelText && label.parentElement.querySelector('.ag-icon-checkbox-checked')) return true;
}
return false;
}
function ensureColumnsUnchecked() {
if (hasRunOnce || !isSpecificPageForColumns() || !areColumnsChecked()) return;
hasRunOnce = true;
setTimeout(() => columnsToUncheck.forEach(clickColumnLabel), 1000);
}
function ensureOtherColumnsChecked() {
if (!isSpecificPageForColumns()) return;
document.querySelectorAll('.ag-column-tool-panel-column-label').forEach(label => {
if (!columnsToUncheck.includes(label.textContent.trim()) && label.parentElement.querySelector('.ag-icon-checkbox-unchecked')) label.click();
});
}
function clickColumnLabel(labelText) {
if (!isSpecificPageForColumns()) return;
document.querySelectorAll('.ag-column-tool-panel-column-label').forEach(label => {
if (label.textContent.trim() === labelText && label.parentElement.querySelector('.ag-icon-checkbox-checked')) label.click();
});
}
function initColumnToggle() {
if (!isSpecificPageForColumns()) return;
let attempts = 0;
const interval = setInterval(() => {
if (document.querySelector('.ag-side-buttons') || ++attempts > 10) {
if (document.querySelector('.ag-side-buttons')) {
ensureColumnsUnchecked();
ensureOtherColumnsChecked();
}
clearInterval(interval);
}
}, 500);
}
// --- Layout and Main Update Orchestration ---
const performLayoutAdjustments = () => {
document.querySelectorAll('ag-grid-angular').forEach(grid => {
if (grid.getAttribute(RESIZED_FLAG)) return;
const api = grid.agGrid?.api || grid.__agGridComp?.api;
if (api && typeof api.sizeColumnsToFit === 'function') {
grid.style.width = GRID_WIDTH;
grid.style.marginLeft = 'auto';
grid.style.marginRight = '0';
api.sizeColumnsToFit();
grid.setAttribute(RESIZED_FLAG, 'true');
}
});
document.querySelectorAll('div.accd-details-table-static.test-open, .card-header').forEach(el => {
el.style.width = GRID_WIDTH;
el.style.marginLeft = 'auto';
el.style.marginRight = '0';
});
document.querySelectorAll('h6[translateid="test-results.Results"]').forEach(el => el.remove());
};
const fullPageUpdate = debounce(() => {
performLayoutAdjustments();
applyPersistentRowStyles();
setupPersistentRowStylesListener();
if (window.enhancedGrid && typeof window.enhancedGrid.triggerSummaryUpdate === 'function') {
window.enhancedGrid.triggerSummaryUpdate();
}
if (isSpecificPageForColumns()) {
hasRunOnce = false;
initColumnToggle();
}
}, 100);
// --- Main Execution Flow ---
fullPageUpdate();
setInterval(fullPageUpdate, INTERVAL);
// --- SPA Navigation Handling ---
let lastUrl = location.href;
const spaUrlObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
document.querySelectorAll('ag-grid-angular').forEach(grid => grid.removeAttribute(RESIZED_FLAG));
setTimeout(fullPageUpdate, 500);
}
});
spaUrlObserver.observe(document.body, { childList: true, subtree: true });
})();
// IIFE for Barcode display box functionality
(function () {
'use strict';
const BARCODE_KEY = 'selectedBarcode';
let currentUrl = location.href;
function loadJsBarcode(callback) {
if (window.JsBarcode) {
callback();
return;
}
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);
}
function insertBarcodeBox(barcode) {
let insertionTarget = document.querySelector('.btn-area.stickey-btnset') || document.querySelector('.test-open.mt-2');
if (!barcode || !insertionTarget || document.getElementById('barcode-display-box')) return;
const box = document.createElement('div');
box.id = 'barcode-display-box';
box.style.cssText = '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.cssText = 'font-weight:bold;font-size:14px;color:#333;';
const text = document.createElement('div');
text.textContent = barcode;
text.style.cssText = 'font-size: 16px; color: #444; font-weight: bold;';
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.id = "barcode-svg";
svg.style.cssText = 'height:40px;width:120px;border:1px solid #ccc;border-radius:4px;padding:2px;';
box.append(label, text, svg);
insertionTarget.insertAdjacentElement('afterend', box);
try {
const buttonBarStyles = window.getComputedStyle(insertionTarget);
if (buttonBarStyles.position === 'sticky' || buttonBarStyles.position === 'fixed') {
const buttonBarHeight = insertionTarget.offsetHeight;
const buttonBarTop = parseInt(buttonBarStyles.top, 10) || 0;
const desiredTopForBarcode = buttonBarTop + buttonBarHeight + 5;
Object.assign(box.style, {
position: 'sticky',
top: `${desiredTopForBarcode}px`,
zIndex: '99',
boxShadow: '0 3px 5px rgba(0,0,0,0.08)',
});
} else box.style.marginTop = '10px';
} catch (e) {
console.error("Enhanced AG Grid: Error applying sticky style to barcode box.", e);
box.style.marginTop = '10px';
}
loadJsBarcode(() => {
try {
JsBarcode(svg, barcode, { format: "CODE128", displayValue: false, height: 40, width: 2, margin: 0 });
} catch (err) {
console.warn('Barcode render error:', err);
svg.outerHTML = "<span>Invalid Barcode</span>";
}
});
}
function watchGridClicksForBarcodeBox() {
document.body.addEventListener('click', e => {
const cell = e.target.closest('.ag-row')?.querySelector('[col-id="barcode"]');
if (cell?.textContent.trim()) localStorage.setItem(BARCODE_KEY, cell.textContent.trim());
});
}
function waitAndShowBarcode() {
const barcode = localStorage.getItem(BARCODE_KEY);
const urlPattern = /\/0\/.*\/undefined$/;
if (!barcode || !urlPattern.test(location.href)) return;
const interval = setInterval(() => {
const ready = document.querySelector('.btn-area.stickey-btnset') || document.querySelector('.test-open.mt-2');
if (ready) {
clearInterval(interval);
insertBarcodeBox(barcode);
}
}, 300);
}
function observeSPA() {
const bodyObserver = new MutationObserver(() => {
if (location.href !== currentUrl) {
currentUrl = location.href;
waitAndShowBarcode();
}
});
bodyObserver.observe(document.body, { childList: true, subtree: true });
}
watchGridClicksForBarcodeBox();
observeSPA();
waitAndShowBarcode();
})();
// IIFE for Dropdown pagination
(function() {
'use strict';
const setDropdownValue = () => {
const dropdown = document.getElementById("dropdownPaginationPageSize");
if (dropdown && dropdown.value !== "100") {
dropdown.value = "100";
dropdown.dispatchEvent(new Event('change', { bubbles: true }));
}
};
new MutationObserver(setDropdownValue).observe(document.body, { childList: true, subtree: true });
window.addEventListener('load', setDropdownValue);
})();
// IIFE for Unified Test Summary Table functionality
(function() {
'use strict';
GM_addStyle(`
#test-summary-container {
width: 40%; float: left; margin: 25px 10px 10px 0;
background-color: #fff; border: 1px solid #ddd; border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08); display: flex; flex-direction: column;
}
.card-body::after { content: ""; display: table; clear: both; }
#test-summary-container .summary-content { padding: 0; position: relative; flex-grow: 1; }
.summary-header {
font-weight: 500; font-size: 12px; padding: 8px 10px;
border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; gap: 10px;
color: #000000; cursor: pointer; user-select: none; flex-shrink: 0;
}
.summary-header > span {
flex-grow: 1; /* Allow text to take available space */
}
.summary-status-icon {
margin-left: auto; /* Push icon to the far right */
margin-right: 5px;
font-size: 1.2em; /* Make icon slightly larger */
}
/* --- Custom Icon Colors for Summary Statuses --- */
.summary-header i.vicon.fa-check-circle { color: #28a745; }
.summary-header i.vicon.fa-star { color: #6c757d; }
.summary-header i.vicon.fa-arrow-circle-up.ord { color: #17a2b8; }
.summary-header i.vicon.fa-arrow-circle-up:not(.ord) { color: #007bff; }
.collapse-icon {
transition: color 0.2s ease-in-out;
color: #28a745;
}
.collapse-icon.collapsed {
color: #dc3545;
}
.no-tests { padding: 20px; color: #6c757d; text-align: center; }
.hidden-note { color: #dc3545; font-style: italic; }
.test-list-container { display: contents; }
.hidden-note-container { display: none; }
#test-summary-container .summary-content.collapsed { display: none; }
#test-summary-container .summary-content.collapsed .test-list-container { display: none; }
#test-summary-container .summary-content.collapsed .hidden-note-container { display: block; }
.summary-grid-container {
display: grid;
grid-template-columns: 1fr minmax(80px, auto) minmax(80px, auto) min-content min-content;
align-items: start;
}
.summary-grid-header { display: contents; font-weight: bold; }
.summary-grid-header > div {
background-color: #f0f0f0; padding: 6px 15px; border-bottom: 1px solid #e0e0e0;
white-space: nowrap; color: #555; font-size: 12px;
position: sticky; top: 0; z-index: 1;
}
.summary-grid-header > div.test-name-header { text-align: left; }
.summary-grid-header > div.test-value-header { text-align: right; }
.summary-grid-header > div.range-header { text-align: left; }
.summary-grid-header > div.uom-header { text-align: left; }
.summary-grid-header > div.flag-header { text-align: center; }
.test-item { display: contents; }
.grid-cell {
padding: 4px 15px; border-bottom: 1px solid #f0f0f0; font-size: 13px;
display: flex; align-items: center; min-height: 28px;
overflow: hidden; text-overflow: ellipsis;
}
.test-item:last-of-type > .grid-cell { border-bottom: none; }
.grid-cell.test-details { gap: 8px; }
.grid-cell.pending-value { justify-content: right; color: #999; }
.test-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.test-icon { color: #28a745; }
.test-bullet { color: #dc3545; }
.test-value {
font-weight: bold; color: #000000; border-radius: 4px;
font-size: 13px; text-align: right; padding: 2px 4px;
justify-content: flex-end; width: 100%;
}
.test-value.highlight-H { background-color: #ffe0e0 !important; }
.test-value.highlight-L { background-color: #e0f2f7 !important; }
.test-uom { font-size: 12px; color: #555; }
.grid-cell.flag-cell { justify-content: center; }
.flag {
font-weight: bold; border-radius: 50%; width: 16px; height: 16px;
display: inline-flex; align-items: center; justify-content: center;
font-size: 11px; color: white; flex-shrink: 0;
}
.flag-placeholder { width: 16px; height: 16px; visibility: hidden; flex-shrink: 0; }
.flag-H { background-color: #d32f2f !important; }
.flag-L { background-color: #1976d2 !important; }
.summary-footer {
display: flex; justify-content: space-between; align-items: center; background: #f8f9fa;
border-top: 1px solid #e0e0e0; padding: 10px 15px; font-size: 12px; color: #6c757d;
flex-shrink: 0;
}
`);
const CONFIG = {
COLUMN_PATTERNS: {
TEST_DESC: ['testdesc', 'testdescription', 'name'],
STATUS: ['resultstatus', 'teststatus', 'status', 'state'],
RESULT_VALUE: ['testresult'],
UOM: ['uom', 'uomvalue'],
REFERENCE_RANGE: ['referencerange', 'range']
},
STATUS_PROPERTIES: {
resulted: { color: '#ffca77', className: 'fa fa-check-circle vicon' },
ordered: { color: '#E0E0E0', className: 'fa fa-star vicon' },
verifiedlevel1: { color: '#90EE90', className: 'fa fa-arrow-circle-up ord vicon' },
verifiedlevel2: { color: '#28a745', className: 'fa fa-arrow-circle-up vicon' },
default: { color: '#E0E0E0', className: null }
}
};
const collapseState = {};
// Defines the desired sort order for tests within a summary group.
const testSortOrder = [
// User Rule: Na -> K (Electrolytes)
'sodium',
'potassium',
'chloride',
// Kidney Function
'urea',
'creatinine',
// User Rule: Lipid Panel
'cholesterol',
'triglycerides',
'high-density lipoprotein',
'low-density lipoprotein',
// Liver Function
'alkaline phosphatase',
'alanine amino transferase',
'aspartate amino transferase',
'gamma-glutamyl transferase',
// User Rule: TBIL -> DBIL
'bilirubin - total',
'bilirubin - direct',
'total protein',
'albumin',
// Minerals
'calcium',
'phosphate'
];
/**
* Returns a numeric index for a given test name based on the testSortOrder array.
* Lower numbers have higher sort priority. Tests not in the list get a high number (Infinity).
* @param {string} testName - The name of the test.
* @returns {number} The sort index.
*/
function getSortIndex(testName) {
const lowerTestName = testName.toLowerCase();
const index = testSortOrder.findIndex(sortedName => lowerTestName.includes(sortedName));
return index === -1 ? Infinity : index;
}
const findCellByPatterns = (row, patterns) => {
for (const cell of row.querySelectorAll('[col-id]')) {
const colId = cell.getAttribute('col-id')?.toLowerCase();
if (colId && patterns.includes(colId)) return cell;
}
return null;
};
const parseRangeAndCompare = (rangeStr, value) => {
if (!rangeStr || value === null || isNaN(parseFloat(value))) return null;
const numVal = parseFloat(value);
const stdRange = rangeStr.match(/(\d+\.?\d*)\s*-\s*(\d+\.?\d*)/);
if (stdRange) {
const lower = parseFloat(stdRange[1]);
const upper = parseFloat(stdRange[2]);
if (numVal < lower) return 'L';
if (numVal > upper) return 'H';
}
const ltRange = rangeStr.match(/(?:<|Less than)\s*(\d+\.?\d*)/i);
if (ltRange) { if (numVal >= parseFloat(ltRange[1])) return 'H'; }
const gtRange = rangeStr.match(/(?:>|Greater than)\s*(\d+\.?\d*)/i);
if (gtRange) { if (numVal <= parseFloat(gtRange[1])) return 'L'; }
return null;
};
const capitalize = (s) => {
if (typeof s !== 'string' || s.length === 0) return '';
const spaced = s.replace(/([A-Z])|([0-9]+)/g, ' $1$2').trim();
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
};
const getNormalizedStatusKey = (status) => {
const normalized = status.toLowerCase().replace(/\s+/g, '');
if (normalized.includes('verified') && normalized.includes('1')) return 'verifiedlevel1';
if (normalized.includes('verified') && normalized.includes('2')) return 'verifiedlevel2';
if (normalized === 'resulted') return 'resulted';
if (normalized === 'ordered') return 'ordered';
return 'default';
};
function getAllTests() {
const testsByStatus = {};
const leftRows = document.querySelectorAll('.ag-pinned-left-cols-container .ag-row');
const centerRows = document.querySelectorAll('.ag-center-cols-container .ag-row');
const rightRows = document.querySelectorAll('.ag-pinned-right-cols-container .ag-row');
const combinedRows = [];
for (let i = 0; i < centerRows.length; i++) {
const combined = document.createElement('div');
[leftRows[i], centerRows[i], rightRows[i]].forEach(p => {
if (p) p.querySelectorAll('[col-id]').forEach(c => combined.appendChild(c.cloneNode(true)));
});
combinedRows.push(combined);
}
const uniqueTests = new Map();
for (const combined of combinedRows) {
const nameCell = findCellByPatterns(combined, CONFIG.COLUMN_PATTERNS.TEST_DESC);
const testName = nameCell?.textContent?.trim();
if (!testName) continue;
const statusCell = findCellByPatterns(combined, CONFIG.COLUMN_PATTERNS.STATUS);
const status = statusCell?.textContent?.trim().toLowerCase() || 'unknown';
let data = { name: testName, status: status, value: null, uom: null, range: null, flag: null };
const valCell = findCellByPatterns(combined, CONFIG.COLUMN_PATTERNS.RESULT_VALUE);
const rangeCell = findCellByPatterns(combined, CONFIG.COLUMN_PATTERNS.REFERENCE_RANGE);
const uomCell = findCellByPatterns(combined, CONFIG.COLUMN_PATTERNS.UOM);
data.value = valCell?.textContent?.trim().replace('-', '') || null;
data.range = rangeCell?.textContent?.trim().replace('-', '') || null;
data.uom = uomCell?.textContent?.trim().replace('-', '') || null;
data.flag = parseRangeAndCompare(data.range, data.value);
uniqueTests.set(testName, data);
}
uniqueTests.forEach(data => {
if (!testsByStatus[data.status]) testsByStatus[data.status] = [];
testsByStatus[data.status].push(data);
});
return testsByStatus;
}
const renderList = (list) => {
if (list.length === 0) return `<div class="no-tests" style="grid-column: 1 / -1;">No tests in this category.</div>`;
const headerHtml = `
<div class="summary-grid-header">
<div class="test-name-header">Test Name</div>
<div class="test-value-header">Result</div>
<div class="range-header">Ref Range</div>
<div class="uom-header">UOM</div>
<div class="flag-header">H/L</div>
</div>`;
const itemsHtml = list.map(t => {
const iconHtml = t.status.includes('result') ? `<span class="test-icon">✓</span>` : `<span class="test-bullet">●</span>`;
const resultCellHtml = (t.value !== null && t.value !== undefined) ? `<div class="grid-cell test-value ${t.flag ? 'highlight-' + t.flag : ''}">${t.value}</div>` : `<div class="grid-cell pending-value">${capitalize(t.status)}</div>`;
return `
<div class="test-item" title="${t.name} (${capitalize(t.status)})">
<div class="grid-cell test-details">${iconHtml}<span class="test-name">${t.name}</span></div>
${resultCellHtml}
<div class="grid-cell test-range">${t.range || '-'}</div>
<div class="grid-cell test-uom">${t.uom || '-'}</div>
<div class="grid-cell flag-cell"><span class="flag ${t.flag ? 'flag-' + t.flag : 'flag-placeholder'}">${t.flag || ''}</span></div>
</div>`;
}).join('');
return headerHtml + itemsHtml;
};
function updateSummaryContent(containerEl) {
if (!containerEl) return;
const testsByStatus = getAllTests();
// ** FIX: Corrected the group sort order **
const statusOrder = ['resulted', 'ordered', 'verifiedlevel1', 'verifiedlevel2'];
const sortedStatuses = Object.keys(testsByStatus).sort((a, b) => {
const keyA = getNormalizedStatusKey(a);
const keyB = getNormalizedStatusKey(b);
const indexA = statusOrder.indexOf(keyA);
const indexB = statusOrder.indexOf(keyB);
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
let contentHtml = '';
let totalTests = 0;
if (sortedStatuses.length === 0) {
contentHtml = `<div class="no-tests">No tests found on this page.</div>`;
} else {
sortedStatuses.forEach(status => {
const testList = testsByStatus[status];
if (testList.length === 0) return;
// Sort the test list based on the custom order
testList.sort((a, b) => {
const indexA = getSortIndex(a.name);
const indexB = getSortIndex(b.name);
// If both tests are in the custom sort list, sort by that order
if (indexA !== Infinity || indexB !== Infinity) {
return indexA - indexB;
}
// Otherwise, fall back to alphabetical sorting
return a.name.localeCompare(b.name);
});
totalTests += testList.length;
collapseState[status] = collapseState[status] ?? false;
const isCollapsed = collapseState[status];
const collapseIconClass = isCollapsed ? 'fa-arrow-circle-right' : 'fa-arrow-circle-down';
const statusDisplayName = capitalize(status);
const normalizedKey = getNormalizedStatusKey(status);
const properties = CONFIG.STATUS_PROPERTIES[normalizedKey] || CONFIG.STATUS_PROPERTIES.default;
const headerColor = properties.color;
const statusIconHtml = properties.className ? `<i class="${properties.className} summary-status-icon"></i>` : '';
contentHtml += `
<div class="summary-header" data-section="${status}" style="background-color: ${headerColor};">
<i class="fas ${collapseIconClass} collapse-icon ${isCollapsed ? 'collapsed' : ''}"></i>
<span>${statusDisplayName} (${testList.length})</span>
${statusIconHtml}
</div>
<div id="${status.replace(/\s+/g, '-')}-tests-content" class="summary-content ${isCollapsed ? 'collapsed' : ''}">
<div class="summary-grid-container test-list-container">${renderList(testList)}</div>
<div class="hidden-note-container">
${testList.length > 0 ? `<div class="no-tests hidden-note">${testList.length} ${statusDisplayName} tests hidden</div>` : ''}
</div>
</div>`;
});
}
const now = new Date();
containerEl.innerHTML = contentHtml + `
<div class="summary-footer">
<span>Total Tests: ${totalTests}</span>
<span>Updated: ${now.toLocaleDateString([], {month: 'short', day: 'numeric'})} ${now.toLocaleTimeString([], { hour: '2-digit', minute:'2-digit' })}</span>
</div>`;
}
const summaryContainer = document.createElement('div');
summaryContainer.id = 'test-summary-container';
summaryContainer.addEventListener('click', e => {
const header = e.target.closest('.summary-header');
if (!header) return;
const section = header.dataset.section;
if (section) {
collapseState[section] = !collapseState[section];
const content = document.getElementById(`${section.replace(/\s+/g, '-')}-tests-content`);
const icon = header.querySelector('.collapse-icon');
if (content && icon) {
content.classList.toggle('collapsed', collapseState[section]);
icon.classList.toggle('collapsed', collapseState[section]);
icon.classList.toggle('fa-arrow-circle-down', !collapseState[section]);
icon.classList.toggle('fa-arrow-circle-right', collapseState[section]);
}
}
});
let insertionIntervalId = null;
const isTargetPageForSummary = () => /\/0\/.*\/undefined$/.test(window.location.href);
const removeSummaryContainer = () => {
const container = document.getElementById(summaryContainer.id);
if (container) {
Object.assign(container.style, { position: '', top: '', zIndex: '' });
container.remove();
}
};
const attemptInsertion = () => {
let targetElement = document.getElementById('barcode-display-box') || document.querySelector('.test-open.mt-2');
if (!targetElement || document.getElementById(summaryContainer.id)) return false;
targetElement.insertAdjacentElement('afterend', summaryContainer);
try {
const barcodeBox = document.getElementById('barcode-display-box');
if (barcodeBox) {
const barcodeBoxStyles = window.getComputedStyle(barcodeBox);
if (barcodeBoxStyles.position === 'sticky') {
const barcodeBoxHeight = barcodeBox.offsetHeight;
const barcodeBoxTop = parseInt(barcodeBoxStyles.top, 10) || 0;
const summaryTopPosition = barcodeBoxTop + barcodeBoxHeight + 10;
Object.assign(summaryContainer.style, { position: 'sticky', top: `${summaryTopPosition}px`, zIndex: '98' });
}
}
} catch(e) { console.error("Enhanced AG Grid: Error applying sticky style to summary container.", e); }
return true;
};
const startInsertionPolling = () => {
if (insertionIntervalId !== null) return;
insertionIntervalId = setInterval(() => {
if (attemptInsertion()) {
clearInterval(insertionIntervalId);
insertionIntervalId = null;
if (window.enhancedGrid && window.enhancedGrid.triggerSummaryUpdate) window.enhancedGrid.triggerSummaryUpdate();
}
}, 500);
};
const stopInsertionPolling = () => {
if (insertionIntervalId !== null) {
clearInterval(insertionIntervalId);
insertionIntervalId = null;
}
};
const manageSummaryTableLifecycle = () => {
if (isTargetPageForSummary()) startInsertionPolling();
else {
stopInsertionPolling();
removeSummaryContainer();
}
};
window.enhancedGrid.triggerSummaryUpdate = () => {
const container = document.getElementById(summaryContainer.id);
if (container && isTargetPageForSummary()) updateSummaryContent(container);
};
const observeSpaUrlChangesForSummary = () => {
let lastHandledUrl = location.href;
const checkUrlChange = () => {
if (location.href !== lastHandledUrl) {
lastHandledUrl = location.href;
manageSummaryTableLifecycle();
}
};
const bodyObserver = new MutationObserver(checkUrlChange);
bodyObserver.observe(document.body, { childList: true, subtree: true });
};
observeSpaUrlChangesForSummary();
manageSummaryTableLifecycle();
})();