vzp

 

/** * VZP Monitor & Analyzér - Web Component Widget * * Použití: * 1. Vložte tento skript do stránky. * 2. Kamkoliv do HTML vložte tag: * 3. Widget se vykreslí uvnitř tohoto tagu. */ class VzpMonitor extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.render(); this.initScripts(); } render() { // Tailwind CSS + Custom Styles + HTML Structure this.shadowRoot.innerHTML = `

VZP Nástroje

Monitor & Prohlížeč

1. Kontrola verze

Nekontrolováno.

2. Vyhledávání

📂

Nahrát soubor

Full-text 0 řádků

Výsledky

📊

Data se zobrazí zde.

 

`; } initScripts() { // --- SCOPED REFERENCES --- // Protože jsme v Shadow DOM, musíme hledat elementy uvnitř `this.shadowRoot` // nikoliv v `document`. const $ = (id) => this.shadowRoot.getElementById(id); const root = this.shadowRoot; // State let lastVersion = null; let timer = null; let localDataLines = []; let currentRenderedLines = []; let headerLine = null; const PROXY_URL = 'https://corsproxy.io/?'; // --- EVENT LISTENERS --- $(('btnCheck')).onclick = () => checkNow(); $(('urlInput')).onchange = () => saveSettings(); $(('searchTextInput')).onchange = () => saveSettings(); $(('autoCheck')).onchange = (e) => toggleAutoCheck(e.target.checked); $(('dropZone')).onclick = () => $(('fileInput')).click(); $(('fileInput')).onchange = (e) => handleFile(e.target.files[0]); $(('localSearchInput')).onkeyup = () => filterLocalData(); $(('btnClearLog')).onclick = () => { $(('logList')).innerHTML = ''; }; $(('btnCloseModal')).onclick = () => closeModal(); // Drag & Drop const dropZone = $(('dropZone')); dropZone.ondragover = (e) => { e.preventDefault(); dropZone.classList.add('bg-blue-50', 'border-blue-400'); }; dropZone.ondragleave = (e) => { e.preventDefault(); dropZone.classList.remove('bg-blue-50', 'border-blue-400'); }; dropZone.ondrop = (e) => { e.preventDefault(); dropZone.classList.remove('bg-blue-50', 'border-blue-400'); if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); }; // --- FUNCTIONS --- const addToLog = (msg, type) => { const logList = $(('logList')); const color = type === 'error' ? 'text-red-500' : (type === 'success' ? 'text-green-600' : 'text-slate-500'); const div = document.createElement('div'); div.className = `flex justify-between ${color} mb-1`; div.innerHTML = `${msg}${new Date().toLocaleTimeString()}`; logList.prepend(div); }; const saveSettings = () => { localStorage.setItem('vzp_url', $(('urlInput')).value); localStorage.setItem('vzp_text', $(('searchTextInput')).value); }; const loadSettings = () => { const savedUrl = localStorage.getItem('vzp_url'); const savedText = localStorage.getItem('vzp_text'); const savedLast = localStorage.getItem('vzp_last_version'); $(('urlInput')).value = savedUrl || "https://www.vzp.cz/poskytovatele/ciselniky/zdravotnicke-prostredky"; $(('searchTextInput')).value = savedText || "Číselník VZP – ZP (Poukaz) verze"; if (savedLast) lastVersion = savedLast; }; const toggleAutoCheck = (enabled) => { if (enabled) { if (Notification.permission !== "granted") Notification.requestPermission(); if (!timer) timer = setInterval(() => checkNow(), 3600000); } else { clearInterval(timer); timer = null; } }; // --- LOGIC --- const fetchWithFallback = async (targetUrl) => { const timestamp = new Date().getTime(); try { const url1 = `https://api.allorigins.win/get?url=${encodeURIComponent(targetUrl)}&timestamp=${timestamp}`; const response1 = await fetch(url1); if (!response1.ok) throw new Error('AllOrigins failed'); const data1 = await response1.json(); if (!data1.contents) throw new Error('AllOrigins empty'); return data1.contents; } catch (e) { console.warn("Fallback to CorsProxy"); } try { const url2 = `https://corsproxy.io/?${encodeURIComponent(targetUrl)}`; const response2 = await fetch(url2); return await response2.text(); } catch (e) { throw new Error("Connection failed."); } }; const downloadDirectly = async (url) => { const resultsContainer = $(('resultsContainer')); const fileNameDisplay = $(('fileNameDisplay')); fileNameDisplay.textContent = "Stahuji..."; fileNameDisplay.classList.remove('hidden'); resultsContainer.innerHTML = '

 

'; try { const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(url)}`; const response = await fetch(proxyUrl); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); const fileName = url.split('/').pop() || 'data.zip'; const file = new File([blob], fileName, { type: blob.type }); addToLog(`Staženo: ${(file.size/1024/1024).toFixed(1)} MB`, 'success'); handleFile(file); } catch (error) { addToLog(`Chyba stahování: ${error.message}`, 'error'); resultsContainer.innerHTML = `

Stažení selhalo. Stáhnout ručně

`; } }; const checkNow = async () => { saveSettings(); const url = $(('urlInput')).value; const searchText = $(('searchTextInput')).value; const btn = $(('btnCheck')); const loader = $(('loading')); btn.disabled = true; btn.classList.add('opacity-50'); loader.classList.remove('hidden'); try { const htmlContent = await fetchWithFallback(url); const parser = new DOMParser(); const doc = parser.parseFromString(htmlContent, 'text/html'); const links = doc.querySelectorAll('a'); let foundLink = null; for (let link of links) { const cleanText = link.textContent.replace(/\s+/g, ' ').trim(); if (cleanText.includes(searchText)) { foundLink = { text: cleanText, href: link.getAttribute('href') }; break; } } updateUI(foundLink); } catch (error) { addToLog(`Chyba: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.classList.remove('opacity-50'); loader.classList.add('hidden'); } }; const updateUI = (linkData) => { const resultCard = $(('resultCard')); if (linkData) { const isNew = lastVersion && lastVersion !== linkData.text; lastVersion = linkData.text; localStorage.setItem('vzp_last_version', lastVersion); let fullUrl = linkData.href; if (fullUrl && !fullUrl.startsWith('http')) fullUrl = 'https://www.vzp.cz' + (fullUrl.startsWith('/') ? '' : '/') + fullUrl; resultCard.className = isNew ? "bg-green-50 border-l-4 border-green-500 p-2 rounded-r" : "bg-blue-50 border-l-4 border-blue-500 p-2 rounded-r"; // Event listener workaround pro tlačítko v innerHTML uvnitř Shadow DOM const safeUrl = fullUrl; resultCard.innerHTML = `

${isNew ? 'Nová verze!' : 'Verze OK'}
${linkData.text}

Link `; // Bind click event const dBtn = resultCard.querySelector('.download-btn'); if(dBtn) dBtn.onclick = () => downloadDirectly(safeUrl); addToLog(`Verze: ${linkData.text.substring(0, 15)}...`, isNew ? 'success' : 'info'); if (isNew) alert("Nová verze nalezena!"); } else { resultCard.innerHTML = "Odkaz nenalezen."; addToLog("Nenalezeno", "warning"); } }; const handleFile = async (file) => { if (!file) return; const fileNameDisplay = $(('fileNameDisplay')); const resultsContainer = $(('resultsContainer')); fileNameDisplay.textContent = file.name; fileNameDisplay.classList.remove('hidden'); resultsContainer.innerHTML = '

 

'; localDataLines = []; headerLine = null; try { // Dynamický import knihoven pokud nejsou načteny (zde spoléháme na globální script tagy, // v ShadowDOM je to tricky, pro jednoduchost předpokládáme window.JSZip/XLSX dostupnost) let textContent = ""; const isXls = file.name.match(/\.xls(x)?$/); const isZip = file.name.endsWith('.zip'); if (isXls) { const data = await file.arrayBuffer(); const workbook = window.XLSX.read(data, {type: 'array'}); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; textContent = window.XLSX.utils.sheet_to_csv(worksheet, {FS: ";", strip: false}); } else if (isZip) { const zip = await window.JSZip.loadAsync(file); let target = Object.values(zip.files).find(f => !f.dir && f.name.match(/\.xls(x)?$/)); let isXlsInside = true; if(!target) { target = Object.values(zip.files).find(f => !f.dir && f.name.match(/\.(txt|csv)$/)); isXlsInside = false; } if (target) { const buffer = await target.async("arraybuffer"); if (isXlsInside || target.name.match(/\.xls(x)?$/)) { const wb = window.XLSX.read(buffer, {type: 'array'}); textContent = window.XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]], {FS: ";", strip: false}); } else { const decoder = new TextDecoder("windows-1250"); textContent = decoder.decode(buffer); } } else throw new Error("Prázdný ZIP"); } else { const buffer = await file.arrayBuffer(); const decoder = new TextDecoder("windows-1250"); textContent = decoder.decode(buffer); } localDataLines = textContent.split('\n').filter(line => line.trim() !== ""); if (localDataLines.length > 0) headerLine = localDataLines[0]; $(('recordCount')).textContent = `${localDataLines.length} ř.`; $(('searchControls')).classList.remove('opacity-50', 'pointer-events-none'); renderResults(localDataLines.slice(0, 50), ""); } catch (e) { resultsContainer.innerHTML = `

Chyba: ${e.message}

`; addToLog(e.message, 'error'); } }; const filterLocalData = () => { const query = $(('localSearchInput')).value.toLowerCase(); if (query.length < 2) { renderResults(localDataLines.slice(0, 50), ""); return; } const matches = []; if (headerLine) matches.push(headerLine); let count = 0; for (let line of localDataLines) { if (line.toLowerCase().includes(query)) { matches.push(line); count++; if (count >= 100) break; } } renderResults(matches, query); }; const renderResults = (lines, query) => { currentRenderedLines = lines; const container = $(('resultsContainer')); if (lines.length === 0) { container.innerHTML = '

Nic nenalezeno.

'; return; } const delimiter = lines[0].includes('|') ? '|' : (lines[0].includes(';') ? ';' : null); let html = `

`; if (delimiter) { const headers = lines[0].split(delimiter); html += ``; headers.forEach(h => html += ``); html += ``; } else html += ``; lines.forEach((line, index) => { if (delimiter && index === 0) return; const cols = delimiter ? line.split(delimiter) : [line]; let rowHtml = ``; cols.forEach(c => { let content = c; if (query) content = content.replace(new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '$1'); rowHtml += `${content}`; }); html += `${rowHtml}`; }); html += `

ℹ️ ${h}

`; container.innerHTML = html; // Bind detail buttons container.querySelectorAll('.detail-btn').forEach(btn => { btn.onclick = (e) => openModal(e.target.dataset.index); }); }; const openModal = (index) => { const line = currentRenderedLines[index]; if (!line) return; const delimiter = headerLine && (headerLine.includes('|') ? '|' : ';'); const headers = delimiter ? headerLine.split(delimiter) : []; const values = delimiter ? line.split(delimiter) : [line]; let html = `

`; values.forEach((val, i) => { const label = headers[i] || `Sloupec ${i+1}`; const highlight = label.match(/(kód|název|cena)/i) ? 'bg-blue-50 border-blue-200' : 'bg-white border-slate-200'; html += `
${label}
${val}
`; }); html += `

`; $(('modalBody')).innerHTML = html; $(('detailModal')).classList.add('open'); }; const closeModal = () => { $(('detailModal')).classList.remove('open'); }; // Init load loadSettings(); } } // Definice Web Componenty customElements.define('vzp-monitor', VzpMonitor);