AG-Grid Data Extractor - Complete (Modified)

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

  1. // ==UserScript==
  2. // @name AG-Grid Data Extractor - Complete (Modified)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.5 // Increased version number
  5. // @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
  6. // @author Your Name
  7. // @match https://his.kaauh.org/lab/*
  8. // @grant GM_addStyle
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. const dateColumnIds = ['orderCreatedOnEpoch', 'createdDate', 'orderDate'];
  15. const lastModifiedColumnId = 'orderLastModifiedOnEpoch';
  16. const clinicColumnId = 'clinic';
  17. const barcodeColumnId = 'barcode';
  18. const testDescriptionColumnId = 'testDescription';
  19. const agGridBodyViewportSelector = '.ag-body-viewport.ag-layout-normal';
  20. const agGridRowSelector = '.ag-row[role="row"]';
  21. const ROWS_PER_PAGE = 500;
  22. const CLINIC_TRUNCATE_LENGTH = 50;
  23.  
  24. GM_addStyle(`
  25. .userscript-container {
  26. position: fixed;
  27. top: 20px;
  28. left: 20px;
  29. background: #fff;
  30. padding: 20px;
  31. border: 1px solid #bbb;
  32. border-radius: 8px;
  33. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  34. z-index: 9999;
  35. max-height: 80vh;
  36. overflow-y: auto;
  37. min-width: 400px;
  38. max-width: 95%;
  39. font-family: sans-serif;
  40. }
  41. .userscript-title {
  42. margin-top: 0;
  43. margin-bottom: 15px;
  44. color: #333;
  45. font-size: 1.4em;
  46. border-bottom: 1px solid #eee;
  47. padding-bottom: 10px;
  48. }
  49. .userscript-table {
  50. border-collapse: collapse;
  51. width: 100%;
  52. font-size: 12px;
  53. }
  54. .userscript-table th,
  55. .userscript-table td {
  56. border: 1px solid #ddd;
  57. padding: 4px 6px;
  58. text-align: left;
  59. vertical-align: top;
  60. word-break: break-word;
  61. }
  62. .userscript-table td {
  63. white-space: nowrap;
  64. overflow: hidden;
  65. text-overflow: ellipsis;
  66. }
  67. .userscript-table th {
  68. background-color: #007bff;
  69. color: white;
  70. font-weight: bold;
  71. border-color: #007bff;
  72. font-size: 11px;
  73. padding: 6px;
  74. }
  75. .userscript-table tr:nth-child(even) {
  76. background-color: #f8f9fa;
  77. }
  78. .userscript-pagination {
  79. display: flex;
  80. justify-content: center;
  81. margin-top: 10px;
  82. gap: 5px;
  83. }
  84. .userscript-page-btn {
  85. padding: 2px 6px;
  86. font-size: 12px;
  87. cursor: pointer;
  88. border: 1px solid #ddd;
  89. background: white;
  90. border-radius: 3px;
  91. }
  92. .userscript-active-page {
  93. font-weight: bold;
  94. background: #007bff;
  95. color: white;
  96. border-color: #007bff;
  97. }
  98. .userscript-button-container {
  99. margin-top: 15px;
  100. display: flex;
  101. gap: 10px;
  102. flex-wrap: wrap;
  103. }
  104. .userscript-button {
  105. padding: 8px 16px;
  106. border-radius: 4px;
  107. border: none;
  108. cursor: pointer;
  109. font-weight: bold;
  110. transition: all 0.2s;
  111. }
  112. .userscript-button-primary {
  113. background-color: #007bff;
  114. color: white;
  115. }
  116. .userscript-button-primary:hover {
  117. background-color: #0069d9;
  118. }
  119. .userscript-button-danger {
  120. background-color: #dc3545;
  121. color: white;
  122. }
  123. .userscript-button-danger:hover {
  124. background-color: #c82333;
  125. }
  126. .userscript-scroll-message {
  127. position: fixed;
  128. bottom: 20px;
  129. left: 50%;
  130. transform: translateX(-50%);
  131. background-color: #e0f7fa;
  132. padding: 10px 15px;
  133. border: 1px solid #00bcd4;
  134. border-radius: 5px;
  135. z-index: 9998;
  136. font-size: 0.9em;
  137. }
  138. `);
  139.  
  140. let collectedDataMap = new Map();
  141. let gridObserver = null;
  142. let observerForButtonContainer = null;
  143. let activeCreationDateColumn = null;
  144. let uiElements = {
  145. extractButton: null,
  146. stopButton: null,
  147. scrollMessage: null
  148. };
  149.  
  150. function parseDateFromCell(text) {
  151. if (!text) return 0;
  152. if (/^\d{10,}$/.test(text)) return parseInt(text);
  153. const date = new Date(text);
  154. return isNaN(date) ? 0 : date.getTime();
  155. }
  156.  
  157. function formatDate(timestamp) {
  158. return timestamp ? new Date(timestamp).toLocaleString() : 'N/A';
  159. }
  160.  
  161. function escapeCsv(text) {
  162. const strText = String(text || '')
  163. .replace(/"/g, '""')
  164. .replace(/^[=+@-]/, "'$&");
  165. return `"${strText}"`;
  166. }
  167.  
  168. function truncateText(text, length) {
  169. const strText = String(text || '');
  170. if (strText.length > length) {
  171. return strText.substring(0, length) + '...';
  172. }
  173. return strText;
  174. }
  175.  
  176. function waitForElement(selector, options = {}) {
  177. const { timeout = 10000, pollInterval = 250 } = options;
  178. return new Promise((resolve, reject) => {
  179. const element = document.querySelector(selector);
  180. if (element) return resolve(element);
  181.  
  182. const startTime = Date.now();
  183. const poll = setInterval(() => {
  184. const element = document.querySelector(selector);
  185. if (element) {
  186. clearInterval(poll);
  187. resolve(element);
  188. } else if (Date.now() - startTime >= timeout) {
  189. clearInterval(poll);
  190. resolve(null);
  191. }
  192. }, pollInterval);
  193. });
  194. }
  195.  
  196.  
  197. function waitForGridData() {
  198. return new Promise((resolve, reject) => {
  199. const startTime = Date.now();
  200. const poll = setInterval(() => {
  201. const rows = document.querySelectorAll(agGridRowSelector);
  202. if (rows.length > 0) {
  203. clearInterval(poll);
  204. setTimeout(resolve, 1000);
  205. } else if (Date.now() - startTime >= 30000) {
  206. clearInterval(poll);
  207. reject(new Error('No grid rows detected within timeout'));
  208. }
  209. }, 500);
  210. });
  211. }
  212.  
  213. function showTemporaryMessage(message, type = 'info', duration = 5000) {
  214. const colors = {
  215. info: { bg: '#e0f7fa', border: '#00bcd4', text: '#006064' },
  216. success: { bg: '#d4edda', border: '#c3e6cb', text: '#155724' },
  217. error: { bg: '#f8d7da', border: '#f5c6cb', text: '#721c24' }
  218. };
  219.  
  220. const messageDiv = document.createElement('div');
  221. messageDiv.style.cssText = `
  222. position: fixed;
  223. top: 50%;
  224. left: 50%;
  225. transform: translate(-50%, -50%);
  226. padding: 15px 20px;
  227. border-radius: 5px;
  228. z-index: 10000;
  229. font-weight: bold;
  230. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  231. max-width: 80%;
  232. text-align: center;
  233. background-color: ${colors[type].bg};
  234. border: 1px solid ${colors[type].border};
  235. color: ${colors[type].text};
  236. `;
  237. messageDiv.textContent = message;
  238.  
  239. document.body.appendChild(messageDiv);
  240. setTimeout(() => messageDiv.remove(), type === 'error' ? 10000 : duration);
  241. }
  242.  
  243. function processRowElement(rowElement) {
  244. const rowData = {};
  245. const cells = rowElement.querySelectorAll('.ag-cell');
  246.  
  247. if (!activeCreationDateColumn) {
  248. for (const colId of dateColumnIds) {
  249. if (rowElement.querySelector(`[col-id="${colId}"]`)) {
  250. activeCreationDateColumn = colId;
  251. console.log(`Detected active creation date column: ${activeCreationDateColumn}`);
  252. break;
  253. }
  254. }
  255. if (!activeCreationDateColumn) {
  256. console.warn('No known date column found in this row.');
  257. }
  258. }
  259.  
  260.  
  261. cells.forEach(cell => {
  262. const colId = cell.getAttribute('col-id') || cell.parentElement?.getAttribute('col-id');
  263. if (colId) rowData[colId] = cell.textContent.trim();
  264. });
  265.  
  266. if (rowData[barcodeColumnId] && rowData[testDescriptionColumnId] && activeCreationDateColumn && rowData[lastModifiedColumnId]) {
  267. if (!collectedDataMap.has(rowData[barcodeColumnId])) {
  268. collectedDataMap.set(rowData[barcodeColumnId], {
  269. testDescription: rowData[testDescriptionColumnId],
  270. barcode: rowData[barcodeColumnId],
  271. clinic: rowData[clinicColumnId] || 'N/A',
  272. orderCreationDate: rowData[activeCreationDateColumn],
  273. orderLastModified: rowData[lastModifiedColumnId],
  274. creationTimestamp: parseDateFromCell(rowData[activeCreationDateColumn]),
  275. modifiedTimestamp: parseDateFromCell(rowData[lastModifiedColumnId])
  276. });
  277. }
  278. } else {
  279. if (!rowData[barcodeColumnId]) console.warn('Skipping row due to missing barcode.');
  280. if (!rowData[testDescriptionColumnId]) console.warn('Skipping row due to missing test description.');
  281. if (!activeCreationDateColumn) console.warn('Skipping row due to unknown creation date column.');
  282. if (!rowData[lastModifiedColumnId]) console.warn('Skipping row due to missing last modified date.');
  283. }
  284. }
  285.  
  286. function createPagination(totalPages, currentPage, onPageChange) {
  287. const container = document.createElement('div');
  288. container.className = 'userscript-pagination';
  289.  
  290. if (currentPage > 1) {
  291. const prevBtn = document.createElement('button');
  292. prevBtn.className = 'userscript-page-btn';
  293. prevBtn.textContent = '←';
  294. prevBtn.onclick = () => onPageChange(currentPage - 1);
  295. container.appendChild(prevBtn);
  296. }
  297.  
  298. const startPage = Math.max(1, currentPage - 2);
  299. const endPage = Math.min(totalPages, currentPage + 2);
  300.  
  301. if (startPage > 1) {
  302. const firstBtn = document.createElement('button');
  303. firstBtn.className = 'userscript-page-btn';
  304. firstBtn.textContent = '1';
  305. firstBtn.onclick = () => onPageChange(1);
  306. container.appendChild(firstBtn);
  307.  
  308. if (startPage > 2) {
  309. const ellipsis = document.createElement('span');
  310. ellipsis.textContent = '...';
  311. ellipsis.style.padding = '0 5px';
  312. container.appendChild(ellipsis);
  313. }
  314. }
  315.  
  316. for (let i = startPage; i <= endPage; i++) {
  317. const btn = document.createElement('button');
  318. btn.className = `userscript-page-btn ${i === currentPage ? 'userscript-active-page' : ''}`;
  319. btn.textContent = i;
  320. btn.onclick = () => onPageChange(i);
  321. container.appendChild(btn);
  322. }
  323.  
  324. if (endPage < totalPages) {
  325. if (endPage < totalPages - 1) {
  326. const ellipsis = document.createElement('span');
  327. ellipsis.textContent = '...';
  328. ellipsis.style.padding = '0 5px';
  329. container.appendChild(ellipsis);
  330. }
  331.  
  332. const lastBtn = document.createElement('button');
  333. lastBtn.className = 'userscript-page-btn';
  334. lastBtn.textContent = totalPages;
  335. lastBtn.onclick = () => onPageChange(totalPages);
  336. container.appendChild(lastBtn);
  337. }
  338.  
  339. if (currentPage < totalPages) {
  340. const nextBtn = document.createElement('button');
  341. nextBtn.className = 'userscript-page-btn';
  342. nextBtn.textContent = '→';
  343. nextBtn.onclick = () => onPageChange(currentPage + 1);
  344. container.appendChild(nextBtn);
  345. }
  346.  
  347. return container;
  348. }
  349.  
  350. function displayData(data) {
  351. document.querySelectorAll('.userscript-container').forEach(el => el.remove());
  352.  
  353. if (data.length === 0) {
  354. showTemporaryMessage('No data collected matching the criteria', 'error');
  355. return;
  356. }
  357.  
  358. const sortedData = [...data].sort((a, b) => b.modifiedTimestamp - a.modifiedTimestamp);
  359. const totalPages = Math.ceil(sortedData.length / ROWS_PER_PAGE);
  360. let currentPage = 1;
  361.  
  362. const container = document.createElement('div');
  363. container.className = 'userscript-container';
  364.  
  365. const title = document.createElement('h2');
  366. title.className = 'userscript-title';
  367. title.textContent = `PENDING TESTS (${sortedData.length} records)`;
  368. container.appendChild(title);
  369.  
  370. const renderPage = (page) => {
  371. const startIdx = (page - 1) * ROWS_PER_PAGE;
  372. const endIdx = startIdx + ROWS_PER_PAGE;
  373. const pageData = sortedData.slice(startIdx, endIdx);
  374.  
  375. const oldTable = container.querySelector('.userscript-table-container');
  376. if (oldTable) oldTable.remove();
  377.  
  378. const tableContainer = document.createElement('div');
  379. tableContainer.className = 'userscript-table-container';
  380.  
  381. const table = document.createElement('table');
  382. table.className = 'userscript-table';
  383. table.setAttribute('role', 'grid');
  384.  
  385. const headers = ['NO.', 'Test Description', 'Barcode', 'Clinic', 'Created', 'Last Modified'];
  386. const thead = document.createElement('thead');
  387. const headerRow = document.createElement('tr');
  388.  
  389. headers.forEach((text, i) => {
  390. const th = document.createElement('th');
  391. th.textContent = text;
  392. headerRow.appendChild(th);
  393. });
  394.  
  395. thead.appendChild(headerRow);
  396. table.appendChild(thead);
  397.  
  398. const tbody = document.createElement('tbody');
  399. pageData.forEach((item, idx) => {
  400. const row = document.createElement('tr');
  401. const absoluteIdx = startIdx + idx + 1;
  402.  
  403. [
  404. absoluteIdx,
  405. item.testDescription,
  406. item.barcode,
  407. truncateText(item.clinic, CLINIC_TRUNCATE_LENGTH),
  408. formatDate(item.creationTimestamp),
  409. formatDate(item.modifiedTimestamp)
  410. ].forEach(content => {
  411. const td = document.createElement('td');
  412. td.textContent = content;
  413. row.appendChild(td);
  414. });
  415.  
  416. tbody.appendChild(row);
  417. });
  418. table.appendChild(tbody);
  419. tableContainer.appendChild(table);
  420.  
  421. const oldPagination = container.querySelector('.userscript-pagination');
  422. if (oldPagination) oldPagination.remove();
  423. tableContainer.appendChild(createPagination(totalPages, page, (newPage) => {
  424. currentPage = newPage;
  425. renderPage(newPage);
  426. }));
  427.  
  428. const buttonContainerElement = container.querySelector('.userscript-button-container');
  429. if (buttonContainerElement) {
  430. container.insertBefore(tableContainer, buttonContainerElement);
  431. } else {
  432. container.appendChild(tableContainer);
  433. }
  434. };
  435.  
  436. let buttonContainer = container.querySelector('.userscript-button-container');
  437. if (!buttonContainer) {
  438. buttonContainer = document.createElement('div');
  439. buttonContainer.className = 'userscript-button-container';
  440. container.appendChild(buttonContainer);
  441. }
  442.  
  443. const printBtn = document.createElement('button');
  444. printBtn.className = 'userscript-button userscript-button-primary';
  445. printBtn.textContent = 'Print All';
  446. printBtn.onclick = () => {
  447. const printWindow = window.open('', '_blank');
  448. printWindow.document.write(`
  449. <html>
  450. <head>
  451. <title>Test Descriptions and Barcodes</title>
  452. <style>
  453. body { font-family: sans-serif; margin: 10mm; }
  454. h2 { margin-top: 0; }
  455. table { border-collapse: collapse; width: 100%; font-size: 10px; margin-bottom: 20px; }
  456. th, td { border: 1px solid #ddd; padding: 3px 5px; }
  457. th { background-color: #f2f2f2; font-weight: bold; }
  458. .page-break { page-break-after: always; }
  459. @page { size: auto; margin: 10mm; }
  460. td {
  461. white-space: nowrap;
  462. overflow: hidden;
  463. text-overflow: ellipsis;
  464. }
  465. </style>
  466. </head>
  467. <body>
  468. <h2>PENDING TESTS (${sortedData.length} records)</h2>
  469. `);
  470.  
  471. for (let i = 0; i < sortedData.length; i += ROWS_PER_PAGE) {
  472. const pageData = sortedData.slice(i, i + ROWS_PER_PAGE);
  473.  
  474. printWindow.document.write(`
  475. <table>
  476. <thead>
  477. <tr>
  478. <th>NO.</th>
  479. <th>Test Description</th>
  480. <th>Barcode</th>
  481. <th>Clinic</th>
  482. <th>Created</th>
  483. <th>Last Modified</th>
  484. </tr>
  485. </thead>
  486. <tbody>
  487. `);
  488.  
  489. pageData.forEach((item, idx) => {
  490. printWindow.document.write(`
  491. <tr>
  492. <td>${i + idx + 1}</td>
  493. <td>${item.testDescription}</td>
  494. <td>${item.barcode}</td>
  495. <td>${truncateText(item.clinic, CLINIC_TRUNCATE_LENGTH)}</td>
  496. <td>${formatDate(item.creationTimestamp)}</td>
  497. <td>${formatDate(item.modifiedTimestamp)}</td>
  498. </tr>
  499. `);
  500. });
  501.  
  502. printWindow.document.write('</tbody></table>');
  503.  
  504. if (i + ROWS_PER_PAGE < sortedData.length) {
  505. printWindow.document.write('<div class="page-break"></div>');
  506. }
  507. }
  508.  
  509. printWindow.document.write(`
  510. <script>
  511. setTimeout(function() {
  512. window.print();
  513. window.close();
  514. }, 200);
  515. </script>
  516. </body>
  517. </html>
  518. `);
  519. printWindow.document.close();
  520. };
  521. buttonContainer.appendChild(printBtn);
  522.  
  523. const downloadBtn = document.createElement('button');
  524. downloadBtn.className = 'userscript-button userscript-button-primary';
  525. downloadBtn.textContent = 'Download CSV';
  526. downloadBtn.onclick = () => {
  527. const headers = ['NO.', 'Test Description', 'Barcode', 'Clinic', 'Created', 'Last Modified'];
  528. let csv = headers.map(escapeCsv).join(',') + '\n';
  529.  
  530. sortedData.forEach((item, index) => {
  531. csv += [
  532. index + 1,
  533. item.testDescription,
  534. item.barcode,
  535. item.clinic,
  536. formatDate(item.creationTimestamp),
  537. formatDate(item.modifiedTimestamp)
  538. ].map(escapeCsv).join(',') + '\n';
  539. });
  540.  
  541. const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
  542. const url = URL.createObjectURL(blob);
  543. const link = document.createElement('a');
  544. link.href = url;
  545. link.download = `test_data_${new Date().toISOString().slice(0,10)}.csv`;
  546.  
  547. document.body.appendChild(link);
  548. link.click();
  549. document.body.removeChild(link);
  550.  
  551. setTimeout(() => URL.revokeObjectURL(url), 100);
  552. };
  553. buttonContainer.appendChild(downloadBtn);
  554.  
  555. const closeBtn = document.createElement('button');
  556. closeBtn.className = 'userscript-button userscript-button-danger';
  557. closeBtn.textContent = 'Close';
  558. closeBtn.onclick = () => container.remove();
  559. buttonContainer.appendChild(closeBtn);
  560.  
  561. document.body.appendChild(container);
  562.  
  563. renderPage(1);
  564. }
  565.  
  566. async function startExtraction() {
  567. if (gridObserver) {
  568. showTemporaryMessage('Extraction already in progress', 'info');
  569. return;
  570. }
  571.  
  572. collectedDataMap.clear();
  573. activeCreationDateColumn = null;
  574. if (uiElements.extractButton) uiElements.extractButton.style.display = 'none';
  575. if (uiElements.stopButton) uiElements.stopButton.style.display = 'inline-block';
  576.  
  577.  
  578. showTemporaryMessage('Preparing extraction...', 'info');
  579.  
  580. try {
  581. const gridBodyViewport = await waitForElement(agGridBodyViewportSelector, { timeout: 30000 });
  582. if (!gridBodyViewport) {
  583. throw new Error('AG-Grid viewport not found within timeout.');
  584. }
  585.  
  586. await waitForGridData();
  587.  
  588. gridBodyViewport.querySelectorAll(agGridRowSelector).forEach(row => {
  589. row.dataset.processed = 'true';
  590. processRowElement(row);
  591. });
  592.  
  593. if (!uiElements.scrollMessage) {
  594. uiElements.scrollMessage = document.createElement('div');
  595. uiElements.scrollMessage.className = 'userscript-scroll-message';
  596. document.body.appendChild(uiElements.scrollMessage);
  597. }
  598. uiElements.scrollMessage.textContent = `Collected ${collectedDataMap.size} records. Scroll to load more...`;
  599. uiElements.scrollMessage.style.display = 'block';
  600.  
  601. gridObserver = new MutationObserver(() => {
  602. clearTimeout(gridObserver._debounce);
  603. gridObserver._debounce = setTimeout(() => {
  604. const newRows = Array.from(gridBodyViewport.querySelectorAll(agGridRowSelector))
  605. .filter(row => !row.dataset.processed);
  606.  
  607. newRows.forEach(row => {
  608. row.dataset.processed = 'true';
  609. processRowElement(row);
  610. });
  611.  
  612. if (uiElements.scrollMessage) {
  613. uiElements.scrollMessage.textContent = `Collected ${collectedDataMap.size} records`;
  614. }
  615. console.log(`Processed ${newRows.length} new rows. Total collected: ${collectedDataMap.size}`);
  616. }, 150);
  617. });
  618.  
  619. gridObserver.observe(gridBodyViewport, { childList: true, subtree: true });
  620.  
  621. showTemporaryMessage('Extraction started. Scroll down the grid to collect more data.', 'success');
  622.  
  623. } catch (error) {
  624. console.error('Extraction error:', error);
  625. cleanup();
  626. showTemporaryMessage(
  627. error.message.includes('Timeout') ?
  628. 'Grid loading timeout or no rows found.' :
  629. 'Failed to start extraction.',
  630. 'error',
  631. 10000
  632. );
  633. }
  634. }
  635.  
  636. function stopExtractionAndDisplay() {
  637. if (!gridObserver) {
  638. showTemporaryMessage('No active extraction to stop', 'info');
  639. return;
  640. }
  641.  
  642. cleanup();
  643. const finalData = Array.from(collectedDataMap.values());
  644.  
  645. if (finalData.length === 0) {
  646. showTemporaryMessage(
  647. 'No data collected. Possible issues:\n1. No matching columns found.\n2. Grid structure changed.\n3. No rows were loaded during extraction.',
  648. 'error',
  649. 10000
  650. );
  651. return;
  652. }
  653.  
  654. showTemporaryMessage(`Collected ${finalData.length} records. Generating report...`, 'success', 3000);
  655. setTimeout(() => displayData(finalData), 500);
  656. }
  657.  
  658. function addButtonsToPage() {
  659. if (uiElements.extractButton && document.body.contains(uiElements.extractButton)) {
  660. return;
  661. }
  662.  
  663. const resetButton = document.querySelector('.nova-btn.nova-btn--ghost.nova-btn--md[translateid="lab-order-list.Reset"]');
  664.  
  665. if (resetButton && resetButton.parentElement) {
  666. uiElements.extractButton = document.createElement('button');
  667. uiElements.extractButton.textContent = 'PRINT';
  668. uiElements.extractButton.className = 'userscript-button nova-btn nova-btn--primary nova-btn--md';
  669. uiElements.extractButton.style.marginLeft = '10px';
  670. uiElements.extractButton.onclick = startExtraction;
  671.  
  672. uiElements.stopButton = document.createElement('button');
  673. uiElements.stopButton.textContent = 'Stop & Generate Report';
  674. uiElements.stopButton.className = 'userscript-button nova-btn nova-btn--danger nova-btn--md';
  675. uiElements.stopButton.style.marginLeft = '10px';
  676. uiElements.stopButton.style.display = 'none';
  677. uiElements.stopButton.onclick = stopExtractionAndDisplay;
  678.  
  679. resetButton.parentElement.insertBefore(uiElements.extractButton, resetButton.nextSibling);
  680. uiElements.extractButton.parentElement.insertBefore(uiElements.stopButton, uiElements.extractButton.nextSibling);
  681.  
  682. console.log('AG-Grid Extractor buttons added next to Reset button.');
  683.  
  684. } else {
  685. console.log('Reset button not found, skipping adding extractor buttons.');
  686. }
  687. }
  688.  
  689.  
  690. function cleanup() {
  691. if (observerForButtonContainer) {
  692. observerForButtonContainer.disconnect();
  693. observerForButtonContainer = null;
  694. console.log('AG-Grid Extractor button container observer disconnected.');
  695. }
  696.  
  697. if (gridObserver) {
  698. gridObserver.disconnect();
  699. clearTimeout(gridObserver._debounce);
  700. gridObserver = null;
  701. console.log('AG-Grid data observer disconnected.');
  702. }
  703.  
  704. if (uiElements.extractButton && uiElements.extractButton.parentElement) {
  705. uiElements.extractButton.parentElement.removeChild(uiElements.extractButton);
  706. uiElements.extractButton = null;
  707. }
  708. if (uiElements.stopButton && uiElements.stopButton.parentElement) {
  709. uiElements.stopButton.parentElement.removeChild(uiElements.stopButton);
  710. uiElements.stopButton = null;
  711. }
  712. if (uiElements.scrollMessage && uiElements.scrollMessage.parentElement) {
  713. uiElements.scrollMessage.parentElement.removeChild(uiElements.scrollMessage);
  714. uiElements.scrollMessage = null;
  715. }
  716. console.log('AG-Grid Extractor UI elements removed.');
  717. }
  718.  
  719.  
  720. function initialize() {
  721. console.log('AG-Grid Data Extractor script initializing...');
  722.  
  723. addButtonsToPage();
  724.  
  725. const targetNode = document.body;
  726. const config = { childList: true, subtree: true };
  727.  
  728. observerForButtonContainer = new MutationObserver((mutationsList, observer) => {
  729. addButtonsToPage();
  730. });
  731.  
  732. observerForButtonContainer.observe(targetNode, config);
  733. console.log('MutationObserver started to watch for button container.');
  734.  
  735.  
  736. window.addEventListener('beforeunload', cleanup);
  737. }
  738.  
  739. if (document.readyState === 'loading') {
  740. document.addEventListener('DOMContentLoaded', initialize);
  741. } else {
  742. initialize();
  743. }
  744.  
  745. })();