// ==UserScript== // @name IDEAERP - Zamówienia // @namespace http://tampermonkey.net/ // @version 3.7 // @description Dodano kopiowanie nazwy zamówienia, NIP faktury i notatek - kliknij aby skopiować // @match https://emma.ideaerp.pl/web* // @icon https://emma.ideaerp.pl/web/image/res.company/1/favicon/ // @downloadURL https://n8n.emma.net.pl/webhook/order // @updateURL https://n8n.emma.net.pl/webhook/order // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // ==/UserScript== /* ========================OPIS============================== WERSJA 3.7 - Rozszerzone kopiowanie elementów WYMAGANE KOLUMNY w tabeli IdeaERP: - product_uom_qty → Ilość - order_line_netto → Netto (lub price_subtotal jako zamiennik) - order_line_gross → Brutto - price_unit → Cena jednostkowa - product_id → Produkt DZIAŁANIE SKRYPTU: - Wszystkie funkcje z v3.6 PLUS: - Kopiowanie nazwy zamówienia - kliknij na nazwę zamówienia (h1) aby skopiować - Kopiowanie NIP faktury - kliknij na element span[name="invoice_vat"] aby skopiować - Kopiowanie notatek - kliknij na element span[name="note"] aby skopiować - Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules - Naprawione wyświetlanie kolumny "Cena jednostkowa" po włączeniu - Lepsze zarządzanie szerokościami tabeli - Optymalizacja layoutu dla różnych rozdzielczości POPRAWKI WZGLĘDEM v3.1: + Naprawione zarządzanie szerokością tabeli (table-layout: auto !important) + Zmniejszone domyślne szerokości kolumn (90px zamiast 120px) + Ulepszona funkcja setPriceUnitVisibility() z lepszym zarządzaniem stylów + Konkretne wymiary dla kolumny "Cena jednostkowa" (120px-180px) + Wymuszenie elastycznego layoutu przy pokazywaniu kolumny + Czyszczenie stylów komórek przy przełączaniu widoczności + Dodatkowe zabezpieczenia w processOnce() dla table-layout ROZWIĄZANE PROBLEMY: - Kolumna "Cena jednostkowa" nie była widoczna po kliknięciu "Pokaż" - Konflikty między table-layout: fixed a dynamicznymi kolumnami - Problemy z szerokościami na różnych rozdzielczościach - Brak miejsca w tabeli po dodaniu nowych kolumn ZAPAMIĘTYWANIE USTAWIEŃ: - Wszystkie ustawienia (widoczność kolumn, tryb kopiowania, szerokości) są zapisywane w trwałej pamięci przeglądarki. - Ustawienia przetrwają: restart komputera, zamknięcie przeglądarki, odświeżenie strony, aktualizację przeglądarki. - Ustawienia zostaną utracone tylko przy: usunięciu skryptu, wyczyszczeniu danych przeglądarki lub zmianie komputera. AUTOMATYCZNE AKTUALIZACJE: - Serwer aktualizacji: https://n8n.emma.net.pl/webhook/order - Tampermonkey automatycznie sprawdza dostępność nowych wersji - Jednoklikowa aktualizacja bez konieczności ręcznego pobierania - Zachowanie ustawień użytkownika podczas aktualizacji - Kompatybilność z systemem n8n do zarządzania wersjami CHANGELOG: v3.7 (2024) - ROZSZERZONE KOPIOWANIE ELEMENTÓW - Dodano kopiowanie nazwy zamówienia (h1) z ikoną ⧉ - Dodano kopiowanie NIP faktury (span[name="invoice_vat"]) z ikoną ⧉ - Dodano kopiowanie notatek (span[name="note"]) z ikoną ⧉ - Dodano style CSS dla elementów span z ikonami kopiowania - Dodano funkcję makeSpanCopyable() dla uniwersalnego kopiowania span - Obsługa błędów w funkcjach kopiowania zgodnie z zasadami .rules - Spójny styl wizualny ikon kopiowania (pomarańczowy kolor #ff9800) - Podwyższenie numeru wersji do 3.7 v3.6 (2024) - KOPIOWANIE NAZWY ZAMÓWIENIA - Dodano kopiowanie nazwy zamówienia z ikoną ⧉ - Dodano style CSS dla elementu h1 z ikoną kopiowania - Dodano funkcję makeOrderNameCopyable() z obsługą błędów - Poprawiono selektor elementu (h1 zamiast span) - Podwyższenie numeru wersji do 3.6 v3.5 (2024) - AUTOMATYCZNE AKTUALIZACJE - Dodano dokumentację automatycznych aktualizacji w sekcji OPIS - Dodano sekcję "AUTOMATYCZNE AKTUALIZACJE" z opisem funkcjonalności - Dokumentacja serwera aktualizacji (n8n.emma.net.pl/webhook/order) - Informacja o zachowaniu ustawień podczas aktualizacji - Podwyższenie numeru wersji do 3.5 v3.4 (2024) - STABILIZACJA - Stabilna wersja z pełną funkcjonalnością - Wszystkie poprzednie poprawki zintegrowane - Gotowość do automatycznych aktualizacji v3.3 (2024) - DROBNE POPRAWKI - Dodatkowe optymalizacje wydajności - Poprawki w obsłudze błędów - Ulepszenia w interfejsie użytkownika v3.2 (2024) - POPRAWKA LAYOUTU - Naprawione zarządzanie szerokością tabeli (table-layout: auto !important) - Zmniejszone domyślne szerokości kolumn (90px zamiast 120px) - Ulepszona funkcja setPriceUnitVisibility() z lepszym zarządzaniem stylów - Konkretne wymiary dla kolumny "Cena jednostkowa" (120px-180px) - Wymuszenie elastycznego layoutu przy pokazywaniu kolumny - Czyszczenie stylów komórek przy przełączaniu widoczności - Dodatkowe zabezpieczenia w processOnce() dla table-layout - Rozwiązano problem z niewidoczną kolumną "Cena jednostkowa" - Optymalizacja dla różnych rozdzielczości ekranu ============================================================ */ (function () { 'use strict'; // ——— Klucze stanu ——— const STATE_KEY = 'tm_show_price_unit_column_v41'; // true = widoczna, false = ukryta (domyślnie false) const WIDTHS_KEY = 'tm_col_widths_v41'; const INIT_FLAG = 'tm_inited_v41'; const COPY_MODE_KEY = 'tm_product_copy_mode_v31'; // true = SKU, false = pełna nazwa (domyślnie true) const TABLE_SEL = 'table.o_list_table.o_section_and_note_list_view'; GM_addStyle(` /* Poprawka dla tabeli - pozwolenie na elastyczne szerokości */ ${TABLE_SEL} { table-layout: auto !important; } th.tm-col, td.tm-col { text-align: right !important; font-size: 13px !important; } th.tm-col { min-width: 90px !important; position: relative; } th.tm-col .tm-resize { position: absolute; right: 0; top: 0; bottom: 0; width: 6px; cursor: col-resize; } th.tm-col.tm-sort-asc::after { content: ' ▲'; font-size: 10px; } th.tm-col.tm-sort-desc::after { content: ' ▼'; font-size: 10px; } /* Zmniejszona szerokość kolumny price_unit dla lepszego dopasowania */ th[data-name="price_unit"] { min-width: 120px !important; width: 120px !important; max-width: 180px !important; } th[data-name="price_unit"] .custom-tm-span-text { color: orange; font-weight: 600; } /* Dodatkowe zabezpieczenie ukrywania po atrybucie */ table.tm-hide-price-unit th[data-name="price_unit"], table.tm-hide-price-unit td[data-name="price_unit"] { display: none !important; } /* 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)); } /* Kopiowanie nazwy zamówienia – ikona dla h1 */ h1.tm-copyable { cursor: copy; } h1.tm-copyable::after { content: '⧉'; /* symbol kopiowania */ display: inline-block; /* w tej samej linii co tekst */ margin-left: 8px; /* odstęp od tekstu */ font-size: 14px; /* większy rozmiar dla h1 */ line-height: 1; color: #ff9800; /* pomarańczowy */ opacity: .75; /* widoczna także bez hovera */ vertical-align: baseline; pointer-events: none; transition: opacity .15s ease; } h1.tm-copyable:hover::after { opacity: 1; } h1.tm-copyable.tm-copied { outline: 2px solid #ff9800; background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0)); } /* Kopiowanie dla elementów span z określonymi name */ span.tm-copyable { cursor: copy; } span.tm-copyable::after { content: '⧉'; /* symbol kopiowania */ display: inline-block; /* w tej samej linii co tekst */ margin-left: 6px; /* odstęp od tekstu */ font-size: 12px; /* rozmiar jak dla komórek tabeli */ line-height: 1; color: #ff9800; /* pomarańczowy */ opacity: .75; /* widoczna także bez hovera */ vertical-align: baseline; pointer-events: none; transition: opacity .15s ease; } span.tm-copyable:hover::after { opacity: 1; } span.tm-copyable.tm-copied { outline: 2px solid #ff9800; background-image: linear-gradient(90deg, rgba(255,152,0,.12), rgba(255,152,0,0)); } `); // ——— Pomocnicze ——— const nfPL = new Intl.NumberFormat('pl-PL', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const fmt = n => (isFinite(n) ? nfPL.format(n) : ''); function isOrderForm() { const params = new URLSearchParams((location.hash || '').replace(/^#/, '')); const model = params.get('model'); const viewType = params.get('view_type'); return (model === 'sale.order' || model === 'shipping.order') && viewType === 'form'; } function parseNumber(str) { if (!str) return NaN; return parseFloat(String(str) .replace(/[\u00A0\u202F]/g, '') // nbsp/thin space .replace(/[^\d,.-]/g, '') .replace(/\.(?=\d{3}(\D|$))/g, '') // kropki tys. .replace(/,(?=\d{0,2}$)/, '.')); // przecinek -> kropka } const headerRow = t => (t && t.tHead) ? t.tHead.rows[t.tHead.rows.length - 1] : t.querySelector('thead tr'); const thIndex = (t, name) => { const hr = headerRow(t); if (!hr) return -1; const arr = Array.from(hr.children); for (let i = 0; i < arr.length; i++) if (arr[i].getAttribute('data-name') === name) return i; return -1; }; const headerMap = t => { const hr = headerRow(t); const m = {}; if (!hr) return m; Array.from(hr.children).forEach((th, i) => { const dn = th.getAttribute('data-name'); if (dn) m[dn] = i; }); return m; }; const cellAt = (row, i) => { const c = row && row.children; return (c && i >= 0 && i < c.length) ? c[i] : null; }; const textOf = el => el ? (el.getAttribute('title') || el.textContent || '').trim() : ''; // ——— Funkcja wyodrębniania SKU z nawiasów kwadratowych ——— function extractSKU(productText) { const match = productText.match(/\[([^\]]+)\]/); return match ? match[1] : productText; } // ——— Schowek: kopiowanie wartości z komórek ——— async function tmCopyToClipboard(text) { try { await navigator.clipboard.writeText(text); } catch { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } } 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 const val = (td.textContent || '').trim(); if (!val) return; 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); } catch (error) { console.error('[Tampermonkey] Błąd podczas kopiowania komórki:', error); td.title = 'Błąd podczas kopiowania'; } }, true); // capture=true } // ——— Kopiowanie produktów z trybem SKU/pełna nazwa ——— function makeProductCellCopyable(td) { if (!td || td.dataset.tmProductCopyable) return; td.dataset.tmProductCopyable = '1'; td.classList.add('tm-copyable'); td.addEventListener('click', async (e) => { e.stopPropagation(); // nie wyzwalaj akcji Odoo na wierszu const fullText = (td.textContent || '').trim(); if (!fullText) return; try { const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true); // domyślnie SKU const textToCopy = copyOnlySKU ? extractSKU(fullText) : fullText; await tmCopyToClipboard(textToCopy); const old = td.title; const mode = copyOnlySKU ? 'SKU' : 'pełną nazwę'; td.title = `Skopiowano ${mode}: ${textToCopy}`; td.classList.add('tm-copied'); setTimeout(() => { td.title = old || 'Skopiowano'; td.classList.remove('tm-copied'); }, 1200); } catch (error) { console.error('[Tampermonkey] Błąd podczas kopiowania produktu:', error); td.title = 'Błąd podczas kopiowania'; } }, true); // capture=true } // ——— Kopiowanie nazwy zamówienia ——— function makeOrderNameCopyable(h1Element) { if (!h1Element || h1Element.dataset.tmOrderNameCopyable) return; h1Element.dataset.tmOrderNameCopyable = '1'; h1Element.classList.add('tm-copyable'); h1Element.style.cursor = 'pointer'; h1Element.title = 'Kliknij aby skopiować nazwę zamówienia'; h1Element.addEventListener('click', async (e) => { e.stopPropagation(); const orderName = (h1Element.textContent || '').trim(); if (!orderName) return; try { await tmCopyToClipboard(orderName); const old = h1Element.title; h1Element.title = `Skopiowano nazwę zamówienia: ${orderName}`; h1Element.classList.add('tm-copied'); setTimeout(() => { h1Element.title = old || 'Kliknij aby skopiować nazwę zamówienia'; h1Element.classList.remove('tm-copied'); }, 1200); } catch (error) { console.error('[Tampermonkey] Błąd podczas kopiowania nazwy zamówienia:', error); h1Element.title = 'Błąd podczas kopiowania'; } }, true); // capture=true } // ——— Kopiowanie elementów span z określonymi name ——— function makeSpanCopyable(spanElement, fieldName) { if (!spanElement || spanElement.dataset.tmSpanCopyable) return; spanElement.dataset.tmSpanCopyable = '1'; spanElement.classList.add('tm-copyable'); spanElement.style.cursor = 'pointer'; spanElement.title = `Kliknij aby skopiować ${fieldName}`; spanElement.addEventListener('click', async (e) => { e.stopPropagation(); const text = (spanElement.textContent || '').trim(); if (!text) return; try { await tmCopyToClipboard(text); const old = spanElement.title; spanElement.title = `Skopiowano ${fieldName}: ${text}`; spanElement.classList.add('tm-copied'); setTimeout(() => { spanElement.title = old || `Kliknij aby skopiować ${fieldName}`; spanElement.classList.remove('tm-copied'); }, 1200); } catch (error) { console.error(`[Tampermonkey] Błąd podczas kopiowania ${fieldName}:`, error); spanElement.title = 'Błąd podczas kopiowania'; } }, true); // capture=true } // ——— Nagłówki (nasze, bez Odoo) ——— function ensureCustomHeaders(table) { const hr = headerRow(table); if (!hr) return; let pIdx = thIndex(table, 'price_unit'); if (pIdx < 0) return; if (!table.querySelector('th[data-name="tm_unit_netto"]')) { const th = document.createElement('th'); th.setAttribute('data-name', 'tm_unit_netto'); th.className = 'tm-col'; th.textContent = 'Cena jedn. netto'; th.title = 'Cena jedn. netto = Netto / Ilość'; th.appendChild(document.createElement('span')).className = 'tm-resize'; const ref = cellAt(hr, pIdx + 1); ref ? hr.insertBefore(th, ref) : hr.appendChild(th); } pIdx = thIndex(table, 'price_unit'); if (!table.querySelector('th[data-name="tm_unit_brutto"]')) { const th = document.createElement('th'); th.setAttribute('data-name', 'tm_unit_brutto'); th.className = 'tm-col'; th.textContent = 'Cena jedn. brutto'; th.title = 'Cena jedn. brutto = Cena jedn. netto × (Brutto/Netto)'; th.appendChild(document.createElement('span')).className = 'tm-resize'; const ref = cellAt(hr, pIdx + 2); ref ? hr.insertBefore(th, ref) : hr.appendChild(th); } } // ——— Komórki wierszy ——— function ensureRowCells(row, priceIdx) { let tdN = row.querySelector('td[data-name="tm_unit_netto"]'); if (!tdN) { tdN = document.createElement('td'); tdN.setAttribute('data-name', 'tm_unit_netto'); tdN.className = 'tm-col o_data_cell o_field_cell o_list_number'; tdN.tabIndex = -1; tdN.title = 'Cena jedn. netto'; const ref = cellAt(row, priceIdx + 1); ref ? row.insertBefore(tdN, ref) : row.appendChild(tdN); } let tdB = row.querySelector('td[data-name="tm_unit_brutto"]'); if (!tdB) { tdB = document.createElement('td'); tdB.setAttribute('data-name', 'tm_unit_brutto'); tdB.className = 'tm-col o_data_cell o_field_cell o_list_number'; tdB.tabIndex = -1; tdB.title = 'Cena jedn. brutto'; const ref2 = cellAt(row, priceIdx + 2); ref2 ? row.insertBefore(tdB, ref2) : row.appendChild(tdB); } // Uczyń kopiowalnymi makeCellCopyable(tdN); makeCellCopyable(tdB); } function ensureAllRowCells(table, priceIdx) { table.querySelectorAll('tbody tr').forEach(tr => ensureRowCells(tr, priceIdx)); } // ——— Kopiowanie produktów ——— function ensureProductCopyable(table, map) { const prodIdx = map["product_id"]; // kolumna Produkt if (prodIdx == null) return; table.querySelectorAll("tbody tr").forEach((tr) => { const td = cellAt(tr, prodIdx); if (td) makeProductCellCopyable(td); }); } // ——— Wyliczenia + chmurki ——— function tagPriceUnitHeader(table, map) { const th = table.querySelector('th[data-name="price_unit"]'); if (!th) return; const row = table.querySelector('tbody tr'); if (!row) return; const qty = parseNumber(textOf(cellAt(row, map['product_uom_qty']))); const net = parseNumber(textOf(cellAt(row, map['order_line_netto'] ?? map['price_subtotal']))); const unit = parseNumber(textOf(cellAt(row, map['price_unit']))); if (!isFinite(qty) || qty <= 0 || !isFinite(net) || !isFinite(unit)) return; const unitNet = net / qty; const txt = (Math.abs(unitNet - unit) < 0.0001) ? 'netto' : (unitNet < unit ? 'brutto' : 'jedn.'); let span = th.querySelector('.custom-tm-span-text'); if (!span) { th.appendChild(document.createTextNode(' ')); span = document.createElement('span'); span.className = 'custom-tm-span-text'; th.appendChild(span); } span.textContent = txt; th.title = `Cena jednostkowa ${txt}`; } function fillValues(table) { const map = headerMap(table); tagPriceUnitHeader(table, map); const qtyIdx = map['product_uom_qty']; const netIdx = map['order_line_netto'] ?? map['price_subtotal']; const grossIdx = map['order_line_gross']; table.querySelectorAll('tbody tr').forEach(row => { const qty = parseNumber(textOf(cellAt(row, qtyIdx))); const net = parseNumber(textOf(cellAt(row, netIdx))); const gross = parseNumber(textOf(cellAt(row, grossIdx))); const outN = row.querySelector('td[data-name="tm_unit_netto"]'); const outB = row.querySelector('td[data-name="tm_unit_brutto"]'); if (!isFinite(qty) || qty <= 0 || !isFinite(net) || net <= 0) { if (outN) { outN.textContent = ''; outN.title = 'Cena jedn. netto'; } if (outB) { outB.textContent = ''; outB.title = 'Cena jedn. brutto'; } return; } const unitNet = net / qty; if (outN) { outN.textContent = fmt(unitNet); outN.title = `Cena jedn. netto = ${fmt(net)} / ${fmt(qty)} = ${fmt(unitNet)}`; makeCellCopyable(outN); } let ratio = null; // Brutto/Netto if (isFinite(gross) && gross > 0) { const r = gross / net; if (r >= 1 && r < 5) ratio = r; } if (ratio == null) { const shown = parseNumber(textOf(cellAt(row, map['price_unit']))); if (shown > unitNet + 0.0001) { const r2 = shown / unitNet; if (r2 > 1 && r2 < 5) ratio = r2; } } if (outB) { if (ratio != null) { const ug = unitNet * ratio; outB.textContent = fmt(ug); outB.title = `Cena jedn. brutto = ${fmt(unitNet)} × ${ratio.toFixed(2)} = ${fmt(ug)}`; } else { outB.textContent = ''; outB.title = 'Cena jedn. brutto'; } makeCellCopyable(outB); } }); } // ——— Sortowanie (tylko nasze kolumny) ——— function bindSort(th, table) { th.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); const hr = headerRow(table); const idx = Array.prototype.indexOf.call(hr.children, th); const asc = !th.classList.contains('tm-sort-asc'); table.querySelectorAll('th.tm-col').forEach(x => x.classList.remove('tm-sort-asc', 'tm-sort-desc')); th.classList.add(asc ? 'tm-sort-asc' : 'tm-sort-desc'); const tbody = table.tBodies && table.tBodies[0]; if (!tbody) return; const rows = Array.from(tbody.querySelectorAll('tr.o_data_row, tr')).filter(r => r.querySelector('td')); rows.sort((a, b) => { const va = parseNumber(textOf(cellAt(a, idx))); const vb = parseNumber(textOf(cellAt(b, idx))); const an = isFinite(va), bn = isFinite(vb); if (an && bn) return asc ? (va - vb) : (vb - va); if (an && !bn) return -1; if (!an && bn) return 1; return 0; }); rows.forEach(r => tbody.appendChild(r)); }, true); } // ——— Zmiana szerokości ——— function loadWidths() { try { return JSON.parse(GM_getValue(WIDTHS_KEY, '{}')) || {}; } catch { return {}; } } function saveWidths(w) { GM_setValue(WIDTHS_KEY, JSON.stringify(w || {})); } function setWidthPx(table, th, px) { const w = Math.max(60, Math.round(px)); th.style.width = w + 'px'; th.style.minWidth = w + 'px'; th.style.maxWidth = w + 'px'; const hr = headerRow(table); const idx = Array.prototype.indexOf.call(hr.children, th); table.querySelectorAll('tbody tr, tfoot tr').forEach(tr => { const td = cellAt(tr, idx); if (td) { td.style.width = w + 'px'; td.style.minWidth = w + 'px'; td.style.maxWidth = w + 'px'; } }); } function bindResize(th, table) { const h = th.querySelector('.tm-resize'); if (!h) return; let sx = 0, sw = 0, moving = false; const down = (e) => { e.preventDefault(); e.stopPropagation(); moving = true; sx = e.pageX; sw = th.getBoundingClientRect().width; document.addEventListener('mousemove', move, true); document.addEventListener('mouseup', up, true); document.body.style.userSelect = 'none'; }; const move = (e) => { if (!moving) return; setWidthPx(table, th, sw + (e.pageX - sx)); }; const up = () => { if (!moving) return; moving = false; document.removeEventListener('mousemove', move, true); document.removeEventListener('mouseup', up, true); document.body.style.userSelect = ''; const w = loadWidths(); w[th.getAttribute('data-name')] = Math.round(th.getBoundingClientRect().width); saveWidths(w); }; h.addEventListener('mousedown', down, true); } function applyWidthsAndBind(table) { ['tm_unit_netto', 'tm_unit_brutto'].forEach(name => { const th = table.querySelector(`th[data-name="${name}"]`); if (!th) return; const saved = loadWidths()[name]; if (saved) setWidthPx(table, th, saved); if (!th.dataset.tmBound) { th.dataset.tmBound = '1'; bindResize(th, table); bindSort(th, table); } }); } // ——— Ulepszona funkcja zarządzania widocznością kolumny "Cena jednostkowa" ——— function setPriceUnitVisibility(table, visible) { if (visible) table.classList.remove('tm-hide-price-unit'); else table.classList.add('tm-hide-price-unit'); const hr = headerRow(table); if (!hr) return; const th = table.querySelector('th[data-name="price_unit"]'); if (!th) return; const idx = Array.prototype.indexOf.call(hr.children, th); if (visible) { // Pokazujemy kolumnę - przywracamy normalną szerokość th.style.display = ''; th.style.width = '120px'; th.style.minWidth = '120px'; th.style.maxWidth = '180px'; // Przywracamy widoczność komórek w tej kolumnie table.querySelectorAll('tbody tr, tfoot tr').forEach(tr => { const c = cellAt(tr, idx); if (c) { c.style.display = ''; c.style.width = ''; c.style.minWidth = ''; c.style.maxWidth = ''; } }); // Upewniamy się, że tabela ma elastyczny layout table.style.tableLayout = 'auto'; } else { // Ukrywamy kolumnę th.style.display = 'none'; th.style.width = '0'; th.style.minWidth = '0'; th.style.maxWidth = '0'; table.querySelectorAll('tbody tr, tfoot tr').forEach(tr => { const c = cellAt(tr, idx); if (c) c.style.display = 'none'; }); } } function applyVisibilityAll(visible) { document.querySelectorAll(TABLE_SEL).forEach(t => setPriceUnitVisibility(t, visible)); } // ——— Lokalizowanie docelowego miejsca dla przycisku ——— function findActionsContainer() { const byId = document.getElementById('notebook_page_130'); if (byId) return byId; const pricelistBtn = document.querySelector('button[name="load_from_pricelist"]'); if (pricelistBtn) return pricelistBtn.closest('.tab-pane') || pricelistBtn.parentElement; return null; } // ——— Przycisk Pokaż/Ukryj (btn btn-secondary) ——— function addToggleButton() { const host = findActionsContainer(); if (!host) return; const idBtn = 'tm-toggle-main-button'; if (host.querySelector('#' + idBtn)) return; const btn = document.createElement('button'); btn.type = 'button'; btn.id = idBtn; btn.className = 'btn btn-secondary'; const label = document.createElement('span'); btn.appendChild(label); const refresh = () => { const v = !!GM_getValue(STATE_KEY, false); label.textContent = `${v ? 'Ukryj' : 'Pokaż'} Cenę Jednostkową`; }; btn.addEventListener('click', () => { const v = !GM_getValue(STATE_KEY, false); GM_setValue(STATE_KEY, v); applyVisibilityAll(v); refresh(); }); const massBtn = host.querySelector('button[name="523"]'); if (massBtn && massBtn.parentElement) massBtn.insertAdjacentElement('afterend', btn); else host.appendChild(btn); refresh(); } // ——— Przycisk przełączania trybu kopiowania produktów ——— function addCopyModeToggleButton() { const host = findActionsContainer(); if (!host) return; const idBtn = 'tm-copy-mode-toggle-button'; if (host.querySelector('#' + idBtn)) return; const btn = document.createElement('button'); btn.type = 'button'; btn.id = idBtn; btn.className = 'btn btn-secondary'; const label = document.createElement('span'); btn.appendChild(label); const refresh = () => { const copyOnlySKU = GM_getValue(COPY_MODE_KEY, true); label.textContent = copyOnlySKU ? 'Kopiuję: SKU' : 'Kopiuję: Pełna nazwa'; btn.title = copyOnlySKU ? 'Kliknij aby przełączyć na kopiowanie pełnej nazwy produktu' : 'Kliknij aby przełączyć na kopiowanie tylko SKU produktu'; }; btn.addEventListener('click', () => { const newMode = !GM_getValue(COPY_MODE_KEY, true); GM_setValue(COPY_MODE_KEY, newMode); refresh(); }); // Umieść przycisk obok pierwszego przycisku const mainToggleBtn = host.querySelector('#tm-toggle-main-button'); if (mainToggleBtn && mainToggleBtn.parentElement) { mainToggleBtn.insertAdjacentElement('afterend', btn); } else { const massBtn = host.querySelector('button[name="523"]'); if (massBtn && massBtn.parentElement) massBtn.insertAdjacentElement('afterend', btn); else host.appendChild(btn); } refresh(); } // ——— Menu Tampermonkey (podmenu) ——— GM_registerMenuCommand('Ustawienia ▶', () => { const choice = prompt( 'Ustawienia (wpisz numer):\n' + '1) Pokaż kolumnę „Cena jednostkowa"\n' + '2) Ukryj kolumnę „Cena jednostkowa"\n' + '3) Zresetuj szerokości netto/brutto\n' + '4) Wyczyść stan (domyślnie ukryta)\n' + '5) Test kopiowania produktów\n' + '6) Przełącz tryb kopiowania (SKU ⇄ Pełna nazwa)' ); if (!choice) return; switch (choice.trim()) { case '1': GM_setValue(STATE_KEY, true); applyVisibilityAll(true); alert('Kolumna widoczna.'); break; case '2': GM_setValue(STATE_KEY, false); applyVisibilityAll(false); alert('Kolumna ukryta.'); break; case '3': GM_setValue(WIDTHS_KEY, '{}'); alert('Zresetowano szerokości. Odśwież widok.'); break; case '4': GM_deleteValue(STATE_KEY); applyVisibilityAll(false); alert('Wyczyszczono. Kolumna ustawiona na: ukryta.'); break; case '5': const tables = document.querySelectorAll(TABLE_SEL); if (tables.length > 0) { const table = tables[0]; const map = headerMap(table); ensureProductCopyable(table, map); alert('Funkcja kopiowania produktów została zastosowana do tabeli.'); } else { alert('Nie znaleziono tabeli zamówień na tej stronie.'); } break; case '6': const currentMode = GM_getValue(COPY_MODE_KEY, true); GM_setValue(COPY_MODE_KEY, !currentMode); const newModeText = !currentMode ? 'SKU' : 'Pełna nazwa'; alert(`Tryb kopiowania zmieniony na: ${newModeText}`); break; default: // nic } }); // ——— Główna procedura ——— function processOnce() { if (!isOrderForm()) return; const tables = document.querySelectorAll(TABLE_SEL); if (!tables.length) { addToggleButton(); addCopyModeToggleButton(); return; } if (!GM_getValue(INIT_FLAG)) { GM_setValue(STATE_KEY, false); GM_setValue(INIT_FLAG, true); } // Dodaj kopiowanie nazwy zamówienia const orderNameSpan = document.querySelector('h1 span[name="name"]'); if (orderNameSpan) { const orderNameH1 = orderNameSpan.closest('h1'); if (orderNameH1) { makeOrderNameCopyable(orderNameH1); } } // Dodaj kopiowanie dla elementów span z określonymi name const invoiceVatSpan = document.querySelector('span[name="invoice_vat"]'); if (invoiceVatSpan) { makeSpanCopyable(invoiceVatSpan, 'NIP faktury'); } const noteSpan = document.querySelector('span[name="note"]'); if (noteSpan) { makeSpanCopyable(noteSpan, 'notatkę'); } tables.forEach(table => { // Upewniamy się, że tabela ma elastyczny layout table.style.tableLayout = 'auto'; ensureCustomHeaders(table); const pIdx = thIndex(table, 'price_unit'); if (pIdx < 0) return; ensureAllRowCells(table, pIdx); fillValues(table); // Dodaj kopiowanie produktów const map = headerMap(table); ensureProductCopyable(table, map); setPriceUnitVisibility(table, !!GM_getValue(STATE_KEY, false)); applyWidthsAndBind(table); }); addToggleButton(); addCopyModeToggleButton(); } // ——— Start + reagowanie na zmiany ——— let deb = null; const debounce = (fn, ms = 150) => { clearTimeout(deb); deb = setTimeout(fn, ms); }; window.addEventListener('load', () => debounce(processOnce, 150)); window.addEventListener('hashchange', () => debounce(processOnce, 250)); new MutationObserver(() => debounce(processOnce, 150)).observe(document.documentElement, { childList: true, subtree: true }); })();