- // ==UserScript==
- // @name AG-Grid Data Extractor - Complete (Modified)
- // @namespace http://tampermonkey.net/
- // @version 2.5 // Increased version number
- // @description Collects unique barcodes with clinic info, 50 rows per page, sorted by last modified date, truncates clinic names, improved SPA support - Buttons next to Reset
- // @author Your Name
- // @match https://his.kaauh.org/lab/*
- // @grant GM_addStyle
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- const dateColumnIds = ['orderCreatedOnEpoch', 'createdDate', 'orderDate'];
- const lastModifiedColumnId = 'orderLastModifiedOnEpoch';
- const clinicColumnId = 'clinic';
- const barcodeColumnId = 'barcode';
- const testDescriptionColumnId = 'testDescription';
- const agGridBodyViewportSelector = '.ag-body-viewport.ag-layout-normal';
- const agGridRowSelector = '.ag-row[role="row"]';
- const ROWS_PER_PAGE = 500;
- const CLINIC_TRUNCATE_LENGTH = 50;
-
- GM_addStyle(`
- .userscript-container {
- position: fixed;
- top: 20px;
- left: 20px;
- background: #fff;
- padding: 20px;
- border: 1px solid #bbb;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- z-index: 9999;
- max-height: 80vh;
- overflow-y: auto;
- min-width: 400px;
- max-width: 95%;
- font-family: sans-serif;
- }
- .userscript-title {
- margin-top: 0;
- margin-bottom: 15px;
- color: #333;
- font-size: 1.4em;
- border-bottom: 1px solid #eee;
- padding-bottom: 10px;
- }
- .userscript-table {
- border-collapse: collapse;
- width: 100%;
- font-size: 12px;
- }
- .userscript-table th,
- .userscript-table td {
- border: 1px solid #ddd;
- padding: 4px 6px;
- text-align: left;
- vertical-align: top;
- word-break: break-word;
- }
- .userscript-table td {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .userscript-table th {
- background-color: #007bff;
- color: white;
- font-weight: bold;
- border-color: #007bff;
- font-size: 11px;
- padding: 6px;
- }
- .userscript-table tr:nth-child(even) {
- background-color: #f8f9fa;
- }
- .userscript-pagination {
- display: flex;
- justify-content: center;
- margin-top: 10px;
- gap: 5px;
- }
- .userscript-page-btn {
- padding: 2px 6px;
- font-size: 12px;
- cursor: pointer;
- border: 1px solid #ddd;
- background: white;
- border-radius: 3px;
- }
- .userscript-active-page {
- font-weight: bold;
- background: #007bff;
- color: white;
- border-color: #007bff;
- }
- .userscript-button-container {
- margin-top: 15px;
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- }
- .userscript-button {
- padding: 8px 16px;
- border-radius: 4px;
- border: none;
- cursor: pointer;
- font-weight: bold;
- transition: all 0.2s;
- }
- .userscript-button-primary {
- background-color: #007bff;
- color: white;
- }
- .userscript-button-primary:hover {
- background-color: #0069d9;
- }
- .userscript-button-danger {
- background-color: #dc3545;
- color: white;
- }
- .userscript-button-danger:hover {
- background-color: #c82333;
- }
- .userscript-scroll-message {
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- background-color: #e0f7fa;
- padding: 10px 15px;
- border: 1px solid #00bcd4;
- border-radius: 5px;
- z-index: 9998;
- font-size: 0.9em;
- }
- `);
-
- let collectedDataMap = new Map();
- let gridObserver = null;
- let observerForButtonContainer = null;
- let activeCreationDateColumn = null;
- let uiElements = {
- extractButton: null,
- stopButton: null,
- scrollMessage: null
- };
-
- function parseDateFromCell(text) {
- if (!text) return 0;
- if (/^\d{10,}$/.test(text)) return parseInt(text);
- const date = new Date(text);
- return isNaN(date) ? 0 : date.getTime();
- }
-
- function formatDate(timestamp) {
- return timestamp ? new Date(timestamp).toLocaleString() : 'N/A';
- }
-
- function escapeCsv(text) {
- const strText = String(text || '')
- .replace(/"/g, '""')
- .replace(/^[=+@-]/, "'$&");
- return `"${strText}"`;
- }
-
- function truncateText(text, length) {
- const strText = String(text || '');
- if (strText.length > length) {
- return strText.substring(0, length) + '...';
- }
- return strText;
- }
-
- function waitForElement(selector, options = {}) {
- const { timeout = 10000, pollInterval = 250 } = options;
- return new Promise((resolve, reject) => {
- const element = document.querySelector(selector);
- if (element) return resolve(element);
-
- const startTime = Date.now();
- const poll = setInterval(() => {
- const element = document.querySelector(selector);
- if (element) {
- clearInterval(poll);
- resolve(element);
- } else if (Date.now() - startTime >= timeout) {
- clearInterval(poll);
- resolve(null);
- }
- }, pollInterval);
- });
- }
-
-
- function waitForGridData() {
- return new Promise((resolve, reject) => {
- const startTime = Date.now();
- const poll = setInterval(() => {
- const rows = document.querySelectorAll(agGridRowSelector);
- if (rows.length > 0) {
- clearInterval(poll);
- setTimeout(resolve, 1000);
- } else if (Date.now() - startTime >= 30000) {
- clearInterval(poll);
- reject(new Error('No grid rows detected within timeout'));
- }
- }, 500);
- });
- }
-
- function showTemporaryMessage(message, type = 'info', duration = 5000) {
- const colors = {
- info: { bg: '#e0f7fa', border: '#00bcd4', text: '#006064' },
- success: { bg: '#d4edda', border: '#c3e6cb', text: '#155724' },
- error: { bg: '#f8d7da', border: '#f5c6cb', text: '#721c24' }
- };
-
- const messageDiv = document.createElement('div');
- messageDiv.style.cssText = `
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: 15px 20px;
- border-radius: 5px;
- z-index: 10000;
- font-weight: bold;
- box-shadow: 0 2px 10px rgba(0,0,0,0.2);
- max-width: 80%;
- text-align: center;
- background-color: ${colors[type].bg};
- border: 1px solid ${colors[type].border};
- color: ${colors[type].text};
- `;
- messageDiv.textContent = message;
-
- document.body.appendChild(messageDiv);
- setTimeout(() => messageDiv.remove(), type === 'error' ? 10000 : duration);
- }
-
- function processRowElement(rowElement) {
- const rowData = {};
- const cells = rowElement.querySelectorAll('.ag-cell');
-
- if (!activeCreationDateColumn) {
- for (const colId of dateColumnIds) {
- if (rowElement.querySelector(`[col-id="${colId}"]`)) {
- activeCreationDateColumn = colId;
- console.log(`Detected active creation date column: ${activeCreationDateColumn}`);
- break;
- }
- }
- if (!activeCreationDateColumn) {
- console.warn('No known date column found in this row.');
- }
- }
-
-
- cells.forEach(cell => {
- const colId = cell.getAttribute('col-id') || cell.parentElement?.getAttribute('col-id');
- if (colId) rowData[colId] = cell.textContent.trim();
- });
-
- if (rowData[barcodeColumnId] && rowData[testDescriptionColumnId] && activeCreationDateColumn && rowData[lastModifiedColumnId]) {
- if (!collectedDataMap.has(rowData[barcodeColumnId])) {
- collectedDataMap.set(rowData[barcodeColumnId], {
- testDescription: rowData[testDescriptionColumnId],
- barcode: rowData[barcodeColumnId],
- clinic: rowData[clinicColumnId] || 'N/A',
- orderCreationDate: rowData[activeCreationDateColumn],
- orderLastModified: rowData[lastModifiedColumnId],
- creationTimestamp: parseDateFromCell(rowData[activeCreationDateColumn]),
- modifiedTimestamp: parseDateFromCell(rowData[lastModifiedColumnId])
- });
- }
- } else {
- if (!rowData[barcodeColumnId]) console.warn('Skipping row due to missing barcode.');
- if (!rowData[testDescriptionColumnId]) console.warn('Skipping row due to missing test description.');
- if (!activeCreationDateColumn) console.warn('Skipping row due to unknown creation date column.');
- if (!rowData[lastModifiedColumnId]) console.warn('Skipping row due to missing last modified date.');
- }
- }
-
- function createPagination(totalPages, currentPage, onPageChange) {
- const container = document.createElement('div');
- container.className = 'userscript-pagination';
-
- if (currentPage > 1) {
- const prevBtn = document.createElement('button');
- prevBtn.className = 'userscript-page-btn';
- prevBtn.textContent = '←';
- prevBtn.onclick = () => onPageChange(currentPage - 1);
- container.appendChild(prevBtn);
- }
-
- const startPage = Math.max(1, currentPage - 2);
- const endPage = Math.min(totalPages, currentPage + 2);
-
- if (startPage > 1) {
- const firstBtn = document.createElement('button');
- firstBtn.className = 'userscript-page-btn';
- firstBtn.textContent = '1';
- firstBtn.onclick = () => onPageChange(1);
- container.appendChild(firstBtn);
-
- if (startPage > 2) {
- const ellipsis = document.createElement('span');
- ellipsis.textContent = '...';
- ellipsis.style.padding = '0 5px';
- container.appendChild(ellipsis);
- }
- }
-
- for (let i = startPage; i <= endPage; i++) {
- const btn = document.createElement('button');
- btn.className = `userscript-page-btn ${i === currentPage ? 'userscript-active-page' : ''}`;
- btn.textContent = i;
- btn.onclick = () => onPageChange(i);
- container.appendChild(btn);
- }
-
- if (endPage < totalPages) {
- if (endPage < totalPages - 1) {
- const ellipsis = document.createElement('span');
- ellipsis.textContent = '...';
- ellipsis.style.padding = '0 5px';
- container.appendChild(ellipsis);
- }
-
- const lastBtn = document.createElement('button');
- lastBtn.className = 'userscript-page-btn';
- lastBtn.textContent = totalPages;
- lastBtn.onclick = () => onPageChange(totalPages);
- container.appendChild(lastBtn);
- }
-
- if (currentPage < totalPages) {
- const nextBtn = document.createElement('button');
- nextBtn.className = 'userscript-page-btn';
- nextBtn.textContent = '→';
- nextBtn.onclick = () => onPageChange(currentPage + 1);
- container.appendChild(nextBtn);
- }
-
- return container;
- }
-
- function displayData(data) {
- document.querySelectorAll('.userscript-container').forEach(el => el.remove());
-
- if (data.length === 0) {
- showTemporaryMessage('No data collected matching the criteria', 'error');
- return;
- }
-
- const sortedData = [...data].sort((a, b) => b.modifiedTimestamp - a.modifiedTimestamp);
- const totalPages = Math.ceil(sortedData.length / ROWS_PER_PAGE);
- let currentPage = 1;
-
- const container = document.createElement('div');
- container.className = 'userscript-container';
-
- const title = document.createElement('h2');
- title.className = 'userscript-title';
- title.textContent = `PENDING TESTS (${sortedData.length} records)`;
- container.appendChild(title);
-
- const renderPage = (page) => {
- const startIdx = (page - 1) * ROWS_PER_PAGE;
- const endIdx = startIdx + ROWS_PER_PAGE;
- const pageData = sortedData.slice(startIdx, endIdx);
-
- const oldTable = container.querySelector('.userscript-table-container');
- if (oldTable) oldTable.remove();
-
- const tableContainer = document.createElement('div');
- tableContainer.className = 'userscript-table-container';
-
- const table = document.createElement('table');
- table.className = 'userscript-table';
- table.setAttribute('role', 'grid');
-
- const headers = ['NO.', 'Test Description', 'Barcode', 'Clinic', 'Created', 'Last Modified'];
- const thead = document.createElement('thead');
- const headerRow = document.createElement('tr');
-
- headers.forEach((text, i) => {
- const th = document.createElement('th');
- th.textContent = text;
- headerRow.appendChild(th);
- });
-
- thead.appendChild(headerRow);
- table.appendChild(thead);
-
- const tbody = document.createElement('tbody');
- pageData.forEach((item, idx) => {
- const row = document.createElement('tr');
- const absoluteIdx = startIdx + idx + 1;
-
- [
- absoluteIdx,
- item.testDescription,
- item.barcode,
- truncateText(item.clinic, CLINIC_TRUNCATE_LENGTH),
- formatDate(item.creationTimestamp),
- formatDate(item.modifiedTimestamp)
- ].forEach(content => {
- const td = document.createElement('td');
- td.textContent = content;
- row.appendChild(td);
- });
-
- tbody.appendChild(row);
- });
- table.appendChild(tbody);
- tableContainer.appendChild(table);
-
- const oldPagination = container.querySelector('.userscript-pagination');
- if (oldPagination) oldPagination.remove();
- tableContainer.appendChild(createPagination(totalPages, page, (newPage) => {
- currentPage = newPage;
- renderPage(newPage);
- }));
-
- const buttonContainerElement = container.querySelector('.userscript-button-container');
- if (buttonContainerElement) {
- container.insertBefore(tableContainer, buttonContainerElement);
- } else {
- container.appendChild(tableContainer);
- }
- };
-
- let buttonContainer = container.querySelector('.userscript-button-container');
- if (!buttonContainer) {
- buttonContainer = document.createElement('div');
- buttonContainer.className = 'userscript-button-container';
- container.appendChild(buttonContainer);
- }
-
- const printBtn = document.createElement('button');
- printBtn.className = 'userscript-button userscript-button-primary';
- printBtn.textContent = 'Print All';
- printBtn.onclick = () => {
- const printWindow = window.open('', '_blank');
- printWindow.document.write(`
- <html>
- <head>
- <title>Test Descriptions and Barcodes</title>
- <style>
- body { font-family: sans-serif; margin: 10mm; }
- h2 { margin-top: 0; }
- table { border-collapse: collapse; width: 100%; font-size: 10px; margin-bottom: 20px; }
- th, td { border: 1px solid #ddd; padding: 3px 5px; }
- th { background-color: #f2f2f2; font-weight: bold; }
- .page-break { page-break-after: always; }
- @page { size: auto; margin: 10mm; }
- td {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- </style>
- </head>
- <body>
- <h2>PENDING TESTS (${sortedData.length} records)</h2>
- `);
-
- for (let i = 0; i < sortedData.length; i += ROWS_PER_PAGE) {
- const pageData = sortedData.slice(i, i + ROWS_PER_PAGE);
-
- printWindow.document.write(`
- <table>
- <thead>
- <tr>
- <th>NO.</th>
- <th>Test Description</th>
- <th>Barcode</th>
- <th>Clinic</th>
- <th>Created</th>
- <th>Last Modified</th>
- </tr>
- </thead>
- <tbody>
- `);
-
- pageData.forEach((item, idx) => {
- printWindow.document.write(`
- <tr>
- <td>${i + idx + 1}</td>
- <td>${item.testDescription}</td>
- <td>${item.barcode}</td>
- <td>${truncateText(item.clinic, CLINIC_TRUNCATE_LENGTH)}</td>
- <td>${formatDate(item.creationTimestamp)}</td>
- <td>${formatDate(item.modifiedTimestamp)}</td>
- </tr>
- `);
- });
-
- printWindow.document.write('</tbody></table>');
-
- if (i + ROWS_PER_PAGE < sortedData.length) {
- printWindow.document.write('<div class="page-break"></div>');
- }
- }
-
- printWindow.document.write(`
- <script>
- setTimeout(function() {
- window.print();
- window.close();
- }, 200);
- </script>
- </body>
- </html>
- `);
- printWindow.document.close();
- };
- buttonContainer.appendChild(printBtn);
-
- const downloadBtn = document.createElement('button');
- downloadBtn.className = 'userscript-button userscript-button-primary';
- downloadBtn.textContent = 'Download CSV';
- downloadBtn.onclick = () => {
- const headers = ['NO.', 'Test Description', 'Barcode', 'Clinic', 'Created', 'Last Modified'];
- let csv = headers.map(escapeCsv).join(',') + '\n';
-
- sortedData.forEach((item, index) => {
- csv += [
- index + 1,
- item.testDescription,
- item.barcode,
- item.clinic,
- formatDate(item.creationTimestamp),
- formatDate(item.modifiedTimestamp)
- ].map(escapeCsv).join(',') + '\n';
- });
-
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = `test_data_${new Date().toISOString().slice(0,10)}.csv`;
-
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
-
- setTimeout(() => URL.revokeObjectURL(url), 100);
- };
- buttonContainer.appendChild(downloadBtn);
-
- const closeBtn = document.createElement('button');
- closeBtn.className = 'userscript-button userscript-button-danger';
- closeBtn.textContent = 'Close';
- closeBtn.onclick = () => container.remove();
- buttonContainer.appendChild(closeBtn);
-
- document.body.appendChild(container);
-
- renderPage(1);
- }
-
- async function startExtraction() {
- if (gridObserver) {
- showTemporaryMessage('Extraction already in progress', 'info');
- return;
- }
-
- collectedDataMap.clear();
- activeCreationDateColumn = null;
- if (uiElements.extractButton) uiElements.extractButton.style.display = 'none';
- if (uiElements.stopButton) uiElements.stopButton.style.display = 'inline-block';
-
-
- showTemporaryMessage('Preparing extraction...', 'info');
-
- try {
- const gridBodyViewport = await waitForElement(agGridBodyViewportSelector, { timeout: 30000 });
- if (!gridBodyViewport) {
- throw new Error('AG-Grid viewport not found within timeout.');
- }
-
- await waitForGridData();
-
- gridBodyViewport.querySelectorAll(agGridRowSelector).forEach(row => {
- row.dataset.processed = 'true';
- processRowElement(row);
- });
-
- if (!uiElements.scrollMessage) {
- uiElements.scrollMessage = document.createElement('div');
- uiElements.scrollMessage.className = 'userscript-scroll-message';
- document.body.appendChild(uiElements.scrollMessage);
- }
- uiElements.scrollMessage.textContent = `Collected ${collectedDataMap.size} records. Scroll to load more...`;
- uiElements.scrollMessage.style.display = 'block';
-
- gridObserver = new MutationObserver(() => {
- clearTimeout(gridObserver._debounce);
- gridObserver._debounce = setTimeout(() => {
- const newRows = Array.from(gridBodyViewport.querySelectorAll(agGridRowSelector))
- .filter(row => !row.dataset.processed);
-
- newRows.forEach(row => {
- row.dataset.processed = 'true';
- processRowElement(row);
- });
-
- if (uiElements.scrollMessage) {
- uiElements.scrollMessage.textContent = `Collected ${collectedDataMap.size} records`;
- }
- console.log(`Processed ${newRows.length} new rows. Total collected: ${collectedDataMap.size}`);
- }, 150);
- });
-
- gridObserver.observe(gridBodyViewport, { childList: true, subtree: true });
-
- showTemporaryMessage('Extraction started. Scroll down the grid to collect more data.', 'success');
-
- } catch (error) {
- console.error('Extraction error:', error);
- cleanup();
- showTemporaryMessage(
- error.message.includes('Timeout') ?
- 'Grid loading timeout or no rows found.' :
- 'Failed to start extraction.',
- 'error',
- 10000
- );
- }
- }
-
- function stopExtractionAndDisplay() {
- if (!gridObserver) {
- showTemporaryMessage('No active extraction to stop', 'info');
- return;
- }
-
- cleanup();
- const finalData = Array.from(collectedDataMap.values());
-
- if (finalData.length === 0) {
- showTemporaryMessage(
- 'No data collected. Possible issues:\n1. No matching columns found.\n2. Grid structure changed.\n3. No rows were loaded during extraction.',
- 'error',
- 10000
- );
- return;
- }
-
- showTemporaryMessage(`Collected ${finalData.length} records. Generating report...`, 'success', 3000);
- setTimeout(() => displayData(finalData), 500);
- }
-
- function addButtonsToPage() {
- if (uiElements.extractButton && document.body.contains(uiElements.extractButton)) {
- return;
- }
-
- const resetButton = document.querySelector('.nova-btn.nova-btn--ghost.nova-btn--md[translateid="lab-order-list.Reset"]');
-
- if (resetButton && resetButton.parentElement) {
- uiElements.extractButton = document.createElement('button');
- uiElements.extractButton.textContent = 'PRINT';
- uiElements.extractButton.className = 'userscript-button nova-btn nova-btn--primary nova-btn--md';
- uiElements.extractButton.style.marginLeft = '10px';
- uiElements.extractButton.onclick = startExtraction;
-
- uiElements.stopButton = document.createElement('button');
- uiElements.stopButton.textContent = 'Stop & Generate Report';
- uiElements.stopButton.className = 'userscript-button nova-btn nova-btn--danger nova-btn--md';
- uiElements.stopButton.style.marginLeft = '10px';
- uiElements.stopButton.style.display = 'none';
- uiElements.stopButton.onclick = stopExtractionAndDisplay;
-
- resetButton.parentElement.insertBefore(uiElements.extractButton, resetButton.nextSibling);
- uiElements.extractButton.parentElement.insertBefore(uiElements.stopButton, uiElements.extractButton.nextSibling);
-
- console.log('AG-Grid Extractor buttons added next to Reset button.');
-
- } else {
- console.log('Reset button not found, skipping adding extractor buttons.');
- }
- }
-
-
- function cleanup() {
- if (observerForButtonContainer) {
- observerForButtonContainer.disconnect();
- observerForButtonContainer = null;
- console.log('AG-Grid Extractor button container observer disconnected.');
- }
-
- if (gridObserver) {
- gridObserver.disconnect();
- clearTimeout(gridObserver._debounce);
- gridObserver = null;
- console.log('AG-Grid data observer disconnected.');
- }
-
- if (uiElements.extractButton && uiElements.extractButton.parentElement) {
- uiElements.extractButton.parentElement.removeChild(uiElements.extractButton);
- uiElements.extractButton = null;
- }
- if (uiElements.stopButton && uiElements.stopButton.parentElement) {
- uiElements.stopButton.parentElement.removeChild(uiElements.stopButton);
- uiElements.stopButton = null;
- }
- if (uiElements.scrollMessage && uiElements.scrollMessage.parentElement) {
- uiElements.scrollMessage.parentElement.removeChild(uiElements.scrollMessage);
- uiElements.scrollMessage = null;
- }
- console.log('AG-Grid Extractor UI elements removed.');
- }
-
-
- function initialize() {
- console.log('AG-Grid Data Extractor script initializing...');
-
- addButtonsToPage();
-
- const targetNode = document.body;
- const config = { childList: true, subtree: true };
-
- observerForButtonContainer = new MutationObserver((mutationsList, observer) => {
- addButtonsToPage();
- });
-
- observerForButtonContainer.observe(targetNode, config);
- console.log('MutationObserver started to watch for button container.');
-
-
- window.addEventListener('beforeunload', cleanup);
- }
-
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initialize);
- } else {
- initialize();
- }
-
- })();