// ==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}`);
}
});
})();