Portál Stavební Správy - Autofill

Automatické vyplňování na portálu stavební správy, které obchazí nedostatky ukládání dat. Podporuje ARES, nahrávání souborů a úpravu scénářů.

// ==UserScript==
// @name         Portál Stavební Správy - Autofill
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Automatické vyplňování na portálu stavební správy, které obchazí nedostatky ukládání dat. Podporuje ARES, nahrávání souborů a úpravu scénářů.
// @author       Teodor Tomáš
// @match        https://portal.stavebnisprava.gov.cz/*
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-idle
// @license      GNU GPLv3
// ==/UserScript==

(function() {
    'use strict';

    const DB_KEY = 'autofillManagerSets'; //neměnit!

    // =================================================================
    // STYLES
    // =================================================================
    const styles = `
        #autofill-manager-container { position: fixed; top: 80px; right: -420px; width: 420px; max-height: 85vh; background: #f9f9f9; border: 1px solid #ccc; border-radius: 8px 0 0 8px; box-shadow: -5px 5px 15px rgba(0,0,0,0.2); z-index: 10001; transition: right 0.3s ease-in-out; display: flex; flex-direction: column; font-family: sans-serif; }
        #autofill-manager-container.visible { right: 0; }
        #autofill-manager-toggle { position: fixed; top: 80px; right: 0; background: #2362A2; color: white; border: none; padding: 10px; border-radius: 5px 0 0 5px; cursor: pointer; z-index: 10002; font-size: 20px; line-height: 1; }
        .autofill-header { padding: 12px; background: #333; color: white; font-size: 18px; text-align: center; border-radius: 8px 0 0 0; }
        .autofill-body { padding: 15px; overflow-y: auto; flex-grow: 1; }
        .autofill-footer { padding: 10px; border-top: 1px solid #ccc; text-align: center; background: #f0f0f0; }
        .autofill-button { background: #2362A2; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin: 5px; }
        .autofill-button.edit { background: #f0ad4e; }
        .autofill-button.edit:hover { background: #ec971f; }
        .autofill-button:hover { background: #1a4a7a; }
        .autofill-button.delete { background: #c52a3a; }
        .autofill-button.delete:hover { background: #a0232f; }
        .scenario-list-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
        .scenario-list-item span { font-weight: bold; flex-grow: 1; }
        #creation-bar { position: fixed; top: 0; left: 0; width: 100%; background: #fff1a8; padding: 10px; z-index: 10001; display: flex; flex-direction: column; align-items: center; justify-content: center; box-shadow: 0 2px 10px rgba(0,0,0,0.2); border-bottom: 3px solid #f9c13c; }
        #creation-bar-main { display: flex; align-items: center; }
        #creation-bar-files { margin-top: 10px; font-size: 14px; width: 600px; }
        #creation-bar-tip { font-size: 12px; font-style: italic; color: #666; margin-bottom: 8px; text-align: center; }
        .file-creator-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; }
        .file-creator-label { min-width: 350px; text-align: right; margin-right: 10px; }
        .file-creator-row input[type="file"] { display: none; }
        .file-creator-row .file-label-btn { background: #555; color: white; padding: 3px 8px; border-radius: 3px; cursor: pointer; font-size: 12px; }
        .file-status-span { min-width: 150px; font-style: italic; color: #333; }
        #creation-bar-label { font-weight: bold; margin-right: 15px; }
        #creation-bar-input { padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 300px; }
    `;
    const styleSheet = document.createElement("style");
    styleSheet.innerText = styles;
    document.head.appendChild(styleSheet);

    // =================================================================
    // CORE LOGIC
    // =================================================================
    class StorageManager {
        constructor() { this.key = DB_KEY; }
        async getSets() { return await GM_getValue(this.key, {}); }
        async saveSet(name, sequence) {
            const allSets = await this.getSets();
            allSets[name] = sequence;
            await GM_setValue(this.key, allSets);
        }
        async deleteSet(name) {
            const allSets = await this.getSets();
            if (allSets[name]) {
                delete allSets[name];
                await GM_setValue(this.key, allSets);
            }
        }
    }

    class SequenceEngine {
        async execute(sequence) {
            console.log("Spouštím scénář:", sequence);
            const toggleBtn = document.getElementById('autofill-manager-toggle');
            toggleBtn.innerText = '⚙️';
            let aresJustClicked = false;

            for (const step of sequence) {
                const element = this.findElement(step.selector);
                if (element) {
                    try {
                        element.scrollIntoView({ behavior: 'auto', block: 'center' });
                        await this.sleep(50);

                        // Zde je klíčová oprava
                        if (aresJustClicked && step.action === 'fill' && element.value) {
                            console.log(`Přeskakuji pole ${step.selector}, bylo vyplněno z ARES.`);
                            continue; // Přeskočí zbytek cyklu pro tento krok
                        }

                        element.focus();
                        let sleepTime = 100;

                        switch(step.action) {
                            case 'click':
                                element.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
                                if (element.type === 'radio') sleepTime = 700;
                                aresJustClicked = false;
                                break;
                            case 'click-ares':
                                element.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
                                sleepTime = 1500;
                                aresJustClicked = true;
                                break;
                            case 'fill':
                            case 'select':
                                this.setNativeValue(element, step.value);
                                if (step.action === 'select') element.dispatchEvent(new Event('change', { bubbles: true }));
                                aresJustClicked = false;
                                break;
                            case 'upload':
                                const file = this.base64ToFile(step.data, step.name, step.type);
                                const dataTransfer = new DataTransfer();
                                dataTransfer.items.add(file);
                                element.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true, cancelable: true }));
                                sleepTime = 800;
                                break;
                        }
                        await this.sleep(sleepTime);
                        if (document.activeElement === element) element.blur();
                        await this.sleep(50);
                    } catch (e) { console.error(`Chyba při kroku pro selector ${step.selector}:`, e); }
                } else { console.warn(`Element nenalezen:`, step); }
            }
            toggleBtn.innerText = '🛠️';
            console.log("Scénář dokončen.");
        }

        generate(formContainer, fileData) {
            const sequence = [];
            const elements = Array.from(formContainer.querySelectorAll('input, select, textarea'));
            const icoFilled = new Set();
            elements.sort((a, b) => (a.type === 'radio' ? 0 : 1) - (b.type === 'radio' ? 0 : 1));
            elements.forEach(el => {
                const selector = this.getSelector(el);
                if (!selector) return;
                if (el.type === 'radio' && el.checked) {
                    sequence.push({ action: 'click', selector });
                } else if (el.type === 'checkbox' && el.checked) {
                    sequence.push({ action: 'click', selector });
                } else if (el.classList.contains('with-ico') && el.value && !icoFilled.has(selector)) {
                    sequence.push({ action: 'fill', selector, value: el.value });
                    const aresButton = el.parentElement.querySelector('button.ico-btn');
                    if (aresButton) {
                        const btnSelector = this.getSelector(aresButton);
                        if(btnSelector) sequence.push({ action: 'click-ares', selector: btnSelector });
                    }
                    icoFilled.add(selector);
                } else if ((el.type === 'text' || el.type === 'date' || el.tagName.toLowerCase() === 'textarea') && el.value && !el.classList.contains('with-ico')) {
                     sequence.push({ action: 'fill', selector, value: el.value });
                } else if (el.tagName.toLowerCase() === 'select' && el.value) {
                    sequence.push({ action: 'select', selector, value: el.value });
                }
            });
            for (const selector in fileData) {
                sequence.push({ action: 'upload', selector, ...fileData[selector] });
            }
            return sequence;
        }

        getSelector(el) {
            if (el.id && document.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1) return `#${CSS.escape(el.id)}`;
            if (el.type === 'radio' && el.name && el.value) return `input[type="radio"][name="${CSS.escape(el.name)}"][value="${CSS.escape(el.value)}"]`;
            let path = ''; let current = el;
            while (current && current.nodeType === Node.ELEMENT_NODE && current.tagName.toLowerCase() !== 'body') {
                let segment = current.tagName.toLowerCase();
                if (current.className && typeof current.className === 'string') {
                    const classNames = current.className.trim().replace(/\s+/g, '.');
                    if(classNames) segment += '.' + classNames;
                }
                const siblings = Array.from(current.parentNode.children);
                const sameTagSiblings = siblings.filter(sibling => sibling.tagName === current.tagName);
                if (sameTagSiblings.length > 1) {
                    segment += `:nth-of-type(${sameTagSiblings.indexOf(current) + 1})`;
                }
                path = segment + (path ? ' > ' + path : '');
                if (current.parentElement && current.parentElement.id && document.querySelectorAll(`#${CSS.escape(current.parentElement.id)}`).length === 1) {
                    path = '#' + CSS.escape(current.parentElement.id) + ' > ' + path;
                    break;
                }
                current = current.parentElement;
            }
            return path;
        }
        base64ToFile(dataurl, filename, mimeType) { const arr=dataurl.split(','),bstr=atob(arr[1]);let n=bstr.length;const u8arr=new Uint8Array(n);while(n--){u8arr[n]=bstr.charCodeAt(n)}return new File([u8arr],filename,{type:mimeType}) }
        findElement(selector) { try { return document.querySelector(selector); } catch (e) { console.error(`Neplatný selector: "${selector}"`, e); return null; }}
        setNativeValue(element, value) { const s=Object.getOwnPropertyDescriptor(element.constructor.prototype,'value').set;s.call(element,value);element.dispatchEvent(new Event('input',{bubbles:true})); }
        sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
    }

    class UIManager {
        constructor(storage, engine) {
            this.storage = storage; this.engine = engine;
        }
        createPanel() {
            this.container=document.createElement('div');
            this.container.id='autofill-manager-container';
            document.body.appendChild(this.container);
            const toggleButton=document.createElement('button');
            toggleButton.id='autofill-manager-toggle';
            toggleButton.innerHTML='🛠️';
            toggleButton.onclick=()=>this.container.classList.toggle('visible');
            document.body.appendChild(toggleButton);
            this.renderListView();
        }
        async renderListView() {
             this.container.innerHTML = `
                <div class="autofill-header">Autofill Manažer</div>
                <div class="autofill-body" id="autofill-body-content"></div>
                <div class="autofill-footer">
                    <button id="create-new-scenario-btn" class="autofill-button">Vytvořit nový scénář</button>
                </div>
            `;
            this.body = document.getElementById('autofill-body-content');
            const sets = await this.storage.getSets();
            if (Object.keys(sets).length === 0) {
                this.body.innerHTML = '<p style="text-align:center;color:#666;">Zatím nemáte žádné uložené scénáře.</p>';
            } else {
                for (const name in sets) {
                    const item = document.createElement('div');
                    item.className = 'scenario-list-item';
                    item.innerHTML = `<span>${name}</span>
                        <div>
                            <button class="autofill-button" data-run-scenario="${name}">Spustit</button>
                            <button class="autofill-button edit" data-edit-scenario="${name}">Upravit</button>
                            <button class="autofill-button delete" data-delete-scenario="${name}">Smazat</button>
                        </div>`;
                    this.body.appendChild(item);
                }
            }
            this.attachListListeners();
        }
        enterCreationMode(scenarioNameToEdit = null, scenarioData = null) {
            this.container.classList.remove('visible');
            const bar = document.createElement('div');
            bar.id = 'creation-bar';
            bar.innerHTML = `
                <div id="creation-bar-main">
                    <span id="creation-bar-label">${scenarioNameToEdit ? 'Režim úpravy' : 'Režim tvorby'} scénáře | Název:</span>
                    <input id="creation-bar-input" type="text" placeholder="Vyplňte jméno a klikněte na Uložit...">
                    <button id="creation-bar-save" class="autofill-button">Uložit</button>
                    <button id="creation-bar-cancel" class="autofill-button delete">Zrušit</button>
                </div>
                <div id="creation-bar-files"></div>
            `;
            document.body.appendChild(bar);
            const nameInput = document.getElementById('creation-bar-input');
            if(scenarioNameToEdit) {
                nameInput.value = scenarioNameToEdit;
            }
            this.populateFileCreators(scenarioData);
            document.getElementById('creation-bar-cancel').onclick = () => this.exitCreationMode();
            document.getElementById('creation-bar-save').onclick = async () => {
                const name = document.getElementById('creation-bar-input').value.trim();
                if (!name) { alert('Prosím, zadejte název scénáře.'); return; }
                const fileData = {};
                document.querySelectorAll('.file-creator-row input[type="file"]').forEach(input => {
                    if (input.dataset.fileData) {
                        fileData[input.dataset.targetSelector] = {
                            data: input.dataset.fileData, name: input.dataset.fileName, type: input.dataset.fileType
                        };
                    }
                });
                const formContainer = document.querySelector('.pe-lg-3.col-xl-8');
                const sequence = this.engine.generate(formContainer, fileData);
                await this.storage.saveSet(name, sequence);
                alert(`Scénář "${name}" byl úspěšně uložen.`);
                this.exitCreationMode();
            };
        }
        populateFileCreators(scenarioData = null) {
            const filesContainer = document.getElementById('creation-bar-files');
            const dropZones = document.querySelectorAll('.file-drop-target');
            if(dropZones.length > 0) {
                 filesContainer.innerHTML = '<div id="creation-bar-tip">Tip: Nejprve na stránce nastavte všechny přepínače (např. \'právnická osoba\'), aby se zobrazily všechny relevantní pole pro nahrání souborů.</div>';
            }
            dropZones.forEach((zone, index) => {
                const title = zone.closest('.row-wrapper')?.querySelector('h3.id-title')?.innerText || `Nahrávání souboru #${index + 1}`;
                const selector = this.engine.getSelector(zone);
                const foundFileStep = scenarioData ? scenarioData.find(step => step.action === 'upload' && step.selector === selector) : null;
                const row = document.createElement('div');
                row.className = 'file-creator-row';
                row.innerHTML = `
                    <span class="file-creator-label">${title}:</span>
                    <label class="file-label-btn" for="file-creator-${index}">Vybrat soubor...</label>
                    <input type="file" id="file-creator-${index}" data-target-selector="${selector}">
                    <span class="file-status-span" id="file-status-${index}">${foundFileStep ? `Vybráno: ${foundFileStep.name}` : 'Nevybrán'}</span>
                `;
                filesContainer.appendChild(row);
                const fileInput = row.querySelector('input[type="file"]');
                if(foundFileStep){
                    fileInput.dataset.fileData = foundFileStep.data;
                    fileInput.dataset.fileName = foundFileStep.name;
                    fileInput.dataset.fileType = foundFileStep.type;
                }
                fileInput.addEventListener('change', (e) => {
                    const file = e.target.files[0];
                    const statusSpan = document.getElementById(`file-status-${index}`);
                    if (!file) {
                        delete fileInput.dataset.fileData; statusSpan.textContent = 'Nevybrán'; return;
                    }
                    const reader = new FileReader();
                    reader.onload = (event) => {
                        fileInput.dataset.fileData = event.target.result;
                        fileInput.dataset.fileName = file.name;
                        fileInput.dataset.fileType = file.type;
                        statusSpan.textContent = `Vybráno: ${file.name}`;
                    };
                    reader.readAsDataURL(file);
                });
            });
        }
        exitCreationMode() { const e=document.getElementById('creation-bar');if(e)e.remove();this.renderListView(); }
        attachListListeners() {
            document.getElementById('create-new-scenario-btn').onclick = () => this.enterCreationMode();
            this.container.querySelectorAll('[data-run-scenario]').forEach(btn => {
                btn.onclick = async () => {
                    const name = btn.dataset.runScenario;
                    const sets = await this.storage.getSets();
                    if(sets[name]) {
                         this.container.classList.remove('visible');
                         await this.engine.execute(sets[name]);
                    }
                };
            });
            this.container.querySelectorAll('[data-edit-scenario]').forEach(btn => {
                btn.onclick = async () => {
                    const name = btn.dataset.editScenario;
                    const sets = await this.storage.getSets();
                    if (sets[name]) {
                        this.container.classList.remove('visible');
                        await this.engine.execute(sets[name]);
                        this.enterCreationMode(name, sets[name]);
                    }
                };
            });
            this.container.querySelectorAll('[data-delete-scenario]').forEach(btn => {
                btn.onclick = async () => {
                    const name = btn.dataset.deleteScenario;
                    if(confirm(`Opravdu chcete smazat scénář "${name}"?`)) {
                        await this.storage.deleteSet(name);
                        this.renderListView();
                    }
                };
            });
        }
    }

    // =================================================================
    // INITIALIZATION
    // =================================================================
    setTimeout(() => {
        try {
            const storage = new StorageManager();
            const engine = new SequenceEngine();
            const ui = new UIManager(storage, engine);
            ui.createPanel();
        } catch(e) { console.error("Chyba při inicializaci Autofill Manažeru:", e); }
    }, 1500);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址