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