Down Jasper XML Tool

Tải jasper/xml tự động

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
})();