// ==UserScript==
// @name Pending barcodes
// @namespace http://tampermonkey.net/
// @version 1.9.5
// @description Collects barcodes automatically, provides a viewing interface, and clicks the filtered result.
// @author Hamad AlShegifi
// @match *://his.kaauh.org/lab/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
#barcode-inpage-container {
width: 30vw !important;
float: left !important;
}
.ag-root-wrapper-body {
width: 70vw !important;
margin-left: auto !important;
margin-right: 0 !important;
}
`);
})();
(function() {
'use strict';
// --- Configuration ---
const TABLE_BODY_SELECTOR = 'tbody[formarrayname="TubeTypeList"]';
const BARCODE_DISPLAY_SELECTOR = '#barcode-display-box';
const STORAGE_KEY = 'collectedBarcodes_storage';
const IN_PAGE_TABLE_ID = 'barcode-inpage-container';
const INJECTION_POINT_SELECTOR = '.row.labordertab';
const GRID_CONTAINER_SELECTOR = '.ag-center-cols-container'; // Selector for the AG-Grid row container
// --- State Flags & Cache ---
const collectedBarcodesThisSession = new Set();
let lastCheckedPatientBarcode = null;
let timeSinceInterval = null;
let observerDebounceTimer = null;
let isTableUpdating = false;
// --- Main Logic ---
function initialize() {
console.log("Barcode Collector: Script started. Observing for page changes...");
const observer = new MutationObserver((mutations, obs) => {
if (observerDebounceTimer) clearTimeout(observerDebounceTimer);
observerDebounceTimer = setTimeout(() => {
const injectionPoint = document.querySelector(INJECTION_POINT_SELECTOR);
if (injectionPoint) {
updateOrInsertBarcodeTable();
} else {
const existingTable = document.getElementById(IN_PAGE_TABLE_ID);
if (existingTable) {
if (timeSinceInterval) clearInterval(timeSinceInterval);
existingTable.remove();
}
}
const patientBarcodeBox = document.querySelector(BARCODE_DISPLAY_SELECTOR);
const barcodeOnPage = patientBarcodeBox ?
Array.from(patientBarcodeBox.querySelectorAll('div')).find(div => div.textContent.includes('Sample Barcode:'))?.nextElementSibling?.textContent.trim() :
null;
if (barcodeOnPage) {
if (barcodeOnPage !== lastCheckedPatientBarcode) {
console.log(`Barcode Collector: Detected patient barcode on page: ${barcodeOnPage}`);
lastCheckedPatientBarcode = barcodeOnPage;
markBarcodeAsFoundAndUpdateStorage(barcodeOnPage);
}
} else {
if (lastCheckedPatientBarcode !== null) {
lastCheckedPatientBarcode = null;
}
}
const allBarcodeInputs = document.querySelectorAll(`${TABLE_BODY_SELECTOR} input[formcontrolname="Barcode"]`);
if (allBarcodeInputs.length > 0) {
(async () => {
for (const input of allBarcodeInputs) {
if (input.value) {
await saveBarcode(input.value.trim());
}
}
})();
}
}, 250);
});
observer.observe(document.body, { childList: true, subtree: true });
}
async function saveBarcode(barcode) {
if (collectedBarcodesThisSession.has(barcode)) return;
let barcodes = await GM_getValue(STORAGE_KEY, []);
if (barcodes.some(entry => entry.barcode === barcode)) {
collectedBarcodesThisSession.add(barcode);
return;
}
const newEntry = {
count: barcodes.length + 1,
barcode: barcode,
timestamp: new Date().toISOString(),
found: false
};
barcodes.push(newEntry);
await GM_setValue(STORAGE_KEY, barcodes);
collectedBarcodesThisSession.add(barcode);
console.log(`Barcode Collector: Saved new barcode - ${barcode}`);
updateOrInsertBarcodeTable();
}
function formatTimeSince(isoTimestamp) {
const date = new Date(isoTimestamp);
const now = new Date();
const totalMinutes = Math.floor((now - date) / (1000 * 60));
if (totalMinutes < 1) return "00:00 ago";
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const formattedHours = String(hours).padStart(2, '0');
const formattedMinutes = String(minutes).padStart(2, '0');
return `${formattedHours}:${formattedMinutes} ago`;
}
async function markBarcodeAsFoundAndUpdateStorage(barcodeToMark) {
let barcodes = await GM_getValue(STORAGE_KEY, []);
const entry = barcodes.find(b => b.barcode === barcodeToMark);
if (entry && !entry.found) {
entry.found = true;
await GM_setValue(STORAGE_KEY, barcodes);
console.log(`Barcode Collector: Marked barcode ${barcodeToMark} as found and saved status.`);
}
updateViewWithHighlight(barcodeToMark);
}
function updateViewWithHighlight(barcode) {
const table = document.getElementById(IN_PAGE_TABLE_ID);
if (!table) return;
const row = table.querySelector(`tr[data-barcode-row="${barcode}"]`);
if (row) {
row.classList.add('barcode-found');
}
}
function findFloatingFilterInputByHeader(headerText) {
const headerViewport = document.querySelector('.ag-header-viewport');
if (!headerViewport) return null;
const allTitleCells = Array.from(headerViewport.querySelectorAll('.ag-header-row[aria-rowindex="1"] .ag-header-cell'));
if (allTitleCells.length === 0) return null;
let targetColumnIndex = -1;
allTitleCells.forEach((cell, index) => {
const cellTextElement = cell.querySelector('.ag-header-cell-text');
if (cellTextElement && cellTextElement.textContent.trim().toLowerCase() === headerText.toLowerCase()) {
targetColumnIndex = index;
}
});
if (targetColumnIndex === -1) return null;
const filterRow = headerViewport.querySelector('.ag-header-row[aria-rowindex="2"]');
if (!filterRow) return null;
const filterCell = filterRow.children[targetColumnIndex];
if (!filterCell) return null;
return filterCell.querySelector('input.ag-floating-filter-input');
}
function waitForGridUpdateAndClick() {
return new Promise((resolve, reject) => {
const gridContainer = document.querySelector(GRID_CONTAINER_SELECTOR);
if (!gridContainer) {
return reject("AG-Grid container not found.");
}
const timeout = setTimeout(() => {
observer.disconnect();
reject("Timeout: AG-Grid did not update within 2 seconds.");
}, 2000);
const observer = new MutationObserver((mutations, obs) => {
const firstRow = gridContainer.querySelector('.ag-row[row-index="0"]');
if (firstRow) {
firstRow.click();
clearTimeout(timeout);
obs.disconnect();
resolve();
}
});
observer.observe(gridContainer, { childList: true, subtree: true });
});
}
// --- NEW: Helper function for creating delays ---
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// --- MODIFIED: Added delays and error handling for more reliable interaction ---
async function enterBarcodeInFilter(barcode) {
const targetInput = findFloatingFilterInputByHeader('Barcode');
if (!targetInput) {
console.error('Barcode Collector: Could not find the "Barcode" filter input field on the page.');
return;
}
try {
console.log(`Barcode Collector: Attempting to filter for barcode "${barcode}"...`);
// Step 1: Focus the input
targetInput.focus();
await sleep(50); // Small delay to ensure the website registers the focus event
// Step 2: Set the value and dispatch an 'input' event to trigger the framework's change detection
targetInput.value = barcode;
targetInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
await sleep(100); // A slightly longer delay to allow any debouncing logic on the site to catch up
// Step 3: Dispatch Enter key events to confirm the filter action
targetInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }));
targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }));
console.log(`Barcode Collector: Programmatically entered barcode "${barcode}" into the filter.`);
// Step 4: Wait for the grid to update with the filtered results and then click the first row
console.log("Barcode Collector: Waiting for AG-Grid to update...");
await waitForGridUpdateAndClick();
console.log("Barcode Collector: Successfully clicked the first grid row.");
} catch (error) {
console.error("Barcode Collector: An error occurred while filtering or clicking the grid row.", error);
} finally {
// Step 5: After all actions, remove focus from the input field
if (targetInput) {
targetInput.blur();
}
}
}
async function updateOrInsertBarcodeTable() {
if (isTableUpdating) return;
isTableUpdating = true;
try {
const injectionPoint = document.querySelector(INJECTION_POINT_SELECTOR);
if (!injectionPoint) {
isTableUpdating = false;
return;
}
let container = document.getElementById(IN_PAGE_TABLE_ID);
if (!container) {
container = document.createElement('div');
container.id = IN_PAGE_TABLE_ID;
container.innerHTML = `
<div class="bc-table-header">
<h2>Pending Barcodes</h2>
<button id="clear-barcodes-btn-inline" class="bc-clear-btn">Clear All</button>
</div>
<div class="bc-table-body">
<table>
<thead>
<tr>
<th>#</th>
<th>Barcode</th>
<th>Time Added</th>
<th>Time Since Received</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>`;
injectionPoint.parentNode.insertBefore(container, injectionPoint.nextSibling);
container.querySelector('#clear-barcodes-btn-inline').addEventListener('click', async () => {
if (confirm("Are you sure you want to delete all pending barcodes? This cannot be undone.")) {
await GM_setValue(STORAGE_KEY, []);
updateOrInsertBarcodeTable();
}
});
}
const barcodes = await GM_getValue(STORAGE_KEY, []);
barcodes.sort((a, b) => a.count - b.count);
let tableRows = barcodes.map(entry => `
<tr data-barcode-row="${entry.barcode}" class="${entry.found ? 'barcode-found' : ''}">
<td>${entry.count}</td>
<td>${entry.barcode}</td>
<td>${new Date(entry.timestamp).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })}</td>
<td data-timestamp="${entry.timestamp}">${formatTimeSince(entry.timestamp)}</td>
<td class="action-cell-bc"><span class="delete-barcode-btn" data-barcode="${entry.barcode}" title="Delete">×</span></td>
</tr>
`).join('');
if (barcodes.length === 0) {
tableRows = '<tr><td colspan="5">No pending barcodes.</td></tr>';
}
const tableBody = container.querySelector('tbody');
if (tableBody.innerHTML !== tableRows) {
tableBody.innerHTML = tableRows;
}
tableBody.removeEventListener('click', handleTableClick);
tableBody.addEventListener('click', handleTableClick);
if (timeSinceInterval) clearInterval(timeSinceInterval);
timeSinceInterval = setInterval(() => {
container.querySelectorAll('td[data-timestamp]').forEach(cell => {
cell.textContent = formatTimeSince(cell.dataset.timestamp);
});
}, 5000);
} finally {
isTableUpdating = false;
}
}
async function handleTableClick(event) {
const row = event.target.closest('tr');
if (!row || !row.dataset.barcodeRow) return;
if (event.target.classList.contains('delete-barcode-btn')) {
const barcodeToDelete = event.target.dataset.barcode;
await deleteBarcode(barcodeToDelete);
} else {
const barcodeToEnter = row.dataset.barcodeRow;
await enterBarcodeInFilter(barcodeToEnter);
}
}
async function deleteBarcode(barcodeToDelete) {
let barcodes = await GM_getValue(STORAGE_KEY, []);
let updatedBarcodes = barcodes.filter(entry => entry.barcode !== barcodeToDelete);
updatedBarcodes.forEach((entry, index) => {
entry.count = index + 1;
});
await GM_setValue(STORAGE_KEY, updatedBarcodes);
console.log(`Barcode Collector: Deleted barcode - ${barcodeToDelete}`);
updateOrInsertBarcodeTable();
}
GM_addStyle(`
#${IN_PAGE_TABLE_ID} {
margin: 15px 0; border: 1px solid #ccc; border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1); font-family: Arial, sans-serif;
background-color: #fff; display: flex; flex-direction: column; height: 90vh;
}
.bc-table-header {
padding: 10px 16px; background-color: #f7f7f7; border-bottom: 1px solid #ccc;
border-top-left-radius: 8px; border-top-right-radius: 8px;
display: flex; justify-content: space-between; align-items: center;
}
.bc-table-header h2 { margin: 0; font-size: 1.1em; color: #333; }
.bc-clear-btn {
background-color: #ef5350; color: white; border: none; padding: 6px 12px;
border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 0.9em;
}
.bc-clear-btn:hover { background-color: #d32f2f; }
.bc-table-body {
padding: 8px; overflow-y: auto; flex-grow: 1; min-height: 0;
}
.bc-table-body table { width: 100%; border-collapse: collapse; }
.bc-table-body th, .bc-table-body td {
border: 1px solid #ddd; padding: 2px 8px; text-align: left; font-size: 0.9em;
}
.bc-table-body th { background-color: #f2f2f2; }
.bc-table-body tbody tr { cursor: pointer; }
.bc-table-body tbody tr:hover { background-color: #e8eaf6; }
.bc-table-body tbody tr.barcode-found { background-color: #50ba58 !important; }
.bc-table-body tbody tr.barcode-found:hover { background-color: #50ba58 !important; }
.action-cell-bc { text-align: center !important; }
.delete-barcode-btn {
cursor: pointer; font-weight: bold; font-size: 18px; color: #ef5350;
padding: 0 4px; border-radius: 4px;
}
.delete-barcode-btn:hover { color: white; background-color: #d32f2f; }
`);
initialize();
})();