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