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