您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Combines Rapid Barcode Receiver with Sample Counter, Lab Filter, and Alert Relocation.
// ==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() {} } })();