// ==UserScript== // @name IDEAERP - Rejestracja Płatności // @namespace http://tampermonkey.net/ // @version 1.0 // @description Dodano kopiowanie kwoty w oknie rejestracji płatności // @match https://emma.ideaerp.pl/web* // @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/ // @downloadURL https://n8n.emma.net.pl/webhook/idea-payment-register // @updateURL https://n8n.emma.net.pl/webhook/idea-payment-register // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // ==/UserScript== /* ========================OPIS============================== WERSJA 1.0 - Kopiowanie kwoty płatności DZIAŁANIE SKRYPTU: - Wykrywa okno modalne "Rejestruj płatność" - Dodaje ikonę kopiowania (⧉) przy polu "Kwota" (amount) - Kliknięcie kopiuje wartość do schowka - Obsługa błędów i logowanie CHANGELOG: v1.0 (2024) - PIERWSZA WERSJA - Dodano obsługę okna modalnego - Dodano kopiowanie pola amount - Stylizacja zgodna z innymi skryptami ============================================================ */ (function () { 'use strict'; // ——— Style CSS ——— GM_addStyle(` /* Kopiowanie – ikona tuż za liczbą (inline), mniejsza, pomarańczowa */ .tm-copyable-icon { cursor: copy; display: inline-block; margin-left: 8px; font-size: 14px; line-height: 1; color: #ff9800; opacity: .75; vertical-align: middle; transition: opacity .15s ease; } .tm-copyable-icon:hover { opacity: 1; } .tm-copied { color: #4caf50 !important; /* Zielony po skopiowaniu */ } `); // ——— Schowek ——— async function tmCopyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.error('[Tampermonkey] Clipboard API failed, trying fallback', err); try { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); return true; } catch (e) { console.error('[Tampermonkey] Fallback copy failed', e); return false; } } } // ——— Główna logika ——— function processModal(modal) { if (modal.dataset.tmProcessed) return; // Sprawdź czy to modal płatności const title = modal.querySelector('.modal-title'); const isPaymentModal = title && (title.textContent.includes('Rejestruj płatność') || title.textContent.includes('Register Payment')); if (!isPaymentModal) { // Jeśli nie ma tytułu, sprawdzamy czy są pola specyficzne dla płatności if (!modal.querySelector('input[name="amount"]')) return; } console.log('[Tampermonkey] Przetwarzanie modalu płatności...'); // Lista pól do obsłużenia const fieldsToProcess = [ { name: 'amount', label: 'Kwota' }, { name: 'amount_div', label: 'Kwota' }, // Fallback dla struktury z amount_div { name: 'payment_date', label: 'Data płatności' }, { name: 'communication', label: 'Notatka' }, { name: 'ref', label: 'Referencja' } ]; fieldsToProcess.forEach(field => { // Priorytet dla inputa - szukamy najpierw inputa let element = modal.querySelector(`input[name="${field.name}"]`); // Jeśli nie ma inputa, szukamy div lub span if (!element) { element = modal.querySelector(`div[name="${field.name}"], span[name="${field.name}"]`); } if (element) { console.log(`[Tampermonkey] Znaleziono pole ${field.name}:`, element); addCopyIcon(element, field.label); // Dla pola kwoty dodaj dodatkowy wiersz z VAT if (field.name === 'amount' || field.name === 'amount_div') { addVatRow(element, modal); } } }); modal.dataset.tmProcessed = '1'; } function addVatRow(amountElement, modal) { // Znajdź wiersz tabeli (tr) zawierający pole kwoty const amountRow = amountElement.closest('tr'); if (!amountRow) return; // Sprawdź czy wiersz VAT już istnieje if (amountRow.nextElementSibling && amountRow.nextElementSibling.classList.contains('tm-vat-row')) return; // Ustal element z wartością (input) - priorytet dla inputa wewnątrz kontenera let valueElement = amountElement; if (amountElement.tagName !== 'INPUT' && amountElement.querySelector('input')) { valueElement = amountElement.querySelector('input'); } const getAmountValue = () => { let valStr = ''; if (valueElement.tagName === 'INPUT') { valStr = valueElement.value; } else { valStr = valueElement.textContent; } // Używamy tej samej logiki czyszczenia co przy kopiowaniu valStr = valStr.replace(/[^\d,.-]/g, '').replace(',', '.'); return parseFloat(valStr) || 0; }; // Oblicz VAT (Brutto -> VAT) // Wzór: VAT = Brutto * 23 / 123 const calculateVat = (val) => { if (!val) return '0,00'; const vat = (val * 23) / 123; return vat.toFixed(2).replace('.', ','); }; const amountVal = getAmountValue(); const calculatedValue = calculateVat(amountVal); // Stwórz nowy wiersz const tr = document.createElement('tr'); tr.className = 'tm-vat-row'; tr.innerHTML = ` ${calculatedValue} `; // Wstaw za wierszem kwoty amountRow.parentNode.insertBefore(tr, amountRow.nextSibling); // Obsługa kopiowania dla nowego pola const copyIcon = tr.querySelector('.tm-copyable-icon'); const valueSpan = tr.querySelector('.tm-vat-value'); copyIcon.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const val = valueSpan.textContent; if (val) { const success = await tmCopyToClipboard(val); if (success) { copyIcon.classList.add('tm-copied'); setTimeout(() => copyIcon.classList.remove('tm-copied'), 1200); } } }); // Nasłuchiwanie zmian w polu kwoty (jeśli to input) if (valueElement.tagName === 'INPUT') { valueElement.addEventListener('input', () => { const newVal = getAmountValue(); valueSpan.textContent = calculateVat(newVal); }); } } function addCopyIcon(element, label) { // Ustal element docelowy dla ikony i kontener let targetContainer = element.parentElement; let valueElement = element; let isDatepicker = false; // Jeśli element to div/span (np. datepicker wrapper), ale zawiera inputa, to chcemy inputa if (element.tagName !== 'INPUT' && element.querySelector('input')) { valueElement = element.querySelector('input'); // W przypadku datepickera (div), on sam jest kontenerem targetContainer = element; } // Sprawdź czy to datepicker if (targetContainer.classList.contains('o_datepicker')) { isDatepicker = true; // Dla datepickera wychodzimy poziom wyżej (do td), żeby nie psuć wewnętrznego layoutu (ikona kalendarza) if (targetContainer.parentElement) { targetContainer = targetContainer.parentElement; } } // Sprawdź czy ikona już nie istnieje w kontenerze if (targetContainer && targetContainer.querySelector('.tm-copyable-icon')) return; const icon = document.createElement('span'); icon.className = 'tm-copyable-icon'; icon.textContent = '⧉'; icon.title = `Kliknij aby skopiować ${label}`; icon.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); let valueToCopy = ''; if (valueElement.tagName === 'INPUT') { valueToCopy = valueElement.value; } else { valueToCopy = valueElement.textContent; } // Czyszczenie wartości if (label === 'Kwota') { valueToCopy = valueToCopy.replace(/[^\d,.-]/g, '').trim(); } else { valueToCopy = valueToCopy.trim(); } if (valueToCopy) { const success = await tmCopyToClipboard(valueToCopy); if (success) { icon.classList.add('tm-copied'); icon.title = `Skopiowano: ${valueToCopy}`; setTimeout(() => { icon.classList.remove('tm-copied'); icon.title = `Kliknij aby skopiować ${label}`; }, 1200); } } }); // Wstawianie ikony i stylowanie if (targetContainer) { // Wymuś flex na kontenerze, żeby ikona była obok targetContainer.style.display = 'flex'; targetContainer.style.alignItems = 'center'; // Jeśli to datepicker, element datepickera (dziecko) powinien zająć resztę miejsca if (isDatepicker) { const datepickerDiv = targetContainer.querySelector('.o_datepicker'); if (datepickerDiv) { datepickerDiv.style.flex = '1'; } } targetContainer.appendChild(icon); } else { // Fallback element.appendChild(icon); } } // ——— Obserwator zmian DOM ——— const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { // Sprawdź czy dodano modal if (node.classList.contains('modal') || node.classList.contains('o_technical_modal') || node.querySelector('.modal-dialog')) { // Poczekaj chwilę na renderowanie zawartości setTimeout(() => processModal(node), 500); } // Czasami modal jest już w DOM, ale zmienia się jego zawartość const modal = node.closest('.modal'); if (modal) { setTimeout(() => processModal(modal), 500); } } } } }); observer.observe(document.body, { childList: true, subtree: true }); })();