tampermonkey/allegro/allegro-stat.user.js
2025-12-15 19:42:21 +01:00

476 lines
20 KiB
JavaScript

// ==UserScript==
// @name Allegro Sales Center - Koszt 1 szt Calculator
// @namespace http://tampermonkey.net/
// @version 1.3.0
// @description Adds a sortable "Koszt 1 szt" column to Allegro Sales Center statistics table that calculates cost per sold item
// @author Emma
// @match https://salescenter.allegro.com/ads/panel/stats/ads?marketplaceId=allegro-pl
// @icon https://www.google.com/s2/favicons?sz=64&domain=allegro.com
// @downloadURL https://n8n.emma.net.pl/webhook/allegro-stat
// @updateURL https://n8n.emma.net.pl/webhook/allegro-stat
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
/**
* Parses Polish currency string to number
* @param {string} value - Currency string (e.g., "1842,53 zł")
* @returns {number} - Parsed number value
*/
const parseCurrency = (value) => {
if (!value || typeof value !== 'string') return 0;
const cleaned = value.replace(/\s+zł/g, '').replace(/\s+/g, '').replace(',', '.');
return parseFloat(cleaned) || 0;
};
/**
* Parses integer string to number
* @param {string} value - Integer string (e.g., "1 002")
* @returns {number} - Parsed integer value
*/
const parseInteger = (value) => {
if (!value || typeof value !== 'string') return 0;
const cleaned = value.replace(/\s+/g, '');
return parseInt(cleaned, 10) || 0;
};
/**
* Formats number to Polish currency format
* @param {number} value - Number to format
* @returns {string} - Formatted currency string
*/
const formatCurrency = (value) => {
if (isNaN(value) || !isFinite(value)) return '0,00 zł';
return value.toFixed(2).replace('.', ',') + ' zł';
};
/**
* Finds the index of a table header by its text content
* @param {NodeList} headers - List of header elements
* @param {string} text - Text to search for
* @returns {number} - Index of the header or -1 if not found
*/
const findHeaderIndex = (headers, text) => {
for (let i = 0; i < headers.length; i++) {
if (headers[i].textContent.trim() === text) {
return i;
}
}
return -1;
};
/**
* Calculates cost per item (Koszt / Sprzedane sztuki)
* @param {number} totalCost - Total cost value
* @param {number} soldItems - Number of sold items
* @returns {number} - Cost per item rounded to 2 decimal places
*/
const calculateCostPerItem = (totalCost, soldItems) => {
if (soldItems === 0) return 0;
return Math.round((totalCost / soldItems) * 100) / 100;
};
/**
* Disables other column sorting and hides their sort icons
*/
const disableOtherSorting = () => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) return;
const headers = scrollableTable.querySelectorAll('thead th:not([data-custom-column="koszt-1-szt"])');
headers.forEach(header => {
header.style.pointerEvents = 'none';
header.style.opacity = '0.5';
// Hide sort icons in other columns
const sortIcon = header.querySelector('.Table-module__table-header__sort-icon__Kx7KdyE');
if (sortIcon) {
sortIcon.style.display = 'none';
sortIcon.setAttribute('data-was-visible', 'true');
}
});
};
/**
* Enables other column sorting and restores their sort icons
*/
const enableOtherSorting = () => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) return;
const headers = scrollableTable.querySelectorAll('thead th:not([data-custom-column="koszt-1-szt"])');
headers.forEach(header => {
header.style.pointerEvents = '';
header.style.opacity = '';
// Restore sort icons in other columns
const sortIcon = header.querySelector('.Table-module__table-header__sort-icon__Kx7KdyE');
if (sortIcon && sortIcon.getAttribute('data-was-visible') === 'true') {
sortIcon.style.display = '';
sortIcon.removeAttribute('data-was-visible');
}
});
};
/**
* Sorts table rows by cost per item
* @param {string} direction - 'asc' or 'desc'
*/
const sortTableByCosztPerItem = (direction) => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) return;
const tbody = scrollableTable.querySelector('tbody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr'));
// Separate summary row (first row) from data rows
const summaryRow = rows[0]?.classList.contains('Table-module__table-summary__ayBWA2V') ? rows[0] : null;
const dataRows = summaryRow ? rows.slice(1) : rows;
// Sort data rows
dataRows.sort((rowA, rowB) => {
const cellA = rowA.querySelector('td[data-custom-column="koszt-1-szt"]');
const cellB = rowB.querySelector('td[data-custom-column="koszt-1-szt"]');
if (!cellA || !cellB) return 0;
const valueA = parseCurrency(cellA.textContent);
const valueB = parseCurrency(cellB.textContent);
if (direction === 'asc') {
return valueA - valueB;
} else {
return valueB - valueA;
}
});
// Clear tbody
tbody.innerHTML = '';
// Re-add rows
if (summaryRow) {
tbody.appendChild(summaryRow);
}
dataRows.forEach(row => tbody.appendChild(row));
console.log(`Table sorted by Koszt 1 szt (${direction})`);
};
/**
* Adds the "Koszt 1 szt" column to the table
*/
const addCostPerItemColumn = () => {
const scrollableTable = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (!scrollableTable) {
console.log('Table not found, retrying...');
return false;
}
const thead = scrollableTable.querySelector('thead tr');
const tbody = scrollableTable.querySelector('tbody');
if (!thead || !tbody) {
console.log('Table structure not found');
return false;
}
const headers = thead.querySelectorAll('th');
const roasIndex = findHeaderIndex(headers, 'ROAS');
if (roasIndex === -1) {
console.log('ROAS column not found');
return false;
}
// Check if column already exists
let existingHeader = scrollableTable.querySelector('thead th[data-custom-column="koszt-1-szt"]');
if (!existingHeader) {
// Add new header after ROAS
const newHeader = document.createElement('th');
newHeader.className = 'Table-module__table-header__cpMic9I Utils-module__cursorPointer__ugRHHx8';
newHeader.setAttribute('data-custom-column', 'koszt-1-szt');
newHeader.setAttribute('data-sort-direction', 'none');
// Create header content with tooltip
const headerContent = document.createElement('div');
headerContent.style.display = 'flex';
headerContent.style.alignItems = 'center';
headerContent.style.justifyContent = 'space-between';
headerContent.style.width = '100%';
const headerText = document.createElement('span');
headerText.textContent = 'Koszt 1 szt';
headerText.style.flex = '1';
// Copy tooltip implementation from other columns by examining existing headers
const currentExistingHeaders = scrollableTable.querySelectorAll('thead th');
if (currentExistingHeaders.length > 0) {
// Copy classes from the first existing header
const firstHeader = currentExistingHeaders[0];
const classes = firstHeader.className;
newHeader.className = classes;
// Copy exact styles from the header itself, not its children
const computedStyle = window.getComputedStyle(firstHeader);
headerText.style.fontSize = computedStyle.fontSize;
headerText.style.fontFamily = computedStyle.fontFamily;
headerText.style.fontWeight = computedStyle.fontWeight;
headerText.style.lineHeight = computedStyle.lineHeight;
headerText.style.color = computedStyle.color;
headerText.style.fontStyle = computedStyle.fontStyle;
headerText.style.textTransform = computedStyle.textTransform;
headerText.style.letterSpacing = computedStyle.letterSpacing;
}
// Create custom tooltip that appears above the element
const tooltipText = 'Wyliczenie kosztów reklamy jednej sprzedanej sztuki. Podzielono Koszt przez sprzedane sztuki.\n\nAby przesortować kolumnę naciśnij raz aby sortować od największej do namniejszej wartości, naciśnij drugi raz aby sortować odwrotnie a trzeci raz aby inne kolumny miały ponownie możliwość sortowania.';
// Add mouse events for custom tooltip
let tooltipDiv = null;
newHeader.addEventListener('mouseenter', (e) => {
// Create tooltip div
tooltipDiv = document.createElement('div');
tooltipDiv.textContent = tooltipText;
tooltipDiv.style.position = 'fixed';
tooltipDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
tooltipDiv.style.color = 'white';
tooltipDiv.style.padding = '8px 12px';
tooltipDiv.style.borderRadius = '4px';
tooltipDiv.style.fontSize = '12px';
tooltipDiv.style.whiteSpace = 'pre-line';
tooltipDiv.style.zIndex = '9999';
tooltipDiv.style.maxWidth = '300px';
tooltipDiv.style.pointerEvents = 'none';
// Position above the element
const rect = newHeader.getBoundingClientRect();
tooltipDiv.style.left = (rect.left + rect.width / 2 - tooltipDiv.offsetWidth / 2) + 'px';
tooltipDiv.style.top = (rect.top - tooltipDiv.offsetHeight - 5) + 'px';
document.body.appendChild(tooltipDiv);
});
newHeader.addEventListener('mouseleave', () => {
if (tooltipDiv) {
tooltipDiv.remove();
tooltipDiv = null;
}
});
// Create sort icon container (hidden by default)
const sortIconContainer = document.createElement('div');
sortIconContainer.className = 'Table-module__table-header__sort-icon__Kx7KdyE';
sortIconContainer.style.display = 'none';
sortIconContainer.setAttribute('data-sort-icon', 'true');
// Create SVG arrow (like Allegro original)
const arrowDiv = document.createElement('div');
arrowDiv.className = 'mp7g_oh m0s5_e6 ArrowIcon-module__arrowDown__HSH8I9k';
arrowDiv.style.width = '22px';
arrowDiv.style.height = '22px';
arrowDiv.setAttribute('title', 'Sortuj');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'mp7g_f6 m7er_k4 mse2_k4 mg9e_0 mvrt_0 mj7a_0 mh36_0 mq1m_0 mj7u_0');
svg.style.width = '22px';
svg.style.height = '22px';
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image');
image.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', 'https://a.allegroimg.com/original/3424ea/a70a9cfd4ee59ddb6d4fc30364c7/action-common-arrowhead-c3c511fba9');
image.setAttribute('href', 'https://a.allegroimg.com/original/3424ea/a70a9cfd4ee59ddb6d4fc30364c7/action-common-arrowhead-c3c511fba9');
image.setAttribute('width', '22px');
image.setAttribute('height', '22px');
image.setAttribute('data-testid', 'svg-image-element');
svg.appendChild(image);
arrowDiv.appendChild(svg);
sortIconContainer.appendChild(arrowDiv);
headerContent.appendChild(headerText);
headerContent.appendChild(sortIconContainer);
newHeader.appendChild(headerContent);
// Add click handler for sorting
newHeader.addEventListener('click', () => {
const currentDirection = newHeader.getAttribute('data-sort-direction');
let newDirection = 'desc';
if (currentDirection === 'none') {
// First click - sort descending (arrow pointing down)
newDirection = 'desc';
sortIconContainer.style.display = 'block';
arrowDiv.style.transform = 'rotate(90deg)';
disableOtherSorting();
} else if (currentDirection === 'desc') {
// Second click - sort ascending (arrow pointing up)
newDirection = 'asc';
arrowDiv.style.transform = 'rotate(-90deg)';
} else {
// Third click - reset sorting and restore original order
newDirection = 'none';
sortIconContainer.style.display = 'none';
enableOtherSorting();
newHeader.setAttribute('data-sort-direction', 'none');
return;
}
newHeader.setAttribute('data-sort-direction', newDirection);
sortTableByCosztPerItem(newDirection);
});
if (roasIndex + 1 < headers.length) {
headers[roasIndex + 1].before(newHeader);
} else {
thead.appendChild(newHeader);
}
existingHeader = newHeader;
}
// Process data cells for each row
const rows = tbody.querySelectorAll('tr');
rows.forEach((row) => {
// Check if this row already has our custom column
const existingCell = row.querySelector('td[data-custom-column="koszt-1-szt"]');
if (existingCell) {
// Remove it to recalculate with correct indices
existingCell.remove();
}
const cells = row.querySelectorAll('td');
// Get Koszt value (index 4 in scrollable table: 0-Kliknięcia, 1-Odsłony, 2-CTR, 3-Średnie CPC, 4-Koszt)
const kosztCell = cells[4];
// Get Sprzedane sztuki value (index 7 in scrollable table: 5-ROAS, 6-Zainteresowanie, 7-Sprzedane sztuki)
const sprzedaneCell = cells[7];
let costPerItemValue = '0,00 zł';
if (kosztCell && sprzedaneCell) {
const kosztText = kosztCell.textContent.trim();
// Handle button elements in sprzedane sztuki column
const sprzedaneButton = sprzedaneCell.querySelector('button');
const sprzedaneText = sprzedaneButton ? sprzedaneButton.textContent.trim() : sprzedaneCell.textContent.trim();
const totalCost = parseCurrency(kosztText);
const soldItems = parseInteger(sprzedaneText);
const costPerItem = calculateCostPerItem(totalCost, soldItems);
costPerItemValue = formatCurrency(costPerItem);
}
const newCell = document.createElement('td');
newCell.className = 'StatisticsTable-module__statistics-table__cell__x1ILnxF Table-module__table-cell__LI2CMBo Table-module__table-cell--align-right__kyh17WP';
newCell.textContent = costPerItemValue;
newCell.setAttribute('data-custom-column', 'koszt-1-szt');
// Find ROAS cell again after removal
const currentCells = row.querySelectorAll('td');
const currentRoasIndex = 5; // ROAS is at index 5 in scrollable table
if (currentRoasIndex + 1 < currentCells.length) {
currentCells[currentRoasIndex + 1].before(newCell);
} else {
row.appendChild(newCell);
}
});
console.log('Koszt 1 szt column added successfully');
return true;
};
/**
* Observes DOM changes and adds the column when table is loaded
*/
const observeTableChanges = () => {
const targetNode = document.body;
const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['aria-selected'] };
let isProcessing = false;
let processingTimeout = null;
const callback = (mutationsList, observer) => {
// Avoid infinite loop - skip if already processing
if (isProcessing) return;
// Check for tab changes or table additions
for (const mutation of mutationsList) {
// Check for tab panel attribute changes (tab switching)
if (mutation.type === 'attributes' && mutation.attributeName === 'aria-selected') {
const target = mutation.target;
if (target.getAttribute('aria-selected') === 'true' && target.getAttribute('role') === 'tabpanel') {
console.log('Tab switched, waiting for table to load...');
isProcessing = true;
// Clear previous timeout
if (processingTimeout) {
clearTimeout(processingTimeout);
}
// Wait for table to load after tab switch
processingTimeout = setTimeout(() => {
addCostPerItemColumn();
isProcessing = false;
}, 800);
break;
}
}
// Check for table additions
if (mutation.type === 'childList') {
const tableExists = document.querySelector('.Table-module__table--scrollable__lNE_xS9');
if (tableExists) {
isProcessing = true;
// Clear previous timeout
if (processingTimeout) {
clearTimeout(processingTimeout);
}
processingTimeout = setTimeout(() => {
addCostPerItemColumn();
isProcessing = false;
}, 500);
break;
}
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
};
/**
* Initializes the script
*/
const init = () => {
console.log('Allegro Sales Center - Koszt 1 szt Calculator initialized');
// Try to add column immediately
setTimeout(() => {
addCostPerItemColumn();
}, 2000);
// Observe for future changes (pagination, filters, etc.)
observeTableChanges();
};
// Start the script
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();