// ==UserScript==
// @name Rapid Receiver & Lab Counter Suite
// @namespace Violentmonkey Scripts
// @version 5.0
// @description Combines Rapid Barcode Receiver with Sample Counter, Lab Filter, and Alert Relocation.
// @match https://his.kaauh.org/lab/*
// @author Gemini & Hamad AlShegifi
// @grant GM_addStyle
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_PREFIX = "[LAB SUITE V5.0]";
const logDebug = msg => console.debug(`${SCRIPT_PREFIX} ${msg}`);
const logError = msg => console.error(`${SCRIPT_PREFIX} ${msg}`);
// --- Injected CSS from both scripts ---
GM_addStyle(`
/* --- Styles from Counter Script --- */
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
@keyframes flash-red { 0%, 100% { background-color: #e74c3c; border-color: #c0392b; color: #fff; } 50% { background-color: #f1f3f5; border-color: #dee2e6; color: #5d6873; } }
.counter-icon { display: inline-block; width: 12px; height: 12px; border: 1.5px solid currentColor; border-top: none; border-radius: 0 0 4px 4px; position: relative; vertical-align: -2px; }
.counter-icon::before { content: ''; position: absolute; top: -2px; left: -2px; width: 14px; height: 2.5px; background-color: currentColor; border-radius: 1px; }
.main-counter-style { display: inline-flex; align-items: center; gap: 10px; animation: fadeIn 0.3s ease-out; box-shadow: 0 3px 6px rgba(0, 83, 153, 0.15), 0 2px 4px rgba(0, 114, 211, 0.1); border-radius: 18px; background: linear-gradient(145deg, #0072d3, #0088f8); color: #ffffff; font-size: 17px; font-weight: 600; padding: 6px 8px 6px 14px; border: 1px solid rgba(255, 255, 255, 0.2); text-shadow: 0 1px 1px rgba(0,0,0,0.1); }
.sample-section-tag { display: inline-flex; align-items: center; background-color: #f1f3f5; border-radius: 16px; padding: 5px 6px 5px 10px; font-size: 14px; font-weight: 600; color: #5d6873; border: 1px solid #dee2e6; gap: 8px; animation: fadeIn 0.3s ease-out; box-shadow: 0 1px 2px rgba(0,0,0,0.05); transition: background-color 0.3s, border-color 0.3s, color 0.3s; }
.flashing-tag { animation: flash-red 1.2s infinite ease-in-out; }
.flashing-tag .section-count-badge { color: #e74c3c !important; background-color: #fff !important; }
.color-dot { width: 10px; height: 10px; border-radius: 50%; }
.section-count-badge { background-color: #495057; color: #fff; border-radius: 10px; padding: 4px 9px; font-size: 15px; font-weight: 700; min-width: 18px; text-align: center; transition: background-color 0.3s, color 0.3s; }
.main-counter-style .section-count-badge { background-color: rgba(0, 0, 0, 0.2); color: #fff; box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); }
.highlighter-container { display: flex; align-items: center; gap: 8px; margin-top: 5px; padding: 5px; animation: fadeIn 0.5s ease-out; }
/* --- Styles from Rapid Receiver Script --- */
.rr-modal-backdrop { display: none; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5); }
.rr-modal-content { background-color: #fefefe; margin: 15% auto; padding: 20px; border: 1px solid #888; width: 80%; max-width: 500px; border-radius: 5px; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); }
.rr-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ddd; padding-bottom: 10px; }
.rr-modal-header h2 { margin: 0; font-size: 1.25rem; }
.rr-modal-body { padding: 15px 0; }
.rr-modal-footer { display: flex; justify-content: flex-end; align-items: center; gap: 15px; border-top: 1px solid #ddd; padding-top: 10px; }
#barcodeList { width: 100%; padding: 8px; font-size: 16px; box-sizing: border-box; }
.rr-close-button { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
.rr-close-button:hover, .rr-close-button:focus { color: black; text-decoration: none; }
.rr-counter-style { font-size: 14px; font-weight: bold; color: #555; margin-right: auto; }
#barcodeList:disabled { background-color: #f2f2f2; cursor: not-allowed; }
`);
// --- Functions from Counter Script ---
function getColorForString(str) { /* ... (Function content is unchanged) ... */ }
function createCounterElement(id) { /* ... (Function content is unchanged) ... */ }
function updateSpecificCounter(modalElementForInputs, counterElement, inputSelector) { /* ... (Function content is unchanged) ... */ }
function setupModalCounter(modalConfig) { /* ... (Function content is unchanged) ... */ }
function setupHighlighterInput(receiverInput) { /* ... (Function content is unchanged) ... */ }
function relocateAllDangerAlerts() { /* ... (Function content is unchanged) ... */ }
// --- Functions from Rapid Receiver Script ---
async function processBarcodes() {
const barcodeInput = $('#barcodecollection');
const barcodeListArea = $('#barcodeList');
const barcodesText = barcodeListArea.val().trim();
const allLines = barcodesText.split(/\r?\n/);
const barcodesToProcess = allLines.filter(line => line.trim() !== '' && !line.includes('✔️'));
const processButton = $('#processBarcodesBtn');
const counterElement = $('#rr-counter');
if (barcodesToProcess.length === 0) return alert('No new barcodes to process.');
if (barcodeInput.length === 0) return alert('Error: Barcode input field "#barcodecollection" not found.');
processButton.prop('disabled', true).text('Processing...');
barcodeListArea.prop('disabled', true);
const INTER_BARCODE_DELAY = 1200;
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const dispatchEvent = (element, eventType) => element.dispatchEvent(new Event(eventType, { bubbles: true }));
const simulateEnter = async (element) => {
const commonEventProps = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
element.dispatchEvent(new KeyboardEvent('keydown', commonEventProps));
await sleep(50);
element.dispatchEvent(new KeyboardEvent('keyup', commonEventProps));
};
let processedCount = 0;
for (const line of barcodesToProcess) {
processedCount++;
counterElement.text(`Processing: ${processedCount} / ${barcodesToProcess.length}`);
const barcode = line.replace(/ ❌$/, '').trim();
const inputElement = barcodeInput[0];
inputElement.value = barcode;
dispatchEvent(inputElement, 'input');
dispatchEvent(inputElement, 'change');
await sleep(100);
inputElement.focus();
await simulateEnter(inputElement);
await sleep(600);
const isError = $("div.alert.alert-danger").is(":visible");
let marker = isError ? ' ❌' : ' ✔️';
const currentLines = barcodeListArea.val().split('\n');
const originalIndex = allLines.findIndex(l => l === line);
if (originalIndex !== -1) {
currentLines[originalIndex] = barcode + marker;
barcodeListArea.val(currentLines.join('\n'));
allLines[originalIndex] = barcode + marker;
}
$('.alert-dismissable .close').click();
await sleep(INTER_BARCODE_DELAY - 600);
}
processButton.prop('disabled', false).text('Process');
barcodeListArea.prop('disabled', false);
counterElement.text('✅ Complete!').css('color', 'green');
}
function setupRapidReceiver() {
const closeButtonSelector = "#closebtn-smplrecieve, #btnclose-smplcollection";
const closeButton = document.querySelector(closeButtonSelector);
if (!closeButton || closeButton.parentElement.querySelector('#rapidReceiveBtn')) {
return; // Exit if no close button found or our button already exists
}
let rapidReceiveBtn = $('<button type="button" class="btn btn-color-1" id="rapidReceiveBtn">Rapid Receiver</button>');
rapidReceiveBtn.css('margin-right', '5px');
$(closeButton).before(rapidReceiveBtn);
if ($('#rapidReceiveModal').length === 0) {
logDebug("Creating Rapid Receiver modal for the first time.");
let modalHTML = `
<div id="rapidReceiveModal" class="rr-modal-backdrop">
<div class="rr-modal-content">
<div class="rr-modal-header">
<h2>Scan or Paste Barcodes</h2>
<span class="rr-close-button">×</span>
</div>
<div class="rr-modal-body">
<p>Enter each barcode on a new line.</p>
<textarea id="barcodeList" rows="10"></textarea>
</div>
<div class="rr-modal-footer">
<span id="rr-counter" class="rr-counter-style">0 Barcodes Entered</span>
<button id="processBarcodesBtn" class="btn btn-success">Process</button>
</div>
</div>
</div>
`;
$('body').append(modalHTML);
$('.rr-close-button').on('click', () => $('#rapidReceiveModal').hide());
$(window).on('click', (event) => {
if ($(event.target).is('#rapidReceiveModal')) $('#rapidReceiveModal').hide();
});
$('#processBarcodesBtn').on('click', processBarcodes);
$('body').on('input', '#barcodeList', function() {
const barcodesText = $(this).val().trim();
let count = 0;
if (barcodesText) {
count = barcodesText.split(/\r?\n/).filter(line => line.trim() !== '').length;
}
$('#rr-counter').text(`${count} Barcodes Entered`);
});
}
$('#rapidReceiveBtn').on('click', () => {
$('#rapidReceiveModal').show();
$('#barcodeList').val('').prop('disabled', false).focus();
$('#barcodeList').trigger('input');
$('#rr-counter').css('color', '');
$('#processBarcodesBtn').prop('disabled', false).text('Process');
});
}
// --- Main DOM Observer (from Counter Script, now runs everything) ---
function observeDOMChanges() {
const activeModalIntervals = new Map();
logDebug("DOM observer started.");
const observer = new MutationObserver((mutationsList) => {
const hasAddedNodes = mutationsList.some(m => m.addedNodes.length > 0);
if (hasAddedNodes) {
// Setup counters and other UI elements from Counter Script
const modalSelectors = {
'button#btnclose-smplcollection': {
counterId: 'inline-counter-smplcollection',
inputSelector: 'tbody[formarrayname="TubeTypeList"] input[formcontrolname="PatientID"]',
},
'button#closebtn-smplrecieve': {
counterId: 'inline-counter-smplrecieve',
inputSelector: 'td input[formcontrolname="PatientID"]',
}
};
for (const btnSelector in modalSelectors) {
const button = document.querySelector(btnSelector);
if (button) {
const modalKeyElement = button.closest('.modal');
if (modalKeyElement && !modalKeyElement.dataset.counterInitialized) {
setupModalCounter({
...modalSelectors[btnSelector],
modalKeyElement,
targetFooter: button.closest('.modal-footer'),
activeIntervalsMap: activeModalIntervals
});
}
}
}
const receiverInput = document.querySelector('input#receiverStaffID');
if (receiverInput) {
setupHighlighterInput(receiverInput);
}
// **MERGED**: Setup the Rapid Receiver button
setupRapidReceiver();
// Relocate any alerts that have appeared.
relocateAllDangerAlerts();
}
// Cleanup logic for removed nodes
mutationsList.forEach(mutation => {
if (mutation.removedNodes.length > 0) {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeType !== 1) return;
if (activeModalIntervals.has(removedNode)) {
logDebug("Tracked modal removed. Cleaning up counter.");
const { interval, counter } = activeModalIntervals.get(removedNode);
clearInterval(interval);
if (counter) counter.remove();
activeModalIntervals.delete(removedNode);
}
if (removedNode.querySelector('#section-highlighter-input')) {
logDebug("Highlighter removed. Cleaning up listeners.");
document.removeEventListener('update-highlight', ()=>{});
}
});
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
window.addEventListener('load', () => {
try {
observeDOMChanges();
} catch (e) {
logError(`Initialization failed: ${e.message}`);
}
});
// Dummy function definitions to ensure script is valid if user copy-pastes partial code
// The actual definitions are further up
if(typeof getColorForString === 'undefined') { function getColorForString(str) {} }
if(typeof createCounterElement === 'undefined') { function createCounterElement(id) {} }
if(typeof updateSpecificCounter === 'undefined') { function updateSpecificCounter(modalElementForInputs, counterElement, inputSelector) {} }
if(typeof setupModalCounter === 'undefined') { function setupModalCounter(modalConfig) {} }
if(typeof setupHighlighterInput === 'undefined') { function setupHighlighterInput(receiverInput) {} }
if(typeof relocateAllDangerAlerts === 'undefined') { function relocateAllDangerAlerts() {} }
})();