Files
CM/magazyn.user.js
2026-09-28 10:35:43 +00:00

1324 lines
43 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==UserScript==
// @name Magazyn – wydania, pozostało, kopiowanie i oznaczenia
// @namespace https://tampermonkey.net/
// @version 4.9.3
// @updateURL https://gites.fdmtech.pl/ast89/CM/raw/branch/main/magazyn.user.js
// @downloadURL https://gites.fdmtech.pl/ast89/CM/raw/branch/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();
})();