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