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

759 lines
35 KiB
JavaScript
Executable file
Raw Permalink 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 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 });
})();