KAAUH Lab PatientID TD Counter (Inline Colorful) - Extended

Flicker-Free: Counts samples in multiple modals with a primary, enlarged counter and section breakdown.

As of 2025-06-23. See the latest version.

// ==UserScript==
// @name         KAAUH Lab PatientID TD Counter (Inline Colorful) - Extended
// @namespace    Violentmonkey Scripts
// @version      2.7.2
// @description  Flicker-Free: Counts samples in multiple modals with a primary, enlarged counter and section breakdown.
// @match        https://his.kaauh.org/lab/*
// @author       Hamad AlShegifi & Gemini
// @grant        GM_addStyle
// ==/UserScript==

(function () {
    'use strict';

    const SCRIPT_PREFIX = "[SAMPLES COUNT V2.7.1]";
    const logSampleCountDebug = msg => console.debug(`${SCRIPT_PREFIX} ${msg}`);
    const logSampleCountError = msg => console.error(`${SCRIPT_PREFIX} ${msg}`);

    // --- Injected CSS ---
    GM_addStyle(`
        @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 { /* The rim */
            content: '';
            position: absolute;
            top: -2px;
            left: -2px;
            width: 14px;
            height: 2.5px;
            background-color: currentColor;
            border-radius: 1px;
        }

        /* A separate, robust style for the main counter tag - UPDATED COLORS */
        .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;
            /* Modern gradient background */
            background: linear-gradient(145deg, #0072d3, #0088f8);
            color: #ffffff; /* White text for contrast */
            font-size: 17px;
            font-weight: 600;
            padding: 6px 8px 6px 14px;
            border: 1px solid rgba(255, 255, 255, 0.2); /* Subtle inner border */
            text-shadow: 0 1px 1px rgba(0,0,0,0.1); /* Slight text shadow for depth */
        }

        /* This style is for the smaller section tags */
        .sample-section-tag {
            display: inline-flex; align-items: center; background-color: #f1f3f5;
            border-radius: 16px; padding: 5px 6px 5px 10px; font-size: 14px;
            font-weight: 600; color: #5d6873; border: 1px solid #dee2e6;
            gap: 8px; animation: fadeIn 0.3s ease-out;
            box-shadow: 0 1px 2px rgba(0,0,0,0.05);
        }

        /* Common styles for elements within the tags */
        .color-dot {
            width: 10px; height: 10px; border-radius: 50%;
        }
        .section-count-badge {
            background-color: #495057; color: #fff; border-radius: 10px;
            padding: 4px 9px; font-size: 15px; font-weight: 700;
            min-width: 18px; text-align: center;
        }

        /* Style adjustment for the badge inside the NEW main counter */
        .main-counter-style .section-count-badge {
            background-color: rgba(0, 0, 0, 0.2); /* Darker, semi-transparent background */
            color: #fff;
            box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
        }
    `);

    function getColorForString(str) {
        let hash = 0;
        if (str.length === 0) return 'hsl(0, 0%, 50%)';
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
            hash = hash & hash;
        }
        const hue = Math.abs(hash % 360);
        return `hsl(${hue}, 70%, 48%)`;
    }

    function createCounterElement(id) {
        const wrapper = document.createElement('div');
        wrapper.id = id;
        Object.assign(wrapper.style, {
            display: 'flex', alignItems: 'center', gap: '12px',
            width: '100%', padding: '5px 10px', marginRight: 'auto',
            animation: 'fadeIn 0.5s ease-out'
        });

        // The main counter gets a unique ID and its own style class
        const mainCountWrapper = document.createElement('div');
        mainCountWrapper.id = 'main-counter-tag-' + id; // Make ID unique per instance
        mainCountWrapper.className = 'main-counter-style';

        mainCountWrapper.innerHTML = `
            <span class="counter-icon"></span>
            <span>SAMPLES COUNT=</span>
            <span class="section-count-badge">0</span>`;
        wrapper.appendChild(mainCountWrapper);

        const sectionPanel = document.createElement('div');
        sectionPanel.id = 'section-panel-' + id; // Make ID unique per instance
        Object.assign(sectionPanel.style, {
            display: 'flex', flex: '1', alignItems: 'center',
            flexWrap: 'wrap', gap: '8px',
        });
        wrapper.appendChild(sectionPanel);

        return wrapper;
    }

    function updateSpecificCounter(modalElementForInputs, counterElement, inputSelector) {
        const mainCountBadge = counterElement.querySelector('.main-counter-style .section-count-badge');
        if (!mainCountBadge || !modalElementForInputs || !document.body.contains(modalElementForInputs)) return;

        const inputs = modalElementForInputs.querySelectorAll(inputSelector);
        const count = inputs.length;
        mainCountBadge.textContent = count;

        const sectionPanel = counterElement.querySelector('[id^="section-panel-"]');
        if (!sectionPanel) return;

        const sectionCounts = {};
        inputs.forEach(input => {
            const sectionInput = input.closest('tr')?.querySelector('input[formcontrolname="TestSection"]');
            if (sectionInput?.value) {
                const sectionName = sectionInput.value.trim().toUpperCase();
                if (sectionName) {
                    sectionCounts[sectionName] = (sectionCounts[sectionName] || 0) + 1;
                }
            }
        });

        const sectionsOnPage = new Set();
        const sortedSections = Object.keys(sectionCounts).sort();

        // Update existing tags or create new ones
        for (const section of sortedSections) {
            sectionsOnPage.add(section);
            let tag = sectionPanel.querySelector(`.sample-section-tag[data-section="${section}"]`);
            if (tag) {
                tag.querySelector('.section-count-badge').textContent = sectionCounts[section];
            } else {
                tag = document.createElement('div');
                tag.className = 'sample-section-tag';
                tag.dataset.section = section;
                tag.innerHTML = `
                    <span class="color-dot" style="background-color: ${getColorForString(section)};"></span>
                    <span>${section}</span>
                    <span class="section-count-badge">${sectionCounts[section]}</span>`;
                sectionPanel.appendChild(tag);
            }
        }

        // Remove old tags that are no longer in the data
        sectionPanel.querySelectorAll('.sample-section-tag[data-section]').forEach(tag => {
            if (!sectionsOnPage.has(tag.dataset.section)) {
                tag.remove();
            }
        });
    }

    function setupModalCounter(modalConfig) {
        const { modalKeyElement, targetFooter, counterId, inputSelector, activeIntervalsMap } = modalConfig;
        if (targetFooter.querySelector('#' + counterId) || modalKeyElement.dataset.counterInitialized) return;

        logSampleCountDebug(`Creating counter for modal: ${counterId}`);
        const counter = createCounterElement(counterId);
        targetFooter.insertBefore(counter, targetFooter.firstChild);
        targetFooter.style.flexWrap = 'wrap';

        const modalElementForInputs = modalKeyElement.querySelector('.modal-body') || modalKeyElement;

        const interval = setInterval(() => {
            updateSpecificCounter(modalElementForInputs, counter, inputSelector);
        }, 500);

        activeIntervalsMap.set(modalKeyElement, { interval, counter });
        modalKeyElement.dataset.counterInitialized = 'true';
    }

    function observeModals() {
        const activeModalIntervals = new Map();
        logSampleCountDebug("Modal observer started.");

        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                // When nodes are ADDED to the page - MERGED LOGIC
                if (mutation.addedNodes.length) {
                    const modalSelectors = {
                        'button#btnclose-smplcollection': {
                            counterId: 'inline-counter-smplcollection',
                            inputSelector: 'tbody[formarrayname="TubeTypeList"] input[formcontrolname="PatientID"]',
                        },
                        'button#closebtn-smplrecieve': {
                            counterId: 'inline-counter-smplrecieve',
                            inputSelector: 'td input[formcontrolname="PatientID"]',
                        }
                    };

                    for (const btnSelector in modalSelectors) {
                        const button = document.querySelector(btnSelector);
                        if (button) {
                            const modalKeyElement = button.closest('.modal');
                            if (modalKeyElement && !modalKeyElement.dataset.counterInitialized) {
                                setupModalCounter({
                                    ...modalSelectors[btnSelector],
                                    modalKeyElement: modalKeyElement,
                                    targetFooter: button.closest('.modal-footer'),
                                    activeIntervalsMap: activeModalIntervals
                                });
                            }
                        }
                    }
                }

                // When nodes are REMOVED from the page (for reliable cleanup)
                if (mutation.removedNodes.length) {
                    mutation.removedNodes.forEach(removedNode => {
                        if (removedNode.nodeType === 1 && activeModalIntervals.has(removedNode)) {
                            logSampleCountDebug("Tracked modal removed. Cleaning up.");
                            const { interval, counter } = activeModalIntervals.get(removedNode);
                            clearInterval(interval);
                            if (counter) counter.remove();
                            activeModalIntervals.delete(removedNode);
                        }
                    });
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    window.addEventListener('load', () => {
        try {
            observeModals();
        } catch (e) {
            logSampleCountError(`Initialization failed: ${e.message}`);
        }
    });

})();