Rapid Receiver & Lab Counter Suite (Network Hub Edition)

A network-enabled Lab Suite with a main-page live worklist. Batches are shared via a central Python Hub server.

// ==UserScript==
// @name         Rapid Receiver & Lab Counter Suite (Network Hub Edition)
// @namespace    Violentmonkey Scripts
// @version      8.6
// @description  A network-enabled Lab Suite with a main-page live worklist. Batches are shared via a central Python Hub server.
// @match        https://his.kaauh.org/lab/*
// @author       Hamad AlShegifi
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      *
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const SCRIPT_PREFIX = "[LAB SUITE V8.3]";
    const logDebug = msg => console.debug(`${SCRIPT_PREFIX} ${msg}`);
    const logError = msg => console.error(`${SCRIPT_PREFIX} ${msg}`);

    // --- ⬇️ IMPORTANT CONFIGURATION ⬇️ ---
    const HUB_SERVER_URL = 'http://REPLACE_WITH_YOUR_HUB_PC_IP:8080';
    // --- ⬆️ IMPORTANT CONFIGURATION ⬆️ ---


    // --- Global state ---
    const collectedData = new Map();
    const processedBarcodes = new Set();
    let autoCollectorInterval = null;
    const liveBatches = new Map();
    let liveBatchPoller = null;

    // --- Injected CSS ---
    GM_addStyle(`
        /* ... All previous styles are unchanged ... */
        @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
        .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); }
        .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; }
        .batch-rerun-container { margin-top: 15px; padding: 10px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; }
        #batchRerunInput { width: 100%; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; border: 1px solid #ccc; border-radius: 4px; }
        .auto-collector-counter { display: inline-flex; align-items: center; gap: 6px; margin-right: 5px; font-weight: 600; color: #6c757d; background-color: #f8f9fa; padding: 6px 12px; border-radius: 6px; border: 1px solid #dee2e6; }
        .auto-collector-counter.has-items { color: #004085; background-color: #cce5ff; border-color: #b8daff; }

        /* --- Styles for Live Worklist (in modal) --- */
        #liveWorklistContainer { border-top: 2px solid #007bff; margin-top: 15px; padding-top: 10px; }
        #liveWorklistContent { max-height: 250px; overflow-y: auto; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 10px; }
        .worklist-batch { margin-bottom: 15px; }
        .worklist-batch h5 { font-size: 1rem; font-weight: 700; color: #0056b3; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid #e9ecef; }
        .worklist-batch ul { list-style-type: none; padding-left: 5px; margin: 0; }
        .worklist-batch li { padding: 4px; font-family: monospace; font-size: 14px; border-radius: 3px; transition: background-color 0.3s; }
        .worklist-batch li.checked { background-color: #d4edda; color: #155724; text-decoration: line-through; }

        /* --- Styles for Main Page Live Batch Viewer --- */
        .live-batch-menu-item a { cursor: pointer; display: block; text-decoration: none; color: inherit; }
        .live-batch-menu-item .icon-holder { font-size: 20px; }
        #mainLiveWorklistPanel {
            display: none; position: fixed; z-index: 10000; top: 50%; left: 50%;
            transform: translate(-50%, -50%); width: 90%; max-width: 400px;
            background-color: #fff; border: 1px solid #ccc; border-radius: 8px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.3); animation: fadeIn 0.3s ease-out;
        }
        .worklist-panel-header {
            display: flex; justify-content: space-between; align-items: center;
            padding: 10px 15px; background-color: #f7f7f7; border-bottom: 1px solid #ddd;
            border-radius: 8px 8px 0 0; cursor: move;
        }
        .worklist-panel-header h3 { margin: 0; font-size: 1.1rem; font-weight: 600; }
        .worklist-panel-body { padding: 15px; max-height: 60vh; overflow-y: auto; }
        .worklist-panel-close {
            font-size: 24px; font-weight: bold; color: #888; cursor: pointer;
            border: none; background: none; padding: 0 5px;
        }
        .worklist-panel-close:hover { color: #000; }
    `);

    // --- Core UI & Helper Functions (RESTORED) ---
    function getColorForString(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
        }
        let color = '#';
        for (let i = 0; i < 3; i++) {
            const value = (hash >> (i * 8)) & 0xFF;
            color += ('00' + value.toString(16)).substr(-2);
        }
        return color;
    }

    function relocateAllDangerAlerts() {
        const alerts = document.querySelectorAll('.modal-body .alert-danger');
        alerts.forEach(alert => {
            const modalBody = alert.closest('.modal-body');
            if (modalBody) modalBody.prepend(alert);
        });
    }

    function createCounterElement(id) {
        return $(`<div id="${id}" class="main-counter-style" style="margin-right: auto;"><span class="counter-icon"></span><span>Total Samples: <span class="section-count-badge">0</span></span></div>`);
    }

    function updateSpecificCounter(modalElementForInputs, counterElement, inputSelector) {
        const inputs = modalElementForInputs.querySelectorAll(inputSelector);
        const sectionCounts = new Map();
        let totalCount = 0;
        inputs.forEach(input => {
            if (input.value.trim() !== '') {
                totalCount++;
                const parentRow = input.closest('tr');
                if (parentRow) {
                    const sectionInput = parentRow.querySelector('input[formcontrolname="TestSection"]');
                    const section = sectionInput ? sectionInput.value.trim() : 'Unknown';
                    sectionCounts.set(section, (sectionCounts.get(section) || 0) + 1);
                }
            }
        });
        counterElement.find('.section-count-badge').first().text(totalCount);
        const existingTags = counterElement.find('.sample-section-tag');
        const displayedSections = new Set();
        existingTags.each(function() {
            const sectionName = $(this).data('section');
            if (sectionCounts.has(sectionName)) {
                $(this).find('.section-count-badge').text(sectionCounts.get(sectionName));
                displayedSections.add(sectionName);
            } else {
                $(this).remove();
            }
        });
        for (const [section, count] of sectionCounts.entries()) {
            if (!displayedSections.has(section)) {
                const color = getColorForString(section);
                counterElement.append(`<div class="sample-section-tag" data-section="${section}"><span class="color-dot" style="background-color: ${color};"></span>${section}: <span class="section-count-badge">${count}</span></div>`);
            }
        }
    }

    function setupModalCounter(modalConfig) {
        const { modalKeyElement, targetFooter, inputSelector, counterId, activeIntervalsMap } = modalConfig;
        if (!targetFooter || modalKeyElement.dataset.counterInitialized) return;
        logDebug(`Setting up counter #${counterId} in modal.`);
        modalKeyElement.dataset.counterInitialized = 'true';
        const counterElement = createCounterElement(counterId);
        $(targetFooter).prepend(counterElement);
        const intervalId = setInterval(() => {
            if (!document.body.contains(modalKeyElement)) {
                clearInterval(intervalId);
                activeIntervalsMap.delete(counterId);
                return;
            }
            updateSpecificCounter(modalKeyElement, counterElement, inputSelector);
        }, 500);
        activeIntervalsMap.set(counterId, intervalId);
    }

    /**
     * Generates a short batch code and saves it to the Hub server.
     * @param {string[]} barcodesArray An array of barcode strings.
     * @param {string} shortPrefix The 2-character prefix for the workbench.
     * @returns {Promise<string|null>}
     */
    function generateAndSaveBatch(barcodesArray, shortPrefix) {
        return new Promise((resolve) => {
            if (HUB_SERVER_URL.includes('REPLACE')) {
                alert("❌ CONFIGURATION ERROR:\nYou must set the HUB_SERVER_URL at the top of the user script.");
                return resolve(null);
            }
            if (!barcodesArray || barcodesArray.length === 0) return resolve(null);

            const shortTimestamp = Math.floor(Date.now() / 1000).toString().slice(-6);
            const shortCode = `${shortPrefix}-${shortTimestamp}-${barcodesArray.length}`;

            GM_xmlhttpRequest({
                method: "POST",
                url: `${HUB_SERVER_URL}/saveBatch`,
                headers: { "Content-Type": "application/json" },
                data: JSON.stringify({ code: shortCode, barcodes: barcodesArray.join('\n') }),
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        logDebug(`✅ Saved batch to Hub server: ${shortCode}`);
                        resolve(shortCode);
                    } else {
                        logError(`Failed to save batch. Status: ${response.status}`);
                        alert(`❌ Failed to save batch. Is the Hub server running at ${HUB_SERVER_URL}?`);
                        resolve(null);
                    }
                },
                onerror: (response) => {
                    logError(`Network error saving batch: ${response.statusText}`);
                    alert(`❌ Could not connect to the Hub server.`);
                    resolve(null);
                }
            });
        });
    }


    // --- Live Worklist Functions ---

    /**
     * Renders the fetched live batches into a specified UI container.
     * @param {string} targetSelector The jQuery selector for the container to render into.
     */
    function renderLiveWorklist(targetSelector) {
        const container = $(targetSelector);
        if (container.length === 0) return;

        container.empty();

        if (liveBatches.size === 0) {
            container.html("<i>No recent batches found. Generate a batch to see it here.</i>");
            return;
        }

        const sortedBatches = new Map([...liveBatches.entries()].sort((a, b) => b[0].localeCompare(a[0])));

        for (const [batchCode, data] of sortedBatches.entries()) {
            const { barcodes, checked } = data;
            const isComplete = checked.size === barcodes.size && barcodes.size > 0;
            const batchDiv = $('<div class="worklist-batch"></div>');
            const title = $(`<h5>${batchCode} (${checked.size} / ${barcodes.size})</h5>`);
            if (isComplete) title.css('color', '#28a745');
            batchDiv.append(title);

            const ul = $('<ul></ul>');
            const sortedBarcodes = Array.from(barcodes).sort();

            sortedBarcodes.forEach(barcode => {
                const li = $(`<li data-barcode="${barcode}"></li>`);
                if (checked.has(barcode)) {
                    li.addClass('checked').html(`✔️ ${barcode}`);
                } else {
                    li.text(barcode);
                }
                ul.append(li);
            });

            batchDiv.append(ul);
            container.append(batchDiv);
        }
    }

    /**
     * Checks a scanned barcode against the live worklists.
     * @param {Event} event The keydown event from the input field.
     */
    function handleWorklistMatching(event) {
        if (event.key !== 'Enter') return;
        const barcode = event.target.value.trim();
        if (!barcode) return;

        if (Array.from(liveBatches.values()).some(data => {
            if (data.barcodes.has(barcode)) {
                data.checked.add(barcode);
                return true;
            }
            return false;
        })) {
            logDebug(`Matched barcode "${barcode}" to a live worklist.`);
            renderLiveWorklist('#liveWorklistContent');
            renderLiveWorklist('#mainLiveWorklistPanel .worklist-panel-body');
        }
    }


    /**
     * Starts polling the Hub server for recent batches.
     */
    function startLiveBatchPolling() {
        if (liveBatchPoller) return;
        logDebug("Starting live batch polling...");

        const poll = () => {
             if (HUB_SERVER_URL.includes('REPLACE')) {
                 logError("Polling skipped: HUB_SERVER_URL is not configured.");
                 return;
             }

            GM_xmlhttpRequest({
                method: "GET",
                url: `${HUB_SERVER_URL}/getRecentBatches`,
                onload: function(response) {
                    if (response.status === 200) {
                        const fetchedBatches = JSON.parse(response.responseText);
                        logDebug(`Successfully polled server. Received ${Object.keys(fetchedBatches).length} batches.`);
                        let updated = false;
                        const fetchedKeys = new Set(Object.keys(fetchedBatches));
                        const localKeys = new Set(liveBatches.keys());

                        if (fetchedKeys.size !== localKeys.size || ![...fetchedKeys].every(k => localKeys.has(k))) {
                            updated = true;
                        }

                        for (const batchCode in fetchedBatches) {
                            const serverBatch = fetchedBatches[batchCode];
                            const serverBarcodes = new Set(serverBatch.barcodes.split('\n').filter(b => b));
                            if (!liveBatches.has(batchCode)) {
                                liveBatches.set(batchCode, { barcodes: serverBarcodes, checked: new Set() });
                                updated = true;
                            } else {
                                if (liveBatches.get(batchCode).barcodes.size !== serverBarcodes.size) {
                                    liveBatches.get(batchCode).barcodes = serverBarcodes;
                                    updated = true;
                                }
                            }
                        }

                        for (const localKey of localKeys) {
                            if (!fetchedKeys.has(localKey)) {
                                liveBatches.delete(localKey);
                                updated = true;
                            }
                        }

                        if(updated) {
                           logDebug(`Live worklist updated. Total batches: ${liveBatches.size}`);
                           renderLiveWorklist('#liveWorklistContent');
                           renderLiveWorklist('#mainLiveWorklistPanel .worklist-panel-body');
                        }
                    } else {
                        logError(`Polling failed. Server responded with status: ${response.status}`);
                    }
                },
                onerror: (response) => logError(`Polling network error. Could not connect to Hub server. Details: ${response.statusText}`)
            });
        };

        poll();
        liveBatchPoller = setInterval(poll, 60000); // Poll every 60 seconds
    }


    // --- Main Setup Functions ---

    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');
        const originalBarcodes = barcodesText.split(/\r?\n/).filter(line => line.trim() !== '');
        const rerunCode = await generateAndSaveBatch(originalBarcodes, "RR");
        if (rerunCode) {
            prompt("Batch processed! Copy this code to re-run this list later:", rerunCode);
        }
    }

    function setupRapidReceiver() {
        const closeButtonSelector = "#closebtn-smplrecieve, #btnclose-smplcollection";
        const closeButton = document.querySelector(closeButtonSelector);
        if (!closeButton || closeButton.parentElement.querySelector('#rapidReceiveBtn')) return;
        let rapidReceiveBtn = $('<button type="button" class="btn btn-color-1" id="rapidReceiveBtn">Rapid Receiver</button>').css('margin-right', '5px');
        $(closeButton).before(rapidReceiveBtn);
        if ($('#rapidReceiveModal').length === 0) {
            logDebug("Creating Rapid Receiver modal.");
            $('body').append(`
                <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">&times;</span></div>
                        <div class="rr-modal-body"><p>Enter each barcode on a new line. To re-run a previous batch, use the 'Batch Re-run' field on the main page.</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>`);
            $('.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 count = $(this).val().trim() ? $(this).val().trim().split(/\r?\n/).filter(line => line.trim() !== '').length : 0;
                $('#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');
        });
    }

    function createBatchInputField() {
        const mainInputContainer = $('#barcodecollection').closest('.form-group');
        if (mainInputContainer.length === 0 || $('#batchRerunInput').length > 0) return;
        logDebug("Creating dedicated batch re-run input field.");
        mainInputContainer.after(`
            <div class="form-group batch-rerun-container">
                <label for="batchRerunInput">Batch Re-run Code</label>
                <input type="text" id="batchRerunInput" class="form-control" placeholder="Enter Batch Code (e.g., CH-123456-1) and Press Enter">
            </div>`);
    }

    function createBatchUI() {
        const closeButton = $("#closebtn-smplrecieve, #btnclose-smplcollection");
        if (closeButton.length === 0 || $('#generateBatchBtn').length > 0) return;
        logDebug("Creating 'Generate Batch' UI elements.");
        const batchBtn = $('<button type="button" class="btn btn-info" id="generateBatchBtn">Generate Batch</button>').css('margin-right', '5px');
        const batchCounter = $(`<span id="collectedCounter" class="auto-collector-counter"><span class="counter-icon"></span> 0 Collected</span>`);
        closeButton.before(batchBtn);
        batchBtn.before(batchCounter);
        batchBtn.on('click', async () => {
            if (collectedData.size === 0) return alert("No barcodes have been collected to generate a batch.");
            const batchResults = [];
            for (const [workbench, barcodeSet] of collectedData.entries()) {
                const barcodesArray = Array.from(barcodeSet);
                const shortPrefix = workbench.replace(/[^a-zA-Z0-9]/g, '').substring(0, 2).toUpperCase();
                const code = await generateAndSaveBatch(barcodesArray, shortPrefix);
                if (code) batchResults.push({ workbench, code });
            }
            if (batchResults.length > 0) {
                let promptMessage = "Batches generated! Copy your codes:\n\n";
                batchResults.forEach(result => { promptMessage += `${result.workbench}: ${result.code}\n`; });
                prompt(promptMessage);
            } else {
                alert("Could not save any re-run codes. Please check the Hub server connection.");
            }
            collectedData.clear();
            processedBarcodes.clear();
            $('#collectedCounter').html('<span class="counter-icon"></span> 0 Collected').removeClass('has-items');
        });
    }

    function setupRerunListener() {
        const shortCodeRegex = /^[A-Z]{2}-\d{6}-\d+$/i;
        $(document).on('keydown', '#batchRerunInput', function(e) {
            if (e.key === 'Enter') {
                e.preventDefault();
                const potentialCode = $(this).val().trim();
                if (!shortCodeRegex.test(potentialCode)) return alert(`"${potentialCode}" is not a valid batch code format.`);
                logDebug(`Attempting to retrieve code from Hub server: ${potentialCode}`);
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `${HUB_SERVER_URL}/getBatch/${potentialCode}`,
                    onload: (response) => {
                        if (response.status === 200) {
                            logDebug("✅ Found matching barcode list on Hub server.");
                            const data = JSON.parse(response.responseText);
                            $('#rapidReceiveBtn').trigger('click');
                            $('#barcodeList').val(data.barcodes);
                            $('#barcodeList').trigger('input');
                            $(this).val('');
                        } else {
                            logError(`Re-run code "${potentialCode}" not found on server.`);
                            alert('Re-run code not found.');
                        }
                    },
                    onerror: () => {
                        logError(`Network error while trying to retrieve batch.`);
                        alert(`❌ Could not connect to the Hub server to retrieve the batch.`);
                    }
                });
            }
        });
    }

    function startAutoCollectorPolling() {
        if (autoCollectorInterval) return;
        logDebug("Starting auto-collector polling for table data.");
        autoCollectorInterval = setInterval(() => {
            const tableRows = document.querySelectorAll('.tubes-types-ro tbody tr');
            if (tableRows.length === 0) return;
            let updated = false;
            tableRows.forEach(row => {
                const barcodeInput = row.querySelector('input[formcontrolname="Barcode"]');
                const workbenchInput = row.querySelector('input[formcontrolname="TestSection"]');
                if (barcodeInput && workbenchInput) {
                    const barcodeValue = barcodeInput.value;
                    if (barcodeValue && !processedBarcodes.has(barcodeValue)) {
                        const workbenchValue = workbenchInput.value || 'UNKNOWN';
                        processedBarcodes.add(barcodeValue);
                        if (!collectedData.has(workbenchValue)) collectedData.set(workbenchValue, new Set());
                        collectedData.get(workbenchValue).add(barcodeValue);
                        updated = true;
                    }
                }
            });
            if (updated) {
                let totalCount = 0;
                for (const barcodeSet of collectedData.values()) totalCount += barcodeSet.size;
                logDebug(`Data collected. Total unique barcodes: ${totalCount}`);
                $('#collectedCounter').html(`<span class="counter-icon"></span> ${totalCount} Collected`).addClass('has-items');
            }
        }, 500);
    }

    function setupMainMenuFeatures() {
        if ($('#mainLiveWorklistPanel').length === 0) {
            $('body').append(`
                <div id="mainLiveWorklistPanel">
                    <div class="worklist-panel-header"><h3>Live Worklist</h3><button class="worklist-panel-close">&times;</button></div>
                    <div class="worklist-panel-body"><i>Connecting...</i></div>
                </div>`);
            $('.worklist-panel-close').on('click', () => $('#mainLiveWorklistPanel').hide());
            let isDragging = false, offset = { x: 0, y: 0 };
            const panel = $('#mainLiveWorklistPanel'), header = panel.find('.worklist-panel-header');
            header.on('mousedown', function(e) {
                isDragging = true;
                const panelOffset = panel.offset();
                offset.x = e.clientX - panelOffset.left;
                offset.y = e.clientY - panelOffset.top;
                header.css('cursor', 'grabbing');
            });
            $(document).on('mousemove', function(e) {
                if (!isDragging) return;
                panel.css({ top: e.clientY - offset.y, left: e.clientX - offset.x, transform: 'none' });
            }).on('mouseup', () => {
                isDragging = false;
                header.css('cursor', 'move');
            });
        }
        const docMenuItem = $('span.csi-menu-text[title="Documents"]').closest('csi-main-menu').parent();
        if (docMenuItem.length > 0 && docMenuItem.parent().find('.live-batch-menu-item').length === 0) {
            logDebug("Injecting 'Live Batches' menu item.");
            const menuItem = $(`
                <div class="live-batch-menu-item">
                    <csi-main-menu><a>
                        <span class="icon-holder csi-menu-icon"><i class="nova-icon-lab-flask-2"></i></span>
                        <div class="csi-menu-text-wrapper"><span class="csi-menu-text sidemenu-title" title="Live Batches">Live Worklist</span></div>
                    </a></csi-main-menu>
                </div>`);
            docMenuItem.after(menuItem);
            menuItem.on('click', () => {
                const panel = $('#mainLiveWorklistPanel');
                panel.is(':visible') ? panel.hide() : (renderLiveWorklist('#mainLiveWorklistPanel .worklist-panel-body'), panel.show());
            });
        } else if (docMenuItem.length === 0) {
            logError("Could not find the 'Documents' menu item to inject the Live Batches button.");
        }
    }


    function setupDOMObserver() {
        logDebug("DOM observer started.");
        const observer = new MutationObserver(() => {
            setupMainMenuFeatures();
            const modalSelector = '.modal-body app-sample-receive, .modal-body app-sample-collection';
            const targetModal = document.querySelector(modalSelector);
            if (targetModal && !targetModal.dataset.suiteInitialized) {
                logDebug("Detected a sample modal. Initializing suite features...");
                targetModal.dataset.suiteInitialized = 'true';
                $(targetModal).append(`
                    <div id="liveWorklistContainer">
                        <h4>Live Worklist</h4>
                        <div id="liveWorklistContent">Connecting to Hub server...</div>
                    </div>`);
                const barcodeInput = targetModal.querySelector('input[formcontrolname="Barcode"], #barcodecollection');
                if (barcodeInput) {
                    barcodeInput.addEventListener('keydown', handleWorklistMatching);
                    logDebug("Attached live worklist matcher to barcode input.");
                }
                renderLiveWorklist('#liveWorklistContent');
            }
            setupRapidReceiver();
            createBatchUI();
            if (document.querySelector('.tubes-types-ro')) startAutoCollectorPolling();
            if (document.querySelector('#barcodecollection')) createBatchInputField();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // --- Script Initialization ---
    window.addEventListener('load', () => {
        logDebug("Initializing Lab Suite...");
        setupDOMObserver();
        setupRerunListener();
        startLiveBatchPolling();
    });

})();