commit f395aa4017ebb98e26bd301a842d69dc7548ee19 Author: ast89 Date: Mon Sep 28 10:07:12 2026 +0000 Add magazyn.user.js diff --git a/magazyn.user.js b/magazyn.user.js new file mode 100644 index 0000000..d22ddb7 --- /dev/null +++ b/magazyn.user.js @@ -0,0 +1,1324 @@ +// ==UserScript== +// @name Magazyn – wydania, pozostało, kopiowanie i oznaczenia +// @namespace https://tampermonkey.net/ +// @version 4.9.1 +// @updateURL https://gites.fdmtech.pl/ast89/CM/main/magazyn.user.js +// @downloadURL https://gites.fdmtech.pl/ast89/CM/main/magazyn.user.js +// @description Sumuje wydania, oblicza pozostało, kopiuje symbol oraz filtruje gramatury i formaty materiałów. +// @match http://192.168.0.7:73/* +// @grant GM_setClipboard +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_addValueChangeListener +// @grant unsafeWindow +// @run-at document-idle +// ==/UserScript== + +(function () { + 'use strict'; + + /* + * Ustawienie pozostawione celowo. Zmień na true tylko wtedy, + * gdy ponownie będą potrzebne komunikaty diagnostyczne w konsoli. + */ + const DEBUG_MODE = false; + + const EXPECTED_PORT = '73'; + const WAREHOUSE_PATH = '/warehouse'; + const DEMANDS_PATH = '/warehouse/demands'; + const WAREHOUSE_TABLE_SELECTOR = '#warehouse-card-indexes-table'; + const DEMANDS_TABLE_SELECTOR = '#table'; + const STORAGE_KEY = 'tm-warehouse-demand-totals-v4'; + const REFRESH_INTERVAL_MS = 60 * 1000; + const REFRESH_TIMEOUT_MS = 45 * 1000; + + const CLASS = { + issuedHeader: 'tm-issued-header', + issuedCell: 'tm-issued-cell', + remainingHeader: 'tm-remaining-header', + remainingCell: 'tm-remaining-cell', + copyHeader: 'tm-copy-header', + copyCell: 'tm-copy-cell', + copyButton: 'tm-copy-symbol-button', + token: 'tm-material-token', + weightToken: 'tm-material-weight', + formatToken: 'tm-material-format' + }; + + let demandData = { updatedAt: 0, totals: {} }; + let refreshInProgress = false; + let activeRequestId = ''; + let refreshTimeoutId = null; + let activeMaterialFilters = { weight: null, format: null }; + let enhancingWarehouseTable = false; + let enhanceWarehouseScheduled = false; + const materialOptionCache = { + weight: new Map(), + format: new Map() + }; + + function debug(...args) { + if (DEBUG_MODE) { + console.log('[MAGAZYN DEBUG]', ...args); + } + } + + function debugWarn(...args) { + if (DEBUG_MODE) { + console.warn('[MAGAZYN DEBUG]', ...args); + } + } + + function normalizeWhitespace(value) { + return String(value ?? '') + .replace(/\u00a0/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + function cleanText(element) { + return normalizeWhitespace(element?.textContent || ''); + } + + function normalizeHeading(value) { + return normalizeWhitespace(value) + .toLocaleLowerCase('pl-PL') + .replace(/[.:]/g, ''); + } + + function normalizeSymbol(value) { + return normalizeWhitespace(value).toLocaleUpperCase('pl-PL'); + } + + function parseNumber(value) { + let text = normalizeWhitespace(value) + .replace(/[^0-9,.-]/g, '') + .replace(/\s/g, ''); + + if (!text) { + return 0; + } + + if (text.includes(',') && text.includes('.')) { + if (text.lastIndexOf(',') > text.lastIndexOf('.')) { + text = text.replace(/\./g, '').replace(',', '.'); + } else { + text = text.replace(/,/g, ''); + } + } else if (text.includes(',')) { + text = text.replace(/\./g, '').replace(',', '.'); + } + + const number = Number.parseFloat(text); + return Number.isFinite(number) ? number : 0; + } + + function formatNumber(value) { + const safeValue = Math.abs(value) < 0.0000001 ? 0 : value; + return new Intl.NumberFormat('pl-PL', { + minimumFractionDigits: 0, + maximumFractionDigits: 3 + }).format(safeValue); + } + + function currentPath() { + return window.location.pathname.replace(/\/+$/, '') || '/'; + } + + /* ---------- Kolory gramatur i formatów ---------- */ + + function hashText(value) { + let hash = 2166136261; + + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + + return hash >>> 0; + } + + function tokenColors(group, normalizedValue) { + const hash = hashText(`${group}:${normalizedValue}`); + const hue = hash % 360; + const saturation = 58 + ((hash >>> 8) % 14); + + return { + background: `hsl(${hue} ${saturation}% 88%)`, + border: `hsl(${hue} ${Math.min(82, saturation + 8)}% 58%)`, + text: `hsl(${hue} 64% 24%)` + }; + } + + function normalizeWeightToken(value) { + return normalizeWhitespace(value) + .toLocaleLowerCase('pl-PL') + .replace(/\s+/g, '') + .replace(',', '.'); + } + + function normalizeFormatToken(value) { + return normalizeWhitespace(value) + .toLocaleLowerCase('pl-PL') + .replace(/[×x]/g, 'x') + .replace(/\s+/g, ''); + } + + function createToken(text, group) { + const normalized = group === 'weight' + ? normalizeWeightToken(text) + : normalizeFormatToken(text); + const colors = tokenColors(group, normalized); + const span = document.createElement('span'); + + span.className = `${CLASS.token} ${ + group === 'weight' ? CLASS.weightToken : CLASS.formatToken + }`; + span.setAttribute('role', 'button'); + span.setAttribute('tabindex', '0'); + span.textContent = text; + span.title = group === 'weight' + ? `Kliknij, aby filtrować gramaturę: ${normalizeWhitespace(text)}` + : `Kliknij, aby filtrować format: ${normalizeWhitespace(text)}`; + span.setAttribute('aria-label', span.title); + span.style.setProperty('--tm-token-background', colors.background); + span.style.setProperty('--tm-token-border', colors.border); + span.style.setProperty('--tm-token-text', colors.text); + + const filterFromBadge = event => { + event.preventDefault(); + event.stopPropagation(); + + const table = span.closest('table'); + if (table) { + applyMaterialFilter(table, { + group, + key: normalized, + label: normalizeWhitespace(text) + }); + } + }; + + span.addEventListener('click', filterFromBadge); + span.addEventListener('keydown', event => { + if (event.key === 'Enter' || event.key === ' ') { + filterFromBadge(event); + } + }); + + return span; + } + + function extractMaterialTokens(value) { + const text = normalizeWhitespace(value); + const pattern = /(\b\d+(?:[.,]\d+)?\s*g(?:\s*\/\s*m(?:2|²))?\b)|(\b\d{2,4}\s*[x×]\s*\d{2,4}\b)/gi; + const result = []; + let match; + + while ((match = pattern.exec(text)) !== null) { + const group = match[1] ? 'weight' : 'format'; + const key = group === 'weight' + ? normalizeWeightToken(match[0]) + : normalizeFormatToken(match[0]); + + result.push({ + group, + key, + label: normalizeWhitespace(match[0]) + }); + } + + return result; + } + + function highlightMaterialName(cell) { + if (!cell) { + return; + } + + const hasOurTokens = Boolean(cell.querySelector(`.${CLASS.token}`)); + const visibleText = cleanText(cell); + let sourceText = cell.dataset.tmOriginalMaterialName || visibleText; + + // Jeżeli aplikacja podmieniła zawartość tej samej komórki, przyjmij nowy tekst. + if (!hasOurTokens && visibleText !== sourceText) { + sourceText = visibleText; + cell.dataset.tmOriginalMaterialName = sourceText; + delete cell.dataset.tmHighlightedMaterialName; + } + + if (!cell.dataset.tmOriginalMaterialName) { + cell.dataset.tmOriginalMaterialName = sourceText; + } + + if (cell.dataset.tmHighlightedMaterialName === sourceText && hasOurTokens) { + return; + } + + const tokenPattern = /(\b\d+(?:[.,]\d+)?\s*g(?:\s*\/\s*m(?:2|²))?\b)|(\b\d{2,4}\s*[x×]\s*\d{2,4}\b)/gi; + const tokens = extractMaterialTokens(sourceText); + + if (!tokens.length) { + cell.dataset.tmHighlightedMaterialName = sourceText; + return; + } + + const cleanedName = normalizeWhitespace( + sourceText.replace(tokenPattern, ' ') + ); + + const nameLine = document.createElement('div'); + nameLine.className = 'tm-material-name-main'; + nameLine.textContent = cleanedName; + + const badges = document.createElement('div'); + badges.className = 'tm-material-name-badges'; + + const added = new Set(); + tokens.forEach(token => { + const uniqueKey = `${token.group}:${token.key}`; + if (added.has(uniqueKey)) { + return; + } + + added.add(uniqueKey); + badges.appendChild(createToken(token.label, token.group)); + }); + + cell.replaceChildren(nameLine, badges); + cell.dataset.tmHighlightedMaterialName = sourceText; + } + + /* ---------- Style ---------- */ + + function installStyles() { + if (document.getElementById('tm-warehouse-styles')) { + return; + } + + const style = document.createElement('style'); + style.id = 'tm-warehouse-styles'; + style.textContent = ` + body .container, + body .container-fluid { + width: 100% !important; + max-width: none !important; + } + + ${WAREHOUSE_TABLE_SELECTOR} { + width: 100% !important; + table-layout: auto !important; + } + + ${WAREHOUSE_TABLE_SELECTOR} th, + ${WAREHOUSE_TABLE_SELECTOR} td { + vertical-align: middle !important; + overflow-wrap: break-word; + word-break: normal; + } + + ${WAREHOUSE_TABLE_SELECTOR} th .th-inner { + white-space: normal !important; + } + + ${WAREHOUSE_TABLE_SELECTOR} .tm-hidden-column { + display: none !important; + } + + ${WAREHOUSE_TABLE_SELECTOR} .${CLASS.issuedCell}, + ${WAREHOUSE_TABLE_SELECTOR} .${CLASS.remainingCell} { + text-align: right; + font-variant-numeric: tabular-nums; + font-weight: 700; + } + + ${WAREHOUSE_TABLE_SELECTOR} .${CLASS.issuedCell} { + background: #fff8dc; + border-left: 2px solid #e0b84f; + border-right: 2px solid #e0b84f; + cursor: help; + } + + ${WAREHOUSE_TABLE_SELECTOR} .${CLASS.remainingCell} { + background-color: #edf7ff !important; + color: #174b78 !important; + } + + ${WAREHOUSE_TABLE_SELECTOR} .tm-negative-value { + color: #a40000 !important; + background-color: #ffe7e7 !important; + } + + ${WAREHOUSE_TABLE_SELECTOR} .${CLASS.copyCell}, + ${WAREHOUSE_TABLE_SELECTOR} .${CLASS.copyHeader} { + text-align: center; + } + + ${WAREHOUSE_TABLE_SELECTOR} .${CLASS.copyButton} { + width: auto; + max-width: 100%; + padding: 4px 7px; + white-space: normal; + line-height: 1.2; + } + + .${CLASS.token} { + display: inline-flex; + align-items: center; + min-height: 18px; + padding: 1px 6px; + border: 1px solid var(--tm-token-border); + border-radius: 999px; + color: var(--tm-token-text); + background: var(--tm-token-background); + font-weight: 700; + font-size: 10px; + line-height: 1.2; + white-space: nowrap; + cursor: pointer; + transition: filter 120ms ease, transform 120ms ease; + } + + .${CLASS.token}:hover { + filter: brightness(0.94); + transform: translateY(-1px); + } + + .${CLASS.token}:focus-visible { + outline: 2px solid #245c91; + outline-offset: 2px; + } + + .tm-material-name-main { + display: block; + min-width: 0; + } + + .tm-material-name-badges { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 3px; + margin-top: 3px; + } + + .tm-material-filter-bar { + box-sizing: border-box; + width: 100%; + margin: 6px 0 10px; + padding: 8px 10px; + border: 1px solid #d7dde4; + border-radius: 4px; + background: #f7f9fb; + } + + .tm-material-filter-group { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 5px; + } + + .tm-material-filter-group + .tm-material-filter-group { + margin-top: 6px; + } + + .tm-material-filter-label { + flex: 0 0 76px; + color: #485563; + font-size: 12px; + font-weight: 700; + } + + .tm-material-filter-button { + min-height: 26px; + padding: 3px 8px; + border: 1px solid var(--tm-filter-border, #aab4bf); + border-radius: 4px; + color: var(--tm-filter-text, #24313d); + background: var(--tm-filter-background, #fff); + font-size: 12px; + line-height: 1.2; + cursor: pointer; + } + + .tm-material-filter-button:hover { + filter: brightness(0.96); + border-color: #4979a8; + } + + .tm-material-filter-button:focus-visible { + outline: 2px solid #2468a2; + outline-offset: 2px; + } + + .tm-material-filter-button[aria-pressed="true"] { + color: #fff; + background: #2468a2; + border-color: #174b78; + box-shadow: inset 0 0 0 1px rgba(255,255,255,.25); + } + + .tm-material-filter-reset { + border-color: #8d99a5; + color: #33404c; + background: #fff; + } + + @media (max-width: 900px) { + .tm-material-filter-label { + flex-basis: 100%; + } + } + + .tm-issued-loader { + display: inline-block; + width: 13px; + height: 13px; + border: 2px solid #d8d8d8; + border-top-color: #3977b7; + border-radius: 50%; + animation: tm-loader-spin 0.75s linear infinite; + vertical-align: -2px; + } + + @keyframes tm-loader-spin { + to { transform: rotate(360deg); } + } + + @media (prefers-reduced-motion: reduce) { + .tm-issued-loader { + animation: none; + border-top-color: #d8d8d8; + } + } + `; + + document.head.appendChild(style); + } + + /* ---------- Tabela wydań ---------- */ + + function getHeaderIndexes(table) { + const indexes = {}; + const headers = table?.querySelectorAll('thead tr:last-child th') || []; + + headers.forEach((header, index) => { + const field = normalizeWhitespace(header.dataset.field || ''); + const title = normalizeHeading(header.dataset.title || header.textContent || ''); + + if (field) { + indexes[field] = index; + } + + if (title) { + indexes[`title:${title}`] = index; + } + }); + + return indexes; + } + + function firstDefined(...values) { + return values.find(value => Number.isInteger(value)); + } + + function demandsColumnIndexes(table) { + const map = getHeaderIndexes(table); + + return { + order: firstDefined(map.order_no, map['title:nr zlecenia']), + symbol: firstDefined(map.symbol_zap, map['title:symbol zap']), + quantity: firstDefined(map.card_index_ilosc, map['title:ilość']), + quantity2: firstDefined(map.card_index_ilosc2, map['title:ilość2']), + unit2: firstDefined(map.jm2, map['title:jm2']), + rw: firstDefined(map.transactions_RW, map.transactions_rw, map['title:wydania rw']), + unassign: firstDefined(map.unassign_card_index) + }; + } + + function demandRowsReady(table) { + const rows = Array.from(table?.querySelectorAll('tbody tr') || []); + return rows.some(row => row.querySelectorAll(':scope > td').length >= 10); + } + + function calculateDemandTotals(table) { + const indexes = demandsColumnIndexes(table); + const totals = {}; + const rows = Array.from(table.querySelectorAll('tbody tr')); + + if (![indexes.symbol, indexes.quantity, indexes.quantity2, indexes.unit2] + .every(Number.isInteger)) { + throw new Error('Nie rozpoznano wymaganych kolumn tabeli wydań.'); + } + + rows.forEach((row, rowIndex) => { + const cells = row.querySelectorAll(':scope > td'); + + if (cells.length < 10) { + return; + } + + const lastCell = cells[cells.length - 1]; + const lastText = cleanText(lastCell).toLocaleLowerCase('pl-PL'); + const hasUnassignAction = Boolean(row.querySelector('.unassign-card-index')) || + lastText.includes('usuń powiązanie'); + + if (!hasUnassignAction) { + debug('Pominięto: brak „usuń powiązanie”', rowIndex + 1); + return; + } + + const rwValue = Number.isInteger(indexes.rw) ? cleanText(cells[indexes.rw]) : ''; + + if (rwValue) { + debug('Pominięto: istnieje Wydanie RW', rowIndex + 1, rwValue); + return; + } + + const originalSymbol = cleanText(cells[indexes.symbol]); + const symbol = normalizeSymbol(originalSymbol); + + if (!symbol) { + return; + } + + const unit2 = cleanText(cells[indexes.unit2]).toLocaleLowerCase('pl-PL'); + const usesSheets = unit2 === 'ark'; + const quantityCell = usesSheets ? cells[indexes.quantity2] : cells[indexes.quantity]; + const quantity = parseNumber(cleanText(quantityCell)); + const order = Number.isInteger(indexes.order) ? cleanText(cells[indexes.order]) : ''; + const unit = usesSheets ? 'ark' : ''; + + if (!totals[symbol]) { + totals[symbol] = { + symbol: originalSymbol, + total: 0, + sources: [] + }; + } + + totals[symbol].total += quantity; + totals[symbol].sources.push({ + order: order || 'bez numeru zlecenia', + quantity, + unit, + sourceColumn: usesSheets ? 'Ilość2' : 'Ilość' + }); + }); + + return totals; + } + + function publishDemandTotals(requestId) { + const startedAt = Date.now(); + + const attempt = () => { + const table = document.querySelector(DEMANDS_TABLE_SELECTOR); + + if (table && demandRowsReady(table)) { + try { + const totals = calculateDemandTotals(table); + GM_setValue(STORAGE_KEY, { + requestId, + updatedAt: Date.now(), + totals + }); + debug('Zapisano sumy wydań', totals); + return; + } catch (error) { + debugWarn(error); + } + } + + if (Date.now() - startedAt < 30000) { + window.setTimeout(attempt, 500); + return; + } + + GM_setValue(STORAGE_KEY, { + requestId, + updatedAt: Date.now(), + error: 'Tabela wydań nie załadowała danych w wymaganym czasie.' + }); + }; + + attempt(); + } + + /* ---------- Główna tabela magazynu ---------- */ + + function findHeaderIndex(table, options) { + const headers = Array.from(table.querySelectorAll('thead tr:last-child th')); + + return headers.findIndex(header => { + const field = normalizeWhitespace(header.dataset.field || ''); + const title = normalizeHeading(header.dataset.title || header.textContent || ''); + + return options.fields.includes(field) || options.titles.includes(title); + }); + } + + function warehouseColumnIndexes(table) { + return { + symbol: findHeaderIndex(table, { + fields: ['symbol', 'card_index_symbol', 'index'], + titles: ['symbol'] + }), + name: findHeaderIndex(table, { + fields: ['name', 'nazwa'], + titles: ['nazwa'] + }), + description: findHeaderIndex(table, { + fields: ['description', 'opis'], + titles: ['opis'] + }), + state: findHeaderIndex(table, { + fields: ['stan', 'state'], + titles: ['stan'] + }) + }; + } + + function createHeader(text, className) { + const header = document.createElement('th'); + header.className = className; + header.textContent = text; + return header; + } + + function ensureWarehouseHeaders(table) { + const headerRow = table.querySelector('thead tr:last-child'); + + if (!headerRow) { + return; + } + + let indexes = warehouseColumnIndexes(table); + const stateHeader = headerRow.children[indexes.state]; + + if (!stateHeader) { + return; + } + + let issuedHeader = headerRow.querySelector(`.${CLASS.issuedHeader}`); + if (!issuedHeader) { + issuedHeader = createHeader('Wydane', CLASS.issuedHeader); + stateHeader.insertAdjacentElement('afterend', issuedHeader); + } + + if (!headerRow.querySelector(`.${CLASS.remainingHeader}`)) { + const remainingHeader = createHeader('Pozostało', CLASS.remainingHeader); + issuedHeader.insertAdjacentElement('afterend', remainingHeader); + } + + if (!headerRow.querySelector(`.${CLASS.copyHeader}`)) { + headerRow.appendChild(createHeader('Kopiuj symbol', CLASS.copyHeader)); + } + + indexes = warehouseColumnIndexes(table); + debug('Indeksy tabeli magazynu', indexes); + } + + function hideUnwantedWarehouseColumns(table) { + const headers = Array.from( + table.querySelectorAll('thead tr:last-child th') + ); + const hiddenIndexes = []; + + headers.forEach((header, index) => { + const title = normalizeHeading( + header.dataset.title || header.textContent || '' + ); + const field = normalizeWhitespace( + header.dataset.field || '' + ).toLocaleLowerCase('pl-PL'); + + const shouldHide = + title === 'ost cena' || + title === 'stan min' || + ['ost_cena', 'ostatnia_cena', 'last_price', 'stan_min'] + .includes(field); + + header.classList.toggle('tm-hidden-column', shouldHide); + + if (shouldHide) { + hiddenIndexes.push(index); + } + }); + + table.querySelectorAll('tbody tr').forEach(row => { + const cells = row.querySelectorAll(':scope > td'); + + cells.forEach(cell => { + cell.classList.remove('tm-hidden-column'); + }); + + hiddenIndexes.forEach(index => { + cells[index]?.classList.add('tm-hidden-column'); + }); + }); + } + + function createLoader() { + const loader = document.createElement('span'); + loader.className = 'tm-issued-loader'; + loader.setAttribute('aria-label', 'Ładowanie danych wydań'); + return loader; + } + + function sourceTooltip(entry) { + if (!entry || !entry.sources?.length) { + return 'Brak niepowiązanych wydań dla tego symbolu.'; + } + + const lines = entry.sources.map((source, index) => { + const unit = source.unit ? ` ${source.unit}` : ''; + return `${index + 1}. ${source.order}: ${formatNumber(source.quantity)}${unit} (${source.sourceColumn})`; + }); + + return `Składniki sumy ${formatNumber(entry.total)}:\n${lines.join('\n')}`; + } + + function setIssuedCellContent(cell, symbol) { + if (refreshInProgress) { + if (!cell.querySelector('.tm-issued-loader')) { + cell.replaceChildren(createLoader()); + } + cell.title = 'Odświeżanie danych wydań…'; + return; + } + + const entry = demandData.totals?.[normalizeSymbol(symbol)]; + const total = entry?.total || 0; + const formattedTotal = formatNumber(total); + + if (cell.textContent !== formattedTotal || cell.children.length) { + cell.textContent = formattedTotal; + } + + cell.title = sourceTooltip(entry); + } + + function setRemainingCellContent(cell, state, symbol) { + const entry = demandData.totals?.[normalizeSymbol(symbol)]; + const remaining = state - (entry?.total || 0); + + const formattedRemaining = formatNumber(remaining); + + if (cell.textContent !== formattedRemaining || cell.children.length) { + cell.textContent = formattedRemaining; + } + + cell.classList.toggle('tm-negative-value', remaining < 0); + cell.title = `${formatNumber(state)} − ${formatNumber(entry?.total || 0)} = ${formatNumber(remaining)}`; + } + + function addCopyButton(cell, nameCell, descriptionCell) { + let button = cell.querySelector(`.${CLASS.copyButton}`); + + if (!button) { + button = document.createElement('button'); + button.type = 'button'; + button.className = `btn btn-sm btn-primary ${CLASS.copyButton}`; + button.textContent = 'Kopiuj symbol'; + cell.appendChild(button); + } + + button.onclick = () => { + const name = nameCell.dataset.tmOriginalMaterialName || cleanText(nameCell); + const description = cleanText(descriptionCell); + const firstDescriptionPart = description.split(/\s+/)[0] || ''; + const textToCopy = `Użyć: ${firstDescriptionPart} ${name}`.trim(); + + GM_setClipboard(textToCopy, 'text'); + button.textContent = 'Skopiowano ✓'; + button.classList.remove('btn-primary'); + button.classList.add('btn-success'); + + window.setTimeout(() => { + button.textContent = 'Kopiuj symbol'; + button.classList.remove('btn-success'); + button.classList.add('btn-primary'); + }, 1500); + }; + } + + function valueToPlainText(value) { + const container = document.createElement('div'); + container.innerHTML = String(value ?? ''); + return normalizeWhitespace(container.textContent || ''); + } + + function collectMaterialOptions(table) { + const indexes = warehouseColumnIndexes(table); + const names = []; + + try { + const api = getDataTableApi(table); + if (api) { + // DataTables ma dostęp do wszystkich rekordów, także poza bieżącą stroną. + api.rows({ search: 'applied' }).nodes().toArray().forEach(row => { + const cell = row.querySelectorAll(':scope > td')[indexes.name]; + if (cell) { + names.push(cell.dataset.tmOriginalMaterialName || cleanText(cell)); + } + }); + } + } catch (error) { + debugWarn('Nie udało się odczytać wierszy DataTables.', error); + } + + if (!names.length && indexes.name >= 0) { + table.querySelectorAll('tbody tr').forEach(row => { + const cell = row.querySelectorAll(':scope > td')[indexes.name]; + + if (cell) { + names.push( + cell.dataset.tmOriginalMaterialName || cleanText(cell) + ); + } + }); + } + + debug('Zebrane tokeny do przycisków', { + namesFound: names.length, + gramatures: materialOptionCache.weight.size, + formats: materialOptionCache.format.size, + columns: indexes + }); + + names.forEach(name => { + extractMaterialTokens(name).forEach(token => { + if (!materialOptionCache[token.group].has(token.key)) { + materialOptionCache[token.group].set(token.key, token.label); + } + }); + }); + + return { + weight: Array.from(materialOptionCache.weight, ([key, label]) => ({ + group: 'weight', + key, + label + })).sort((a, b) => parseNumber(a.label) - parseNumber(b.label)), + + format: Array.from(materialOptionCache.format, ([key, label]) => ({ + group: 'format', + key, + label + })).sort((a, b) => { + const firstA = parseNumber(a.key.split('x')[0]); + const firstB = parseNumber(b.key.split('x')[0]); + const secondA = parseNumber(a.key.split('x')[1]); + const secondB = parseNumber(b.key.split('x')[1]); + return firstA - firstB || secondA - secondB; + }) + }; + } + + function getDataTableApi(table) { + try { + const pageWindow = typeof unsafeWindow !== 'undefined' + ? unsafeWindow + : window; + const jq = pageWindow.jQuery; + + if (jq?.fn?.DataTable && jq.fn.dataTable?.isDataTable(table)) { + return jq(table).DataTable(); + } + } catch (error) { + debugWarn('Brak dostępu do API tabeli.', error); + } + + return null; + } + + function applyFallbackRowFilter(table) { + const indexes = warehouseColumnIndexes(table); + let matched = 0; + let total = 0; + + table.querySelectorAll('tbody tr').forEach(row => { + total += 1; + const hasActiveFilters = Boolean( + activeMaterialFilters.weight || activeMaterialFilters.format + ); + if (!hasActiveFilters || indexes.name < 0) { + row.hidden = false; + row.style.removeProperty('display'); + matched += 1; + return; + } + + const nameCell = row.querySelectorAll(':scope > td')[indexes.name]; + const source = nameCell?.dataset.tmOriginalMaterialName || cleanText(nameCell); + const tokens = extractMaterialTokens(source); + const matches = Object.entries(activeMaterialFilters).every(([group, selected]) => ( + !selected || tokens.some(token => token.group === group && token.key === selected.key) + )); + + if (matches) { + row.hidden = false; + row.style.removeProperty('display'); + } else { + row.hidden = true; + row.style.setProperty('display', 'none', 'important'); + } + if (matches) matched += 1; + }); + + debug('Fallback DOM filter', { + filters: activeMaterialFilters, + matched, + total, + nameColumnIndex: indexes.name + }); + } + + function updateFilterButtonStates(bar) { + bar.querySelectorAll('.tm-material-filter-button[data-filter-key]') + .forEach(button => { + const selected = activeMaterialFilters[button.dataset.filterGroup]; + const active = Boolean(selected) && button.dataset.filterKey === selected.key; + + button.setAttribute('aria-pressed', String(active)); + }); + + const reset = bar.querySelector('.tm-material-filter-reset'); + if (reset) { + reset.setAttribute('aria-pressed', String( + !activeMaterialFilters.weight && !activeMaterialFilters.format + )); + } + } + + function applyMaterialFilter(table, filter) { + if (!filter) { + activeMaterialFilters = { weight: null, format: null }; + } else { + const current = activeMaterialFilters[filter.group]; + activeMaterialFilters[filter.group] = current?.key === filter.key + ? null + : filter; + } + + const indexes = warehouseColumnIndexes(table); + + debug('Kliknięto filtr materiału', { + requested: filter, + applied: activeMaterialFilters, + nameColumnIndex: indexes.name, + visibleRowsBefore: table.querySelectorAll('tbody tr:not([style*="display: none"])').length, + totalDomRows: table.querySelectorAll('tbody tr').length + }); + + // Strona ma paging:false i renderuje komplet rekordów w DOM. Filtrujemy + // te wiersze bezpośrednio; nie polegamy na wewnętrznym cache DataTables. + applyFallbackRowFilter(table); + + const bar = document.querySelector('.tm-material-filter-bar'); + if (bar) { + updateFilterButtonStates(bar); + } + } + + function createMaterialFilterButton(table, option) { + const button = document.createElement('button'); + const colors = tokenColors(option.group, option.key); + + button.type = 'button'; + button.className = 'tm-material-filter-button'; + button.textContent = option.label; + button.dataset.filterGroup = option.group; + button.dataset.filterKey = option.key; + button.setAttribute('aria-pressed', 'false'); + button.style.setProperty('--tm-filter-background', colors.background); + button.style.setProperty('--tm-filter-border', colors.border); + button.style.setProperty('--tm-filter-text', colors.text); + button.title = `Pokaż tylko pozycje: ${option.label}`; + + button.addEventListener('click', () => { + applyMaterialFilter(table, option); + }); + + return button; + } + + function createMaterialFilterGroup(table, title, options, includeReset) { + const group = document.createElement('div'); + group.className = 'tm-material-filter-group'; + + const label = document.createElement('span'); + label.className = 'tm-material-filter-label'; + label.textContent = title; + group.appendChild(label); + + if (includeReset) { + const reset = document.createElement('button'); + reset.type = 'button'; + reset.className = 'tm-material-filter-button tm-material-filter-reset'; + reset.textContent = 'Wszystkie'; + reset.setAttribute('aria-pressed', String( + !activeMaterialFilters.weight && !activeMaterialFilters.format + )); + reset.addEventListener('click', () => applyMaterialFilter(table, null)); + group.appendChild(reset); + } + + options.forEach(option => { + group.appendChild(createMaterialFilterButton(table, option)); + }); + + return group; + } + + function findMaterialFilterMount(table) { + const bootstrapWrapper = table.closest('.bootstrap-table'); + const toolbar = bootstrapWrapper?.querySelector(':scope > .fixed-table-toolbar'); + + if (toolbar) { + return { parent: toolbar.parentElement, after: toolbar }; + } + + const fixedContainer = table.closest('.fixed-table-container'); + if (fixedContainer?.parentElement) { + return { parent: fixedContainer.parentElement, before: fixedContainer }; + } + + return { parent: table.parentElement, before: table }; + } + + function ensureMaterialFilterBar(table) { + const options = collectMaterialOptions(table); + const signature = JSON.stringify({ + weight: options.weight.map(item => item.key), + format: options.format.map(item => item.key) + }); + + let bar = document.querySelector('.tm-material-filter-bar'); + + if (!bar) { + bar = document.createElement('div'); + bar.className = 'tm-material-filter-bar'; + bar.setAttribute('aria-label', 'Filtry gramatur i formatów'); + + const mount = findMaterialFilterMount(table); + + if (mount.after) { + mount.after.insertAdjacentElement('afterend', bar); + } else if (mount.before) { + mount.parent.insertBefore(bar, mount.before); + } + } + + if (bar.dataset.optionsSignature !== signature) { + bar.replaceChildren( + createMaterialFilterGroup(table, 'Gramatura:', options.weight, true), + createMaterialFilterGroup(table, 'Format:', options.format, false) + ); + bar.dataset.optionsSignature = signature; + } + + updateFilterButtonStates(bar); + } + + function enhanceWarehouseRow(row, indexes) { + let cells = row.querySelectorAll(':scope > td'); + + if (cells.length < 8 || indexes.symbol < 0 || indexes.name < 0 || + indexes.description < 0 || indexes.state < 0) { + return; + } + + const symbolCell = cells[indexes.symbol]; + const nameCell = cells[indexes.name]; + const descriptionCell = cells[indexes.description]; + const stateCell = cells[indexes.state]; + + if (!symbolCell || !nameCell || !descriptionCell || !stateCell) { + return; + } + + highlightMaterialName(nameCell); + + let issuedCell = row.querySelector(`.${CLASS.issuedCell}`); + if (!issuedCell) { + issuedCell = document.createElement('td'); + issuedCell.className = CLASS.issuedCell; + stateCell.insertAdjacentElement('afterend', issuedCell); + } + + let remainingCell = row.querySelector(`.${CLASS.remainingCell}`); + if (!remainingCell) { + remainingCell = document.createElement('td'); + remainingCell.className = CLASS.remainingCell; + issuedCell.insertAdjacentElement('afterend', remainingCell); + } + + let copyCell = row.querySelector(`.${CLASS.copyCell}`); + if (!copyCell) { + copyCell = document.createElement('td'); + copyCell.className = CLASS.copyCell; + row.appendChild(copyCell); + } + + const symbol = cleanText(symbolCell); + const state = parseNumber(cleanText(stateCell)); + + setIssuedCellContent(issuedCell, symbol); + setRemainingCellContent(remainingCell, state, symbol); + addCopyButton(copyCell, nameCell, descriptionCell); + } + + function enhanceWarehouseTable() { + if (enhancingWarehouseTable) { + return; + } + + const table = document.querySelector(WAREHOUSE_TABLE_SELECTOR); + + if (!table) { + return; + } + + enhancingWarehouseTable = true; + + try { + ensureWarehouseHeaders(table); + const indexes = warehouseColumnIndexes(table); + + table.querySelectorAll('tbody tr').forEach(row => { + enhanceWarehouseRow(row, indexes); + }); + + // Dopiero po dodaniu nowych komórek indeksy nagłówków i wierszy są identyczne. + hideUnwantedWarehouseColumns(table); + + ensureMaterialFilterBar(table); + + // Reaplikuj po renderze DataTables; observer strony wykrywa też + // ponowne utworzenie wierszy po zmianie sortowania/wyszukiwania. + applyFallbackRowFilter(table); + } finally { + enhancingWarehouseTable = false; + } + } + + function scheduleWarehouseEnhancement() { + if (enhanceWarehouseScheduled) { + return; + } + + enhanceWarehouseScheduled = true; + + window.requestAnimationFrame(() => { + enhanceWarehouseScheduled = false; + enhanceWarehouseTable(); + }); + } + + function applyDemandData(payload) { + if (!payload || payload.error || !payload.totals) { + if (payload?.error) { + debugWarn(payload.error); + } + return false; + } + + demandData = payload; + return true; + } + + function finishRefresh(payload) { + if (payload?.requestId && activeRequestId && payload.requestId !== activeRequestId) { + return; + } + + if (refreshTimeoutId) { + window.clearTimeout(refreshTimeoutId); + refreshTimeoutId = null; + } + + applyDemandData(payload); + refreshInProgress = false; + enhanceWarehouseTable(); + + document.querySelectorAll('iframe.tm-demands-loader').forEach(frame => frame.remove()); + } + + function refreshDemandData() { + if (refreshInProgress) { + return; + } + + refreshInProgress = true; + activeRequestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + enhanceWarehouseTable(); + + const oldFrame = document.querySelector('iframe.tm-demands-loader'); + if (oldFrame) { + oldFrame.remove(); + } + + const frame = document.createElement('iframe'); + frame.className = 'tm-demands-loader'; + frame.src = `${DEMANDS_PATH}?tm_sync=${encodeURIComponent(activeRequestId)}&tm_time=${Date.now()}`; + frame.setAttribute('aria-hidden', 'true'); + frame.style.cssText = 'position:fixed;width:1px;height:1px;left:-10000px;top:-10000px;border:0;opacity:0;pointer-events:none;'; + document.body.appendChild(frame); + + refreshTimeoutId = window.setTimeout(() => { + refreshInProgress = false; + enhanceWarehouseTable(); + frame.remove(); + debugWarn('Przekroczono czas odświeżania danych wydań.'); + }, REFRESH_TIMEOUT_MS); + } + + function runWarehousePage() { + installStyles(); + + const cached = GM_getValue(STORAGE_KEY, null); + applyDemandData(cached); + enhanceWarehouseTable(); + + GM_addValueChangeListener(STORAGE_KEY, (_name, _oldValue, newValue) => { + finishRefresh(newValue); + }); + + const observer = new MutationObserver(() => { + scheduleWarehouseEnhancement(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + window.setInterval(enhanceWarehouseTable, 1000); + window.setTimeout(refreshDemandData, 300); + window.setInterval(refreshDemandData, REFRESH_INTERVAL_MS); + } + + function runDemandsPage() { + const requestId = new URLSearchParams(window.location.search).get('tm_sync') || ''; + publishDemandTotals(requestId); + } + + function main() { + if (window.location.port !== EXPECTED_PORT) { + return; + } + + const path = currentPath(); + + if (path === DEMANDS_PATH) { + runDemandsPage(); + } else if (path === WAREHOUSE_PATH) { + runWarehousePage(); + } + } + + main(); +})(); \ No newline at end of file