476 lines
20 KiB
JavaScript
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();
|
|
}
|
|
|
|
})();
|