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