您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Collects barcodes automatically, provides a viewing interface, and clicks the filtered result.
当前为
// ==UserScript== // @name Pending barcodes // @namespace http://tampermonkey.net/ // @version 1.9.4 // @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'); } // --- NEW: Function to wait for the grid to update and then click the first row --- 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 }); }); } // --- CHANGED: Converted to async to handle waiting for the click --- async 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.`); // --- NEW: Wait for the grid to update and then click the first row --- try { 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: Could not click grid row.", error); } } else { console.error('Barcode Collector: Could not find the "Barcode" filter input field on the page.'); } } 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); // Use await here } } 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: 85vh; } .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: 4px 8px; text-align: left; font-size: 0.85em; } .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: #E8F5E9 !important; } .bc-table-body tbody tr.barcode-found:hover { background-color: #C8E6C9 !important; } .action-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; } `); initialize(); })();