309 lines
12 KiB
JavaScript
309 lines
12 KiB
JavaScript
// ==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 = `
|
||
<td class="o_td_label">
|
||
<label class="o_form_label">Kwota VAT (obl.)</label>
|
||
</td>
|
||
<td style="width: 100%; display: flex; align-items: center;">
|
||
<span class="tm-vat-value" style="margin-right: 5px; font-weight: bold;">${calculatedValue}</span>
|
||
<span class="tm-copyable-icon" title="Kliknij aby skopiować Kwota VAT">⧉</span>
|
||
</td>
|
||
`;
|
||
|
||
// 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 });
|
||
|
||
})();
|