tampermonkey/idea/idea-kfz-listing.user.js
2025-12-15 19:42:21 +01:00

982 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==UserScript==
// @name IDEA KFZ Listing
// @namespace http://tampermonkey.net/
// @version 3.1
// @description Ulepszenia dla listy faktur KFZ w systemie IDEA ERP
// @author Adam
// @match https://*.ideaerp.pl/*
// @icon https://www.ideaerp.pl/favicon.ico
// @downloadURL https://n8n.emma.net.pl/webhook/kfz-listing
// @updateURL https://n8n.emma.net.pl/webhook/kfz-listing
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// ==/UserScript==
/* ========================OPIS==============================
WERSJA 3.1 - Uproszczona logika wykrywania strony
DZIAŁANIE SKRYPTU:
- Wykrywa strony z listą faktur KFZ w systemie IDEA ERP
- Dodaje ulepszenia wizualne tabeli (sticky header, podświetlanie)
- Implementuje szybkie filtry według dostawców
- Umożliwia kopiowanie zawartości komórek w kolumnach:
* Numer faktury (data-name="name")
* Data faktury (data-name="invoice_date")
* Odnośnik (data-name="invoice_origin")
* Należność (data-name="amount_residual_signed" - NIE NETTO!)
* Na podstawie (data-name="reversed_entry_id")
- Pokazuje powiadomienia o skopiowaniu
ZMIANY W WERSJI 3.1:
- 🔍 UPROSZCZONA logika wykrywania strony - bardziej liberalne warunki
- ✅ Działaj jeśli: action=278+menu_id=141 LUB breadcrumb="Korekty" LUB numery KFZ
- 📊 Dodano szczegółowe logi w initEnhancements()
- 🚀 Usunięto zbędne skomplikowane warunki
- 🎯 Focus na działanie, nie na perfekcyjne wykrywanie
ZMIANY W WERSJI 3.0:
- 🔄 POWRÓT do stabilnej wersji bez waitForPageLoad()
- ❌ USUNIĘTO skomplikowaną funkcję waitForPageLoad() która powodowała błędy
- ✅ PRZYWRÓCONO prostą logikę z setTimeout(initEnhancements)
- 🎯 Wymagane action=278 + menu_id=141 dla aktywacji
- 📊 Zachowano podstawowe logi debugowania
- 🚀 Punkt wyjścia do dalszych ulepszeń
ZMIANY W WERSJI 2.9.1:
- 🐛 NAPRAWIONO błąd "hasAction278 is not defined"
- 🔧 Poprawiono wszystkie odwołania do zmiennych w waitForPageLoad()
- ✅ Usunięto błędy JavaScript które blokowały działanie skryptu
ZMIANY W WERSJI 2.9:
- 🔍 DODANO szczegółowe debugowanie w waitForPageLoad() i isKfzListingPage()
- 📊 Emoji w logach dla lepszej czytelności
- 🎯 Wymagane action=278 + menu_id=141 dla aktywacji
- 📍 Logi URL i elementów na starcie skryptu
- 🚀 Test czy skrypt w ogóle się ładuje
- 🔧 Diagnostyka dla problemu "nie działa w ogóle"
ZMIANY W WERSJI 2.8:
- 🔄 DODANO resetowanie stanu przy zmianie URL (hashchange i polling)
- 🔍 Sprawdzanie czy elementy rzeczywiście istnieją mimo markera
- 🧹 Automatyczne usuwanie markerów przy zmianie strony
- 🎯 Dodano hasKfzNumbers jako dodatkowy warunek aktywacji
- 📊 Szczegółowe logi resetowania stanu
- ✅ Rozwiązano problem niespójnego działania przy nawigacji
ZMIANY W WERSJI 2.7:
- 🕐 DODANO funkcję waitForPageLoad() - inteligentne oczekiwanie na elementy
- 🔄 Maksymalnie 10 prób co 1 sekundę na załadowanie tabeli/breadcrumb
- 🚫 Wykrywanie elementów ładowania (.o_loading, .fa-spin) - czeka aż znikną
- 🎯 Wszystkie wywołania initEnhancements zastąpione waitForPageLoad
- 📊 Szczegółowe logi procesu oczekiwania na załadowanie
- ✅ Rozwiązano problem "Table: false, Breadcrumb: null" po odświeżeniu
ZMIANY W WERSJI 2.6:
- 🎯 DODANO wykrywanie strony przez breadcrumb "Korekty"
- 🔄 ZMIENIONA logika: aktywacja nawet bez danych KFZ (mogą się załadować później)
- 📍 Backup timer sprawdza breadcrumb co 3 sekundy
- 🚀 Rozwiązano problem z URL: action=278&model=account.move&view_type=list&cids=1&menu_id=141
- 📊 Dodatkowe logi dla debugowania wykrywania strony
ZMIANY W WERSJI 2.5:
- 🔄 DODANO wielopoziomowe wykrywanie zmian URL dla aplikacji SPA
- 📍 5 metod wykrywania: hashchange, URL polling, title observer, breadcrumb observer, backup timer
- ⚡ Szybsze reakcje: 200ms, 300ms, 400ms, 500ms, 1000ms, 1500ms
- 🎯 Rozwiązano problem z brakiem działania przy nawigacji (bez odświeżania)
- 🔍 Backup sprawdzanie co 3 sekundy czy strona KFZ ma filtr
- 📊 Szczegółowe logi dla debugowania zmian URL
ZMIANY W WERSJI 2.4:
- 🔧 PRZENIESIONO filtr za przyciski w lewym panelu (.o_cp_left)
- 🚫 ROZWIĄZANO problem nakładania się na wyszukiwarkę
- 📍 Filtr teraz w linii: [Zapisz] [Odrzuć] [🔽 Dostawca] | [Wyszukiwarka]
- 💫 Lepsze wykorzystanie przestrzeni bez konfliktów
ZMIANY W WERSJI 2.3:
- 🔧 NAPRAWIONO pozycjonowanie filtra - teraz jest OBOK wyszukiwarki
- 📍 Filtr wstawiony do `.o_searchview_input_container` zamiast `.o_cp_searchview`
- 💫 Filtr i wyszukiwarka w tej samej linii (flex-shrink: 0)
- 🎯 Lepsze wykorzystanie przestrzeni w panelu kontrolnym
ZMIANY W WERSJI 2.2:
- 🎨 PRZENIESIONO filtr dostawców do panelu kontrolnego (.o_cp_searchview)
- 💫 Filtr ma teraz profesjonalny wygląd zgodny z interfejsem Odoo
- 📍 Umieszczony obok wyszukiwarki z ikoną i odpowiednim stylem
- 🔄 Dodano fallback - jeśli brak panelu, filtr pojawi się przed tabelą
- 🎯 Zaktualizowano funkcję filtrowania na nowe indeksy pozycyjne
ZMIANY W WERSJI 2.1:
- 🔧 NAPRAWIONO błąd TypeError w isKfzListingPage()
- 🎯 ZASTĄPIONO wyszukiwanie po data-name na indeksy pozycyjne
- 📍 Mapowanie kolumn na podstawie rzeczywistej struktury:
* Komórka 2: Numer faktury (KFZ/...)
* Komórka 4: Data faktury (DD.MM.YYYY)
* Komórka 5: Dokument źródłowy (ZZ/...)
* Komórka 6: Odnośnik (KFAS/...)
* Komórka 10: Należność (...zł)
* Komórka 11: Na podstawie (KFZ/... (...))
- ✅ Wszystkie kolumny powinny teraz działać poprawnie!
ZMIANY W WERSJI 2.0:
- Dodano bardzo szczegółowe debugowanie dla wszystkich kolumn
- Logowanie struktury pierwszego wiersza z klasami CSS
- Szczegółowe logi dla każdej poszukiwanej kolumny:
* invoice_origin (Dokument źródłowy)
* ref (Odnośnik)
* amount_residual_signed (Należność)
* reversed_entry_id (Na podstawie)
- Informowanie o pustych wartościach i myślnikach
- Wyraźne oznaczenie ❌ gdy kolumna nie została znaleziona
ZMIANY W WERSJI 1.9:
- Usunięto niepotrzebne kopiowanie z kolumny NETTO (amount_untaxed_signed)
- Dodano sprawdzanie data-name żeby unikać kopiowania z NETTO
- Pozostawiono tylko kopiowanie z kolumny NALEŻNOŚĆ (amount_residual_signed)
- Ograniczono debugowanie do pierwszego wiersza
- Dodano szczegółowe logowanie dla każdej kolumny
ZMIANY W WERSJI 1.8:
- Dodano szczegółowe debugowanie struktury tabeli
- Fallback dla różnych formatów data-name (małe/duże litery)
- Alternatywne wyszukiwanie po zawartości komórek
- Debugowanie wszystkich komórek w wierszach
- Logowanie procesu dodawania kopiowania
- Sprawdzanie czy w ogóle są wiersze danych
ZMIANY W WERSJI 1.7:
- Zmieniono z indeksów kolumn na querySelector z data-name
- Numer: td[data-name="name"]
- Data faktury: td[data-name="invoice_date"]
- Odnośnik: td[data-name="invoice_origin"]
- Należność: td[data-name="amount_residual_signed"]
- Na podstawie: td[data-name="reversed_entry_id"]
- Dodano szczegółowe logowanie dla każdej kolumny
- Rozwiązano problem z ukrytymi kolumnami
ZMIANY W WERSJI 1.6:
- Zmieniono nazwę skryptu na "IDEA KFZ Listing"
- Poprawiono mapowanie kolumny "Należność" (amount_residual_signed, indeks 9)
- Naprawiono kopiowanie kolumny "Odnośnik" (invoice_origin, indeks 5)
- Dodano debugowanie dla kolumny "Odnośnik"
- Wykluczenie pustych wartości i myślników "-" z kopiowania
ZMIANY W WERSJI 1.5:
- Rozszerzone kopiowanie na 5 kolumn (Numer, Data faktury, Odnośnik, Należność, Na podstawie)
- Ulepszone czyszczenie kwot (usuwanie "zł" i znaków niełamliwych spacji)
- Automatyczne wykrywanie kolumny "Na podstawie" przez data-name="reversed_entry_id"
- Inteligentne wykrywanie dat w formacie DD.MM.YYYY
- Kopiowanie odnośników (dokumentów źródłowych)
ZMIANY W WERSJI 1.4:
- Dodano zabezpieczenie przed duplikowaniem elementów
- Sprawdzanie czy skrypt już został uruchomiony (.kfz-script-initialized)
- Unikalne ID dla stylów CSS (#kfz-copy-styles, #kfz-table-styles)
- Sprawdzanie istnienia filtrów (.kfz-filter-container)
- Eliminacja powielania elementów przy wielokrotnym uruchomieniu
ZMIANY W WERSJI 1.3:
- Obsługa dynamicznego ładowania danych przez AJAX
- Przechwytywanie żądań do /web/dataset/search_read
- Wykrywanie różnicy między widokiem grupowanym a szczegółowym
- Uruchamianie skryptu po kliknięciu na dostawcę
- Inteligentny MutationObserver dla nowych wierszy danych
ZMIANY W WERSJI 1.2:
- Poprawione wykrywanie strony KFZ (sprawdza URL account.move)
- Dodano obserwację zmian DOM dla dynamicznych stron
- Lepsze debugowanie z logami w konsoli
- Obsługa zmian URL (hashchange)
- MutationObserver dla automatycznego uruchamiania
ZMIANY W WERSJI 1.1:
- Dodano funkcjonalność kopiowania w stylu idea-orders.user.js
- Ikony kopiowania ⧉ po prawej stronie numerów KFZ (pomarańczowe)
- Kliknięcie na komórkę z numerem kopiuje do schowka
- Efekt wizualny po skopiowaniu (pomarańczowa ramka)
- Fallback dla starszych przeglądarek
- Dodano @updateURL i @downloadURL zgodnie z zasadami
WERSJA 1.0 - Podstawowe ulepszenia listy KFZ
- Wykrywanie strony z listą faktur KFZ
- Ulepszenia wizualne tabeli
- Szybkie filtry według dostawców
- System logowania z flagą DEBUG
============================================================ */
(() => {
'use strict';
// [Tampermonkey] Konfiguracja
const DEBUG = true;
const SCRIPT_NAME = '[IDEA KFZ Listing]';
/**
* Funkcja logowania z prefiksem
* @param {string} message - Wiadomość do zalogowania
*/
function log(message) {
if (DEBUG) {
console.log(`${SCRIPT_NAME} ${message}`);
}
}
/**
* Sprawdza czy jesteśmy na stronie z listą faktur KFZ
* @returns {boolean}
*/
function isKfzListingPage() {
// Sprawdź URL - czy to model account.move
const url = window.location.href;
const isAccountMoveList = url.includes('model=account.move') && url.includes('view_type=list');
const hasAction278 = url.includes('action=278');
const hasMenu141 = url.includes('menu_id=141');
// Sprawdź czy jest tabela
const table = document.querySelector('.o_list_table');
// Sprawdź czy są numery KFZ w różnych formatach
const hasKfzNumbers = document.querySelector('td[title*="KFZ/"]') ||
Array.from(document.querySelectorAll('td')).some(td => td.textContent.includes('KFZ/')) ||
(document.textContent && document.textContent.includes('KFZ/'));
// Sprawdź czy są wiersze danych (nie tylko grupowanie)
const hasDataRows = document.querySelectorAll('.o_data_row').length > 0;
// Sprawdź czy to nie jest widok grupowany (ma grupy ale nie ma szczegółów)
const hasGroups = document.querySelectorAll('.o_group_header').length > 0;
const isGroupedView = hasGroups && !hasDataRows;
// Sprawdź breadcrumb - czy zawiera "Korekty"
const breadcrumb = document.querySelector('.breadcrumb');
const hasCorrectionsBreadcrumb = breadcrumb && breadcrumb.textContent.includes('Korekty');
log(`URL: ${isAccountMoveList}, Action278: ${hasAction278}, Menu141: ${hasMenu141}, Table: ${!!table}, KFZ: ${!!hasKfzNumbers}, DataRows: ${hasDataRows}, Breadcrumb: ${hasCorrectionsBreadcrumb}`);
// Bardziej liberalne warunki - działaj jeśli:
// 1. Jest action=278 + menu_id=141 (specificzny URL dla KFZ)
// 2. LUB breadcrumb zawiera "Korekty"
// 3. LUB znaleziono numery KFZ w treści strony
const shouldActivate = (hasAction278 && hasMenu141) ||
hasCorrectionsBreadcrumb ||
(isAccountMoveList && hasKfzNumbers);
log(`Czy aktywować: ${shouldActivate}`);
return shouldActivate;
}
/**
* Czyści duplikaty elementów z poprzednich uruchomień
*/
function cleanupDuplicates() {
// Usuń duplikaty filtrów
const filterContainers = document.querySelectorAll('.kfz-filter-container');
if (filterContainers.length > 1) {
for (let i = 1; i < filterContainers.length; i++) {
filterContainers[i].remove();
}
log(`Usunięto ${filterContainers.length - 1} duplikatów filtrów`);
}
// Usuń duplikaty stylów
const copyStyles = document.querySelectorAll('#kfz-copy-styles');
if (copyStyles.length > 1) {
for (let i = 1; i < copyStyles.length; i++) {
copyStyles[i].remove();
}
log(`Usunięto ${copyStyles.length - 1} duplikatów stylów kopiowania`);
}
const tableStyles = document.querySelectorAll('#kfz-table-styles');
if (tableStyles.length > 1) {
for (let i = 1; i < tableStyles.length; i++) {
tableStyles[i].remove();
}
log(`Usunięto ${tableStyles.length - 1} duplikatów stylów tabeli`);
}
}
/**
* Główna funkcja inicjalizująca ulepszenia
*/
async function initEnhancements() {
try {
log(`🔍 INIT ENHANCEMENTS - sprawdzam stronę: ${window.location.href}`);
if (!isKfzListingPage()) {
log('❌ Nie wykryto strony z listą KFZ');
return;
}
log('✅ WYKRYTO STRONĘ KFZ - kontynuuję...');
// Sprawdź czy skrypt już został uruchomiony na tej stronie
const scriptMarker = document.querySelector('.kfz-script-initialized');
if (scriptMarker) {
log('Skrypt już został uruchomiony - sprawdzam czy elementy istnieją');
// Sprawdź czy elementy rzeczywiście istnieją
const hasFilter = document.querySelector('.kfz-filter-container');
const hasStyles = document.querySelector('#kfz-copy-styles');
if (!hasFilter || !hasStyles) {
log('Elementy skryptu nie istnieją mimo markera - resetuję i uruchamiam ponownie');
scriptMarker.remove();
// Usuń wszystkie elementy skryptu
document.querySelectorAll('.kfz-filter-container, #kfz-copy-styles, #kfz-table-styles').forEach(el => el.remove());
} else {
log('Elementy skryptu istnieją - pomijam');
return;
}
}
// Usuń ewentualne duplikaty z poprzednich uruchomień
cleanupDuplicates();
log('Wykryto stronę z listą faktur KFZ');
// Oznacz że skrypt został uruchomiony
const marker = document.createElement('div');
marker.className = 'kfz-script-initialized';
marker.style.display = 'none';
document.body.appendChild(marker);
// Tutaj można dodać konkretne ulepszenia
enhanceTableVisibility();
addQuickFilters();
addCopyStyles();
addCopyFunctionality();
log('Wszystkie ulepszenia zostały zastosowane');
} catch (error) {
console.error(`${SCRIPT_NAME} Błąd:`, error);
}
}
/**
* Poprawia widoczność tabeli
*/
function enhanceTableVisibility() {
const table = document.querySelector('.o_list_table');
if (!table) return;
// Sprawdź czy style już zostały dodane
if (document.querySelector('#kfz-table-styles')) {
log('Style tabeli już istnieją - pomijam dodawanie');
return;
}
// Dodaj style CSS dla lepszej czytelności
const style = document.createElement('style');
style.id = 'kfz-table-styles';
style.textContent = `
.o_list_table .text-danger {
background-color: #fff5f5 !important;
border-left: 3px solid #dc3545 !important;
}
.o_list_table .o_data_row:hover {
background-color: #f8f9fa !important;
}
.o_list_table th {
position: sticky;
top: 0;
background-color: #fff !important;
z-index: 10;
}
`;
document.head.appendChild(style);
log('Zastosowano ulepszenia wizualne tabeli');
}
/**
* Dodaje szybkie filtry do panelu kontrolnego
*/
function addQuickFilters() {
// Sprawdź czy filtry już istnieją
if (document.querySelector('.kfz-filter-container')) {
log('Filtry już istnieją - pomijam dodawanie');
return;
}
// Znajdź lewą część panelu kontrolnego (za przyciskami)
const leftPanel = document.querySelector('.o_cp_left .o_cp_buttons');
if (!leftPanel) {
log('Nie znaleziono lewego panelu - dodaję filtr przed tabelą');
addQuickFiltersBeforeTable();
return;
}
// Pobierz unikalnych dostawców z wierszy danych
const suppliers = new Set();
const rows = document.querySelectorAll('.o_data_row');
rows.forEach(row => {
const supplierCell = row.children[3]; // Dostawca jest w komórce 3
if (supplierCell) {
const supplierName = supplierCell.textContent.trim();
if (supplierName) {
suppliers.add(supplierName);
}
}
});
if (suppliers.size === 0) {
log('Brak dostawców do filtrowania');
return;
}
// Utwórz kontener filtra - będzie za przyciskami w lewym panelu
const filterContainer = document.createElement('div');
filterContainer.className = 'kfz-filter-container';
filterContainer.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
margin-left: 15px;
height: 32px;
`;
// Etykieta z ikoną
const label = document.createElement('span');
label.innerHTML = '<i class="fa fa-filter"></i> Dostawca:';
label.style.cssText = `
color: #6c757d;
font-size: 13px;
white-space: nowrap;
font-weight: normal;
`;
// Select z dostawcami - styl podobny do innych kontrolek Odoo
const supplierFilter = document.createElement('select');
supplierFilter.className = 'form-control';
supplierFilter.style.cssText = `
height: 32px;
padding: 4px 8px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 13px;
background-color: white;
min-width: 200px;
max-width: 250px;
line-height: 1.5;
`;
// Opcja "Wszyscy"
const allOption = document.createElement('option');
allOption.value = '';
allOption.textContent = 'Wszyscy dostawcy';
supplierFilter.appendChild(allOption);
// Opcje dla każdego dostawcy
Array.from(suppliers).sort().forEach(supplier => {
const option = document.createElement('option');
option.value = supplier;
option.textContent = supplier;
supplierFilter.appendChild(option);
});
// Event listener dla filtra dostawców
supplierFilter.addEventListener('change', () => {
filterBySupplier(supplierFilter.value);
});
filterContainer.appendChild(label);
filterContainer.appendChild(supplierFilter);
// Wstaw filtr za przyciskami w lewym panelu
leftPanel.parentNode.appendChild(filterContainer);
log('Dodano filtr dostawców za przyciskami w lewym panelu');
}
/**
* Fallback - dodaje filtry przed tabelą jeśli nie ma panelu wyszukiwania
*/
function addQuickFiltersBeforeTable() {
const table = document.querySelector('.o_list_table');
if (!table) return;
// Kontener dla filtrów (stary styl)
const filterContainer = document.createElement('div');
filterContainer.className = 'kfz-filter-container';
filterContainer.style.cssText = `
margin: 10px 0;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
display: flex;
gap: 10px;
align-items: center;
`;
// Filtr po dostawcy
const supplierFilter = document.createElement('select');
supplierFilter.style.cssText = 'padding: 5px; border-radius: 3px; border: 1px solid #ccc;';
supplierFilter.innerHTML = '<option value="">Wszyscy dostawcy</option>';
// Pobierz unikalnych dostawców
const suppliers = new Set();
const rows = document.querySelectorAll('.o_data_row');
rows.forEach(row => {
const supplierCell = row.children[3];
if (supplierCell) {
const supplierName = supplierCell.textContent.trim();
if (supplierName) {
suppliers.add(supplierName);
}
}
});
suppliers.forEach(supplier => {
const option = document.createElement('option');
option.value = supplier;
option.textContent = supplier;
supplierFilter.appendChild(option);
});
// Event listener dla filtra dostawców
supplierFilter.addEventListener('change', () => {
filterBySupplier(supplierFilter.value);
});
filterContainer.appendChild(document.createTextNode('Filtruj: '));
filterContainer.appendChild(supplierFilter);
// Wstaw filtry przed tabelą
table.parentNode.insertBefore(filterContainer, table);
log('Dodano filtry przed tabelą (fallback)');
}
/**
* Filtruje wiersze według dostawcy
* @param {string} supplier - Nazwa dostawcy
*/
function filterBySupplier(supplier) {
const rows = document.querySelectorAll('.o_data_row');
rows.forEach(row => {
const supplierCell = row.children[3]; // Dostawca jest w komórce 3
if (!supplierCell) return;
const supplierName = supplierCell.textContent.trim();
const shouldShow = !supplier || supplierName === supplier;
row.style.display = shouldShow ? '' : 'none';
});
log(`Filtrowanie według dostawcy: ${supplier || 'wszyscy'}`);
}
/**
* Kopiuje tekst do schowka (jak w idea-orders.user.js)
* @param {string} text - Tekst do skopiowania
*/
async function tmCopyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (error) {
// Fallback dla starszych przeglądarek
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
}
/**
* Czyni komórkę kopiowalną (styl z idea-orders.user.js)
* @param {Element} td - Komórka tabeli
*/
function makeCellCopyable(td) {
if (!td || td.dataset.tmCopyable) return;
td.dataset.tmCopyable = '1';
td.classList.add('tm-copyable');
td.addEventListener('click', async (e) => {
e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu
let val = (td.textContent || '').trim();
if (!val) return;
// Dla kwot usuń "zł" i zbędne spacje oraz znaki specjalne
if (val.includes('zł')) {
val = val.replace(/\s*zł\s*$/, '').trim();
// Usuń też znaki niełamliwych spacji i inne znaki formatujące
val = val.replace(/[\u00A0\u202F\u2009]/g, ' ').trim();
}
try {
await tmCopyToClipboard(val);
const old = td.title;
td.title = `Skopiowano: ${val}`;
td.classList.add('tm-copied');
setTimeout(() => {
td.title = old || 'Skopiowano';
td.classList.remove('tm-copied');
}, 1200);
log(`Skopiowano: ${val}`);
} catch (error) {
console.error('[Tampermonkey] Błąd podczas kopiowania komórki:', error);
td.title = 'Błąd podczas kopiowania';
}
}, true); // capture=true
}
/**
* Dodaje funkcjonalność kopiowania do wybranych kolumn
*/
function addCopyFunctionality() {
// Znajdź wszystkie wiersze danych
const dataRows = document.querySelectorAll('.o_data_row');
log(`Znaleziono ${dataRows.length} wierszy danych`);
if (dataRows.length === 0) {
log('Brak wierszy danych - sprawdzam strukturę tabeli');
const allRows = document.querySelectorAll('tr');
log(`Wszystkich wierszy: ${allRows.length}`);
allRows.forEach((row, index) => {
if (index < 5) { // Pokaż tylko pierwsze 5
log(`Wiersz ${index}: klasy="${row.className}", komórek=${row.children.length}`);
}
});
}
dataRows.forEach((row, rowIndex) => {
log(`Przetwarzam wiersz ${rowIndex}`);
// Debuguj wszystkie komórki w wierszu (tylko pierwszy wiersz żeby nie spamować)
if (rowIndex === 0) {
log(`=== DEBUGOWANIE STRUKTURY PIERWSZEGO WIERSZA ===`);
Array.from(row.children).forEach((cell, cellIndex) => {
const dataName = cell.getAttribute('data-name');
const text = cell.textContent.trim();
const classes = cell.className;
log(` Komórka ${cellIndex}: data-name="${dataName}", klasy="${classes}", tekst="${text.substring(0, 40)}"`);
});
log(`=== KONIEC DEBUGOWANIA STRUKTURY ===`);
}
// Numer faktury - komórka 2 (na podstawie debugowania)
let numberCell = row.children[2];
if (numberCell && numberCell.textContent.includes('KFZ/')) {
makeCellCopyable(numberCell);
log(`✓ Dodano kopiowanie dla numeru: "${numberCell.textContent.trim()}"`);
} else if (numberCell) {
log(`Pominięto kopiowanie numeru: "${numberCell.textContent.trim()}" (brak KFZ/)`);
} else {
log(`Nie znaleziono komórki numeru (indeks 2)`);
}
// Data faktury - komórka 4 (na podstawie debugowania)
let dateCell = row.children[4];
if (dateCell && dateCell.textContent.match(/\d{2}\.\d{2}\.\d{4}/)) {
makeCellCopyable(dateCell);
log(`✓ Dodano kopiowanie dla daty: "${dateCell.textContent.trim()}"`);
} else if (dateCell) {
log(`Pominięto kopiowanie daty: "${dateCell.textContent.trim()}" (nie pasuje format)`);
} else {
log(`Nie znaleziono komórki daty (indeks 4)`);
}
// Dokument źródłowy - komórka 5 (na podstawie debugowania: "ZZ/00310")
let invoiceOriginCell = row.children[5];
if (invoiceOriginCell) {
const invoiceOriginText = invoiceOriginCell.textContent.trim();
log(`Znaleziono dokument źródłowy: "${invoiceOriginText}"`);
if (invoiceOriginText && invoiceOriginText !== '' && invoiceOriginText !== '-') {
makeCellCopyable(invoiceOriginCell);
log(`✓ Dodano kopiowanie dla dokumentu źródłowego: "${invoiceOriginText}"`);
} else {
log(`Dokument źródłowy pusty lub myślnik: "${invoiceOriginText}"`);
}
} else {
log(`❌ Nie znaleziono komórki dokumentu źródłowego (indeks 5)`);
}
// Odnośnik - komórka 6 (na podstawie debugowania: "KFAS/2025/10/269/PL")
let refCell = row.children[6];
if (refCell) {
const refText = refCell.textContent.trim();
log(`Znaleziono odnośnik: "${refText}"`);
if (refText && refText !== '' && refText !== '-') {
makeCellCopyable(refCell);
log(`✓ Dodano kopiowanie dla odnośnika: "${refText}"`);
} else {
log(`Odnośnik pusty lub myślnik: "${refText}"`);
}
} else {
log(`❌ Nie znaleziono komórki odnośnika (indeks 6)`);
}
// Należność - komórka 10 (na podstawie debugowania: "1 123,80 zł")
let amountCell = row.children[10];
if (amountCell) {
const amountText = amountCell.textContent.trim();
log(`Znaleziono należność: "${amountText}"`);
if (amountText.includes('zł')) {
makeCellCopyable(amountCell);
log(`✓ Dodano kopiowanie dla należności: "${amountText}"`);
} else {
log(`Należność bez 'zł': "${amountText}"`);
}
} else {
log(`❌ Nie znaleziono komórki należności (indeks 10)`);
}
// Na podstawie - komórka 11 (na podstawie debugowania: "KFZ/283/09/2025 (KFAS/2025/09/799/PL)")
let basisCell = row.children[11];
if (basisCell) {
const basisText = basisCell.textContent.trim();
log(`Znaleziono "Na podstawie": "${basisText}"`);
if (basisText && basisText !== '' && basisText !== '-') {
makeCellCopyable(basisCell);
log(`✓ Dodano kopiowanie dla "Na podstawie": "${basisText}"`);
} else {
log(`Na podstawie puste lub myślnik: "${basisText}"`);
}
} else {
log(`❌ Nie znaleziono komórki "Na podstawie" (indeks 11)`);
}
});
log('Zakończono dodawanie funkcjonalności kopiowania');
}
/**
* Dodaje style CSS dla funkcjonalności kopiowania (dokładnie jak w idea-orders.user.js)
*/
function addCopyStyles() {
// Sprawdź czy style już zostały dodane
if (document.querySelector('#kfz-copy-styles')) {
log('Style kopiowania już istnieją - pomijam dodawanie');
return;
}
const style = document.createElement('style');
style.id = 'kfz-copy-styles';
style.textContent = `
/* Kopiowanie ikona tuż za liczbą (inline), mniejsza, pomarańczowa */
td.tm-copyable { cursor: copy; }
td.tm-copyable::after {
content: '⧉'; /* symbol kopiowania w kolorze CSS */
display: inline-block; /* w tej samej linii co liczba */
margin-left: 6px; /* mały odstęp od liczby */
font-size: 12px; /* mniejszy rozmiar */
line-height: 1;
color: #ff9800; /* pomarańczowy */
opacity: .75; /* widoczna także bez hovera */
vertical-align: baseline;
pointer-events: none;
transition: opacity .15s ease;
}
td.tm-copyable:hover::after { opacity: 1; }
td.tm-copyable.tm-copied {
outline: 2px solid #ff9800;
background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0));
}
`;
document.head.appendChild(style);
}
/**
* Czeka na załadowanie elementu
* @param {string} selector - Selektor CSS
* @param {number} timeout - Timeout w ms
* @returns {Promise<Element>}
*/
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} nie został znaleziony w czasie ${timeout}ms`));
}, timeout);
});
}
// Przechwytywanie żądań AJAX do search_read
function interceptAjaxRequests() {
const originalFetch = window.fetch;
window.fetch = function(...args) {
const promise = originalFetch.apply(this, args);
// Sprawdź czy to żądanie do search_read
if (args[0] && args[0].includes('/web/dataset/search_read')) {
promise.then(response => {
if (response.ok) {
log('Wykryto żądanie AJAX search_read - uruchamiam skrypt po opóźnieniu');
setTimeout(initEnhancements, 1500); // Większe opóźnienie dla AJAX
}
});
}
return promise;
};
// Przechwytywanie XMLHttpRequest (backup)
const originalXHR = window.XMLHttpRequest.prototype.open;
window.XMLHttpRequest.prototype.open = function(method, url, ...args) {
if (url && url.includes('/web/dataset/search_read')) {
this.addEventListener('load', () => {
log('Wykryto XHR search_read - uruchamiam skrypt po opóźnieniu');
setTimeout(initEnhancements, 1500);
});
}
return originalXHR.call(this, method, url, ...args);
};
}
// Inicjalizacja skryptu z obserwacją zmian
function startScript() {
log('🚀 SKRYPT ZAŁADOWANY - WERSJA 3.0');
log(`📍 URL: ${window.location.href}`);
// Przechwytuj żądania AJAX
interceptAjaxRequests();
// Pierwsze uruchomienie
setTimeout(initEnhancements, 1000);
// Obserwuj zmiany w DOM (dla dynamicznych stron)
const observer = new MutationObserver((mutations) => {
// Sprawdź czy dodano nowe wiersze danych
const hasNewDataRows = mutations.some(mutation =>
Array.from(mutation.addedNodes).some(node =>
node.nodeType === 1 && (
node.classList?.contains('o_data_row') ||
node.querySelector?.('.o_data_row')
)
)
);
if (hasNewDataRows) {
log('Wykryto nowe wiersze danych - uruchamiam skrypt');
setTimeout(initEnhancements, 800);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Obserwuj zmiany URL (hash) - kilka metod dla pewności
let lastUrl = window.location.href;
// 1. Standardowy hashchange
window.addEventListener('hashchange', () => {
log('Wykryto hashchange - resetuję stan i uruchamiam skrypt');
// Resetuj stan przy zmianie URL
const scriptMarker = document.querySelector('.kfz-script-initialized');
if (scriptMarker) {
scriptMarker.remove();
log('Usunięto marker inicjalizacji przy hashchange');
}
setTimeout(initEnhancements, 500);
setTimeout(initEnhancements, 1500); // Backup po dłuższym czasie
});
// 2. Obserwuj zmiany URL przez polling
setInterval(() => {
const currentUrl = window.location.href;
if (currentUrl !== lastUrl) {
log(`URL zmieniony z ${lastUrl} na ${currentUrl}`);
lastUrl = currentUrl;
// Resetuj stan przy zmianie URL
const scriptMarker = document.querySelector('.kfz-script-initialized');
if (scriptMarker) {
scriptMarker.remove();
log('Usunięto marker inicjalizacji przy zmianie URL');
}
setTimeout(initEnhancements, 300);
setTimeout(initEnhancements, 1000);
}
}, 500);
// 3. Obserwuj zmiany w tytule strony (Odoo często zmienia tytuł)
let lastTitle = document.title;
const titleObserver = new MutationObserver(() => {
if (document.title !== lastTitle) {
log(`Tytuł zmieniony z "${lastTitle}" na "${document.title}"`);
lastTitle = document.title;
setTimeout(initEnhancements, 400);
}
});
titleObserver.observe(document.querySelector('title') || document.head, {
childList: true,
characterData: true,
subtree: true
});
// 4. Obserwuj zmiany w breadcrumb (ścieżka nawigacji Odoo)
const breadcrumbObserver = new MutationObserver(() => {
log('Wykryto zmiany w breadcrumb - sprawdzam czy to strona KFZ');
setTimeout(initEnhancements, 200);
});
// Obserwuj breadcrumb jeśli istnieje
const breadcrumbElement = document.querySelector('.breadcrumb, .o_cp_controller');
if (breadcrumbElement) {
breadcrumbObserver.observe(breadcrumbElement, {
childList: true,
characterData: true,
subtree: true
});
}
// 5. Dodatkowe sprawdzenie co 3 sekundy (backup)
setInterval(() => {
const url = window.location.href;
const isAccountMoveList = url.includes('model=account.move') && url.includes('view_type=list');
const hasFilter = document.querySelector('.kfz-filter-container');
const breadcrumbCheck = document.querySelector('.breadcrumb');
const hasCorrectionsBreadcrumb = breadcrumbCheck && breadcrumbCheck.textContent.includes('Korekty');
if (isAccountMoveList && !hasFilter && (isKfzListingPage() || hasCorrectionsBreadcrumb)) {
log('Backup check - wykryto stronę account.move bez filtra, uruchamiam skrypt');
initEnhancements();
}
}, 3000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startScript);
} else {
startScript();
}
})();