您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Collects barcodes automatically from a table, timestamps them, and provides a viewing interface.
当前为
// ==UserScript== // @name Pending barcodes // @namespace http://tampermonkey.net/ // @version 1.9.1 // @description Collects barcodes automatically from a table, timestamps them, and provides a viewing interface. // @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(` /* This rule targets the barcode container. */ /* It sets its width to 30% of the viewport width. */ /* 'float: left' positions it on the left side of the page. */ #barcode-inpage-container { width: 30vw !important; float: left !important; } /* This rule targets the main AG-Grid table wrapper. */ /* It sets its width to 70% of the viewport width. */ /* 'margin-left: auto' and 'margin-right: 0' push it to the right, next to the floated barcode container. */ .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'; // The ID of the box displaying the current patient's barcode const STORAGE_KEY = 'collectedBarcodes_storage'; const IN_PAGE_TABLE_ID = 'barcode-inpage-container'; const INJECTION_POINT_SELECTOR = '.row.labordertab'; // --- State Flags & Cache --- const collectedBarcodesThisSession = new Set(); let lastCheckedPatientBarcode = null; let timeSinceInterval = null; let observerDebounceTimer = null; // Timer for debouncing the observer let isTableUpdating = false; // Flag to prevent table update re-entry // --- Main Logic --- function initialize() { console.log("Barcode Collector: Script started. Observing for page changes..."); const observer = new MutationObserver((mutations, obs) => { // Debounce the observer to prevent it from firing too rapidly and causing freezes. if (observerDebounceTimer) clearTimeout(observerDebounceTimer); observerDebounceTimer = setTimeout(() => { const patientBarcodeBox = document.querySelector(BARCODE_DISPLAY_SELECTOR); 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 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); // Wait for 250ms of inactivity before running }); 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.`); } updateViewWithCheckmark(barcodeToMark); } function updateViewWithCheckmark(barcode) { const table = document.getElementById(IN_PAGE_TABLE_ID); if (!table) return; const row = table.querySelector(`tr[data-barcode-row="${barcode}"]`); if (row) { const statusCell = row.querySelector('.status-cell-bc'); if (statusCell) { statusCell.innerHTML = `<span style="color: #4CAF50; font-weight: bold; font-size: 1.2em;" title="This barcode has been found on a patient page.">✔</span>`; } } } 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 enterBarcodeInFilter(barcode) { const targetInput = findFloatingFilterInputByHeader('Barcode'); if (targetInput) { targetInput.focus(); targetInput.value = barcode; targetInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); targetInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); 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 })); targetInput.blur(); console.log(`Barcode Collector: Programmatically entered barcode "${barcode}" into the filter.`); } else { console.error('Barcode Collector: Could not find the "Barcode" filter input field on the page.'); } } async function updateOrInsertBarcodeTable() { if (isTableUpdating) return; // Prevent function from running if it's already busy 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>Status</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}"> <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="status-cell-bc"> ${entry.found ? `<span style="color: #4CAF50; font-weight: bold; font-size: 1.2em;" title="Found">✔</span>` : ''} </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="6">No pending barcodes.</td></tr>'; } const tableBody = container.querySelector('tbody'); // Only update the DOM if the content has actually changed to reduce performance impact if (tableBody.innerHTML !== tableRows) { tableBody.innerHTML = tableRows; } tableBody.removeEventListener('click', handleTableClick); // Remove old listener to prevent duplicates tableBody.addEventListener('click', handleTableClick); // Add the new one if (timeSinceInterval) clearInterval(timeSinceInterval); timeSinceInterval = setInterval(() => { container.querySelectorAll('td[data-timestamp]').forEach(cell => { cell.textContent = formatTimeSince(cell.dataset.timestamp); }); }, 5000); } finally { isTableUpdating = false; // Release the lock } } // New handler function for table clicks to keep listener logic clean 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; 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(); // Refresh table } // --- Styling for the In-Page Table --- 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; } .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; max-height: 300px; overflow-y: auto; } .bc-table-body table { width: 100%; border-collapse: collapse; } .bc-table-body th, .bc-table-body td { border: 1px solid #ddd; padding: 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; } .action-cell-bc, .status-cell-bc { text-align: center !important; } .delete-barcode-btn { cursor: pointer; font-weight: bold; font-size: 18px; color: #ef5350; padding: 0 5px; border-radius: 4px; } .delete-barcode-btn:hover { color: white; background-color: #d32f2f; } `); // --- Start the script --- initialize(); })();