1324 lines
43 KiB
JavaScript
1324 lines
43 KiB
JavaScript
// ==UserScript==
|
||
// @name Magazyn – wydania, pozostało, kopiowanie i oznaczenia
|
||
// @namespace https://tampermonkey.net/
|
||
// @version 4.9.2
|
||
// @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();
|
||
})(); |