Down Jasper XML Tool

Tải jasper/xml tự động

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Down Jasper XML Tool
// @namespace    http://vnpt-his/
// @version      1.0
// @description  Tải jasper/xml tự động
// @author       khoanm.vtu
// @match        *://*/vnpthis/main/manager.jsp*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=vncare.vn
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const TOOL_ID = 'jasper-upl-tool';
    const PANEL_STATE_KEY = 'jasper-upl-tool-collapsed';
    const TARGET_PATH = '/vnpthis/main/manager.jsp?func=../danhmuc/UTILS_UPL_TLEE';
    const PENDING_FILE_KEY = 'jasper-upl-pending-file';

    function isUtilsUplPage() {
        return /func=\.\.\/danhmuc\/UTILS_UPL_TLEE/i.test(window.location.href);
    }

    function normalize(text) {
        return String(text || '').trim();
    }

    function debounce(fn, ms) {
        let timeoutId;
        return (...args) => {
            clearTimeout(timeoutId);
            timeoutId = setTimeout(() => fn.apply(null, args), ms);
        };
    }

    function getTargetUrl(rawInput) {
        const base = `${window.location.origin}${TARGET_PATH}`;
        const val = normalize(rawInput);
        return `${base}#${encodeURIComponent(val)}`;
    }

    function getHashText() {
        const raw = String(window.location.hash || '').replace(/^#/, '').trim();
        if (!raw) return '';
        try {
            return decodeURIComponent(raw).trim();
        } catch (e) {
            return raw;
        }
    }

    function savePendingFileText(text) {
        const val = normalize(text);
        if (!val) return;
        localStorage.setItem(PENDING_FILE_KEY, val);
    }

    function getPendingFileText() {
        const fromHash = getHashText();
        if (fromHash) return fromHash;

        return normalize(localStorage.getItem(PENDING_FILE_KEY) || '');
    }

    function clearDoneCache() {
        localStorage.removeItem(PENDING_FILE_KEY);
        localStorage.removeItem(PANEL_STATE_KEY);
    }

    function pickXmlValue(selectEl) {
        if (!selectEl) return '';
        const options = Array.from(selectEl.options || []);
        if (options.some((opt) => opt.value === 'EMRXML')) return 'EMRXML';

        const xmlOpt = options.find((opt) => /xml/i.test(String(opt.value || '')) || /xml/i.test(String(opt.text || '')));
        return xmlOpt ? String(xmlOpt.value || '') : '';
    }

    function findElementByIdDeep(id, rootDoc = document) {
        if (!id || !rootDoc) return null;

        const direct = rootDoc.getElementById(id);
        if (direct) return direct;

        const frames = Array.from(rootDoc.querySelectorAll('iframe, frame'));
        for (const frame of frames) {
            try {
                const childDoc = frame.contentDocument || frame.contentWindow?.document;
                if (!childDoc) continue;
                const nested = findElementByIdDeep(id, childDoc);
                if (nested) return nested;
            } catch (e) {
                // ignore cross-origin frame
            }
        }

        return null;
    }

    function dispatchValueEvents(el) {
        if (!el) return;
        const view = el.ownerDocument?.defaultView || window;
        el.dispatchEvent(new view.Event('input', { bubbles: true }));
        el.dispatchEvent(new view.KeyboardEvent('keyup', { key: 'a', bubbles: true }));
        el.dispatchEvent(new view.KeyboardEvent('keydown', { key: 'a', bubbles: true }));
        el.dispatchEvent(new view.Event('change', { bubbles: true }));
    }

    function findFileInputDeep(preferredId) {
        if (preferredId) {
            const preferred = findElementByIdDeep(preferredId);
            if (preferred) return preferred;
        }

        return (
            findElementByIdDeep('fileDownload') ||
            findElementByIdDeep('fileXMLDownload')
        );
    }

    function fillFileNameInput(fileText, preferredId, attempt = 0) {
        const inputEl = findFileInputDeep(preferredId);
        if (!inputEl) {
            if (attempt < 30) {
                setTimeout(() => fillFileNameInput(fileText, preferredId, attempt + 1), 300);
            }
            return;
        }

        let val = String(fileText || '').trim();
        const isXmlInput = String(inputEl.id || '').toLowerCase() === 'filexmldownload';
        if (isXmlInput && val && !/\.xml$/i.test(val)) {
            val = `${val}.xml`;
        }
        const view = inputEl.ownerDocument?.defaultView || window;
        const valueSetter = Object.getOwnPropertyDescriptor(view.HTMLInputElement.prototype, 'value')?.set;
        const setValue = (nextValue) => {
            if (valueSetter) {
                valueSetter.call(inputEl, nextValue);
            } else {
                inputEl.value = nextValue;
            }
        };

        inputEl.focus();
        setValue(val);
        dispatchValueEvents(inputEl);
        inputEl.blur();
    }

    function clearUrlHash() {
        if (window.location.hash) {
            history.replaceState(null, null, window.location.pathname + window.location.search);
        }
    }

    function clickDownloadBtn(preferredIds, attempt = 0) {
        const ids = Array.isArray(preferredIds) ? preferredIds : [preferredIds];
        let btn = null;
        for (const id of ids) {
            btn = findElementByIdDeep(id);
            if (btn) break;
        }

        if (!btn) {
            if (attempt < 30) {
                setTimeout(() => clickDownloadBtn(ids, attempt + 1), 300);
            }
            return;
        }

        if (btn.disabled) {
            if (attempt < 30) {
                setTimeout(() => clickDownloadBtn(ids, attempt + 1), 250);
            }
            return;
        }

        btn.click();
        // Xóa hash và localStorage NGAY LẬP TỨC sau khi click, tránh lặp lại khi reload
        setTimeout(() => {
            clearUrlHash();
            clearDoneCache();
        }, 200);
    }

    function getAndClearPendingFileText() {
        const fromHash = getHashText();
        let fileText = '';
        if (fromHash) fileText = fromHash;
        else fileText = normalize(localStorage.getItem(PENDING_FILE_KEY) || '');

        // Kiểm tra file vừa xử lý trong sessionStorage để tránh lặp khi reload
        const lastFile = sessionStorage.getItem('jasper-upl-last-file') || '';
        if (fileText && fileText === lastFile) {
            // Đã xử lý file này trong phiên, không làm lại nữa
            clearUrlHash();
            localStorage.removeItem(PENDING_FILE_KEY);
            return '';
        }

        // Lưu lại file vừa xử lý vào sessionStorage
        if (fileText) {
            sessionStorage.setItem('jasper-upl-last-file', fileText);
        }
        // Xóa hash và localStorage NGAY LẬP TỨC
        clearUrlHash();
        localStorage.removeItem(PENDING_FILE_KEY);
        return fileText;
    }

    function selectPathChooseByHash(attempt = 0) {
        if (!isUtilsUplPage()) return;

        // Lấy fileText từ hash/localStorage, xóa ngay để tránh lặp khi reload
        const fileText = getAndClearPendingFileText();
        if (!fileText) return;

        const selectEl = findElementByIdDeep('cboPATH_CHOOSE');
        if (!selectEl) {
            if (attempt < 30) {
                setTimeout(() => selectPathChooseByHash(attempt + 1), 300);
            }
            return;
        }

        const isJasper = /\.jasper$/i.test(fileText);
        const desiredValue = isJasper ? 'JASPER' : pickXmlValue(selectEl);
        if (!desiredValue) return;

        selectEl.value = desiredValue;
        dispatchValueEvents(selectEl);

        const preferredInputId = isJasper ? 'fileDownload' : 'fileXMLDownload';
        setTimeout(() => fillFileNameInput(fileText, preferredInputId), 120);
        if (isJasper) {
            setTimeout(() => clickDownloadBtn(['downloadBtn']), 450);
        } else {
            setTimeout(() => clickDownloadBtn(['downloadXMLBtn', 'downloadBtn']), 450);
        }
    }

    function placePanel(panel) {
        if (!panel) return;
        const PREDEFINED_ORDER = [
            'nv-import-panel',
            'jasper-upl-tool',
            'rpt-id-finder-tool',
            'jasper-upl-telegram-tool',
            'tool-phieuin-network',
            'nv-get-sql-tool',
        ];

        let currentTop = 12;
        for (const id of PREDEFINED_ORDER) {
            const el = document.getElementById(id);
            if (!el) continue;
            if (el === panel) break;

            const style = window.getComputedStyle(el);
            if (style.display === 'none' || style.visibility === 'hidden') continue;

            const rect = el.getBoundingClientRect();
            if (rect.height > 0) {
                currentTop += Math.round(rect.height + 8);
            }
        }

        const vh = window.innerHeight || document.documentElement.clientHeight || 0;
        const panelRect = panel.getBoundingClientRect();
        const panelHeight = Math.max(40, Math.round(panelRect.height || 100));
        const maxTop = Math.max(12, vh - panelHeight - 12);

        panel.style.top = `${Math.min(currentTop, maxTop)}px`;
        panel.style.right = '12px';
        panel.style.left = 'auto';
    }

    function createTool() {
        if (document.getElementById(TOOL_ID)) return;

        const style = document.createElement('style');
        style.textContent = `
            #${TOOL_ID} {
                position: fixed;
                top: 12px;
                right: 12px;
                width: 320px;
                z-index: 999999;
                background: #fff;
                border: 1px solid #c9c9c9;
                border-radius: 8px;
                box-shadow: 0 8px 24px rgba(0,0,0,.15);
                font-family: Arial, sans-serif;
                font-size: 13px;
                padding: 10px;
                transition: width 0.18s ease, padding 0.18s ease;
                overflow: hidden;
            }
            #${TOOL_ID}.collapsed {
                width: 42px;
                padding: 10px 6px;
                max-height: 48px;
            }
            #${TOOL_ID} .jasper-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                font-size: 14px;
                font-weight: 700;
                margin-bottom: 8px;
                gap: 8px;
            }
            #${TOOL_ID} .jasper-toggle {
                width: 28px;
                min-width: 28px;
                height: 28px;
                padding: 0;
                line-height: 1;
                border: 1px solid #c9c9c9;
                border-radius: 6px;
                background: #f7f7f7;
                cursor: pointer;
                font-weight: 700;
            }
            #${TOOL_ID} .jasper-body {
                display: block;
            }
            #${TOOL_ID}.collapsed .jasper-body {
                display: none;
            }
            #${TOOL_ID}.collapsed .jasper-header {
                justify-content: center;
                margin-bottom: 0;
            }
            #${TOOL_ID}.collapsed .jasper-title {
                display: none;
            }
            #${TOOL_ID}.collapsed .jasper-toggle {
                width: 44px;
                min-width: 44px;
                font-size: 11px;
            }
            #${TOOL_ID} .jasper-input,
            #${TOOL_ID} .jasper-find {
                width: 100%;
                box-sizing: border-box;
                padding: 7px;
                font-size: 12px;
            }
            #${TOOL_ID} .jasper-input {
                border: 1px solid #c9c9c9;
                border-radius: 6px;
            }
            #${TOOL_ID} .jasper-find {
                margin-top: 8px;
                border: none;
                border-radius: 6px;
                background: #4b5563;
                color: #fff;
                cursor: pointer;
                font-weight: 700;
            }
            #${TOOL_ID} .jasper-status {
                margin-top: 8px;
                color: #374151;
                font-size: 12px;
                word-break: break-all;
            }
        `;

        const panel = document.createElement('div');
        panel.id = TOOL_ID;
        panel.classList.toggle('collapsed', localStorage.getItem(PANEL_STATE_KEY) === '1');
        panel.innerHTML = `
            <div class="jasper-header">
                <span class="jasper-title">Tải Jasper/XML</span>
                <button class="jasper-toggle" type="button" aria-label="Thu gọn/mở rộng panel">◀</button>
            </div>
            <div class="jasper-body">
                <input class="jasper-input" type="text" placeholder="Nhập tên file jasper/xml.." />
                <button class="jasper-find" type="button">Tìm</button>
                <div class="jasper-status">Sẵn sàng.</div>
            </div>
        `;

        document.head.appendChild(style);
        document.body.appendChild(panel);

        placePanel(panel);

        const input = panel.querySelector('.jasper-input');
        const btnFind = panel.querySelector('.jasper-find');
        const btnToggle = panel.querySelector('.jasper-toggle');
        const status = panel.querySelector('.jasper-status');

        function setCollapsed(collapsed) {
            panel.classList.toggle('collapsed', collapsed);
            btnToggle.textContent = collapsed ? '▶JP' : '◀';
            btnToggle.title = collapsed ? 'Mở Down Jasper XML Tool' : 'Thu gọn Down Jasper XML Tool';
            localStorage.setItem(PANEL_STATE_KEY, collapsed ? '1' : '0');
        }

        function goSearch() {
            const val = normalize(input.value);
            const targetUrl = getTargetUrl(val);
            savePendingFileText(val);
            status.textContent = `Đang mở: ${targetUrl}`;
            window.open(targetUrl, '_blank');
        }

        btnFind.addEventListener('click', goSearch);
        btnToggle.addEventListener('click', () => {
            setCollapsed(!panel.classList.contains('collapsed'));
        });

        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                goSearch();
            }
        });

        const debouncedPlace = debounce(() => placePanel(panel), 100);
        window.addEventListener('resize', debouncedPlace);

        ['nv-import-panel', 'jasper-upl-tool', 'rpt-id-finder-tool', 'tool-phieuin-network', 'jasper-upl-telegram-tool'].forEach((id) => {
            const target = document.getElementById(id);
            if (!target) return;
            const observer = new MutationObserver(debouncedPlace);
            observer.observe(target, { attributes: true, attributeFilter: ['class', 'style'] });
        });

        const bodyObserver = new MutationObserver(debouncedPlace);
        bodyObserver.observe(document.body, { childList: true, subtree: true });

        setCollapsed(panel.classList.contains('collapsed'));
    }

    function initWhenReady() {
        if (document.body && document.head) {
            createTool();
            selectPathChooseByHash();
        } else {
            setTimeout(initWhenReady, 200);
        }
    }

    initWhenReady();
})();