Albert Heijn Korting

Add price per gram and discount percentage to products

目前為 2024-11-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Albert Heijn Korting
// @namespace    https://wol.ph/
// @version      1.0.4
// @description  Add price per gram and discount percentage to products
// @author       wolph
// @match        https://www.ah.nl/*
// @icon         https://icons.duckduckgo.com/ip2/ah.nl.ico
// @grant        none
// @license      BSD
// ==/UserScript==

const DEBUG = false;

(function() {
    'use strict';

    // Set to keep track of processed products
    const processedProducts = new Set();

    function updateProductInfo() {
        console.log('Starting updateProductInfo');

        // Handle normal product cards
		let productCards;
      	productCards = document.querySelectorAll('article[data-testhook="product-card"]');

        console.log(`Found ${productCards.length} product cards`);

        productCards.forEach(function(productCard, index) {
            // console.debug(`Processing product card ${index}`);

            // Use a unique identifier for the product
            let productId = productCard.getAttribute('data-product-id') || productCard.querySelector('a[class*="link_root__"]')?.getAttribute('href');

            if (!productId) {
                console.log('Product ID not found, skipping');
                return;
            }

            if (processedProducts.has(productId)) {
                // console.debug(`Product ${productId} already processed, skipping`);
                return;
            }

            console.log(`Processing product ${productId}`);

            // Extract price
            let priceElement = productCard.querySelector('[data-testhook="price-amount"]');
            if (!priceElement) {
                console.log('Price element not found, skipping');
                return;
            }
            let priceInt = priceElement.querySelector('[class*="price-amount_integer__"]');
            let priceFrac = priceElement.querySelector('[class*="price-amount_fractional__"]');
			if (!priceInt || !priceFrac) {
                console.log('Price integer or fractional part not found, skipping');
                return;
            }
            let price = parseFloat(priceInt.textContent + '.' + priceFrac.textContent);
            console.log(`Price: ${price}`);

            // Extract unit size
            let unitSizeElement = productCard.querySelector('[data-testhook="product-unit-size"]');
            if (!unitSizeElement) {
                console.log('Unit size element not found, skipping');
                return;
            }
            let unitSizeText = unitSizeElement.textContent.trim(); // e.g., "300 g"
            console.log(`Unit size text: ${unitSizeText}`);

            // Parse unit size
            let unitMatch = unitSizeText.match(/(ca\. |)([\d.,]+)\s*(g|kg)/i);
			let countMatch = unitSizeText.match(/(\d+)\s*(stuk)s?/i);
            if (!unitMatch && !countMatch) {
                console.log('Weight and count not found in unit size text, skipping');
                return;
            }

			let pricePerUnit = 0;
			let unit = '';

			if(unitMatch){
				let weight = parseFloat(unitMatch[2].replace(',', '.'));
				unit = unitMatch[3].toLowerCase();
				console.log(`Weight: ${weight}, Unit: ${unit}`);

				// Convert weight to grams
				if (unit === 'kg') {
					weight = weight * 1000;
					console.log(`Converted weight to grams: ${weight}`);
				} else {
					// We calculate with grams but we display per kg in all cases.
					unit = 'kg';
				}

				// Calculate price per kg
				pricePerUnit = (price * 1000) / weight; // Price per kg
			}
			if(countMatch){
				let count = parseInt(countMatch[1]);
				unit = countMatch[2].toLowerCase();
				console.log(`Count: ${count}`);

				// Calculate price per item
				pricePerUnit = price / count; // Price per item
			}
			console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}`);

            // Extract old price if available
            let oldPrice = null;
            // Assuming there's an element for the old price
            let oldPriceElement = priceElement.querySelector('[class*="price-amount_original__"]');
            if (oldPriceElement) {
                let oldPriceInt = oldPriceElement.querySelector('[class*="price-amount_integer__"]');
                let oldPriceFrac = oldPriceElement.querySelector('[class*="price-amount_fractional__"]');
                if (oldPriceInt && oldPriceFrac) {
                    oldPrice = parseFloat(oldPriceInt.textContent + '.' + oldPriceFrac.textContent);
                    console.log(`Old price: ${oldPrice}`);
                }
            }

            // Calculate discount percentage
            let discountPercentage = null;
            if (oldPrice) {
                discountPercentage = ((oldPrice - price) / oldPrice) * 100;
                discountPercentage = discountPercentage.toFixed(1);
                discountPercentage = parsePromotionText(shieldText, price);
            } else {
                // Try extracting from shield
                let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
                if (shieldElement) {
                    let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');

                    if (shieldTextElement) {
                        let shieldText = shieldTextElement.textContent.trim();
                        console.log(`Shield text: ${shieldText}, ${oldPrice}, ${price}`);
                        discountPercentage = parsePromotionText(shieldText, price);
                        if (discountPercentage) {
                            console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
                        }
                    }
                }
            }

            // If discount percentage is not found, set it to 0%
            if (!discountPercentage) {
                discountPercentage = 0;
                console.log('Discount percentage not found, setting to 0%');
            }

            // Add or update shield element with discount percentage
            let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
            if (!shieldElement) {
                // Create shield element
                console.log('Creating shield element');
                shieldElement = document.createElement('div');
                shieldElement.className = 'shield_root__SmhpN';
                shieldElement.setAttribute('data-testhook', 'product-shield');

                let shieldTextElement = document.createElement('span');
                shieldTextElement.className = 'shield_text__kNeiW';
                shieldElement.appendChild(shieldTextElement);

                let shieldContainer = productCard.querySelector('[class*="product-card-portrait_shieldProperties__"]');
                if (!shieldContainer) {
                    // Create shield container
                    console.log('Creating shield container');
                    shieldContainer = document.createElement('div');
                    shieldContainer.className = 'product-card-portrait_shieldProperties__+JZJI';
                    let header = productCard.querySelector('[class*="header_root__"]');

					if(header === null) // Albert Heijn Ad products, skipping for now.
						return;
                    header.appendChild(shieldContainer);
                }
				shieldContainer.appendChild(shieldElement);
            }

            // Update shield text with discount percentage
            let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');
            if (!shieldTextElement) {
                shieldTextElement = document.createElement('span');
                shieldTextElement.className = 'shield_text__kNeiW';
                shieldElement.appendChild(shieldTextElement);
            }
            // Just set the text to the discount percentage
            shieldTextElement.textContent = `${discountPercentage}%`;

            // Set background and text color based on discount percentage
            let { backgroundColor, textColor } = getDiscountColors(discountPercentage);
            shieldElement.style.backgroundColor = backgroundColor;
            shieldElement.style.color = textColor; // Ensure text is readable

            // Modify price element to include price per unit in small letters next to discounted price and old price
            let priceContainer = priceElement.parentElement; // Should be '.price_portrait__pcgwD'
            if (priceContainer) {
                // Create or update price per unit element
                let pricePerUnitElement = priceContainer.querySelector('.price-per-unit');
                if (!pricePerUnitElement) {
                    pricePerUnitElement = document.createElement('div');
                    pricePerUnitElement.className = 'price-per-unit';
                    pricePerUnitElement.style.fontSize = 'smaller';
                    priceContainer.appendChild(pricePerUnitElement);
                }
				pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`;
            }

            // Mark this product as processed
            processedProducts.add(productId);
            console.log(`Product ${productId} processed`);
        });

        // Handle promotion cards
        let promotionCards = document.querySelectorAll('.promotion-card_root__ENX4w');
        console.log(`Found ${promotionCards.length} promotion cards`);

        promotionCards.forEach(function(promotionCard, index) {
            // console.debug(`Processing promotion card ${index}`);

            // Use a unique identifier for the promotion
            let promotionId = promotionCard.getAttribute('id') || promotionCard.querySelector('a').getAttribute('href');
            if (!promotionId) {
                console.log('Promotion ID not found, skipping');
                return;
            }

            if (processedProducts.has(promotionId)) {
                // console.debug(`Promotion ${promotionId} already processed, skipping`);
                return;
            }

            // Try to extract from description and title
            let cardDescription = promotionCard.querySelector('[data-testhook="card-description"]');
            let cardTitle = promotionCard.querySelector('[data-testhook="card-title"]');
            let descriptionText = cardDescription?.textContent.trim() || '';
            let titleText = cardTitle?.textContent.trim() || '';

            // DEBUG stuff
            //if(!/Verse sappen/.test(titleText))return;

            //console.log(`Processing promotion ${promotionId}`);

            // Extract current price and previous price
            let priceElement = promotionCard.querySelector('[data-testhook="price"]');
            if (!priceElement) {
                console.log(`Price element not found for ${titleText}, skipping`);
                return;
            }
            let priceNow = parseFloat(priceElement.getAttribute('data-testpricenow'));
            let priceWas = parseFloat(priceElement.getAttribute('data-testpricewas'));
            console.log(`Price now: ${priceNow}, Price was: ${priceWas} for ${titleText}`);

            // Extract unit size, if available
            let count = null;
            let unit = null;

            console.log(`Description text: ${descriptionText}`);
            console.log(`Title text: ${titleText}`);

            // Try to extract weight from description or title
			const unitRe = /(^|\s|-)([\d.,]+|\bPer)\s+\b(gram|zak|kilo|ml|stuk)s?(\s|$)/i;
            let unitMatch;
			unitMatch = titleText.match(unitRe);
            console.log('unit', unitMatch);
			if(!unitMatch)
				unitMatch = descriptionText.match(unitRe);

			if (unitMatch) {
				if(unitMatch[2] == 'Per'){
					count = 1;
				}else{
                	count = parseFloat(unitMatch[2].replace(',', '.'));
				}
                unit = unitMatch[3].toLowerCase();
                if(unit === 'gram'){
					count *= 0.001;
				}
                if (unit === 'kilo' || unit == 'gram') {
					unit = 'kg';
                }
                if (unit === 'ml'){
                    count *= 0.001;
                    unit = 'l';
                }
				console.log(`${count} ${unit}s`);
            } else {
                console.log('Unit not found in description or title');
            }

            // Calculate discount percentage
            let discountPercentage = null;
            if (priceWas && priceNow) {
                discountPercentage = ((priceWas - priceNow) / priceWas) * 100;
                discountPercentage = discountPercentage.toFixed(1);
                console.log(`Calculated discount percentage: ${discountPercentage}%`);
            } else {
                // Try extracting from promotion shield
                let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]');
                if (promotionShields) {
                    let shieldText = promotionShields.textContent.trim();
                    console.log(`Promotion shield text: ${shieldText}, ${priceWas}, ${priceNow}`);
                    discountPercentage = parsePromotionText(shieldText, priceWas);
                    if (discountPercentage) {
                        console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
                    }
                }
            }

            if (!discountPercentage) {
                discountPercentage = 0;
                console.log('Discount percentage not found, setting to 0%');
            }

            // Add or update promotion shield with discount percentage
            let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]');
            if (!promotionShields) {
                // Create promotion shields element
                console.log('Creating promotion shields element');
                promotionShields = document.createElement('div');
                promotionShields.className = 'promotion-shields_root__cVEfN';
                promotionShields.setAttribute('data-testhook', 'promotion-shields');
                promotionShields.style = promotionCard.querySelector('[class*="promotion-card-content_root__"]')?.style || '';

                let cardContent = promotionCard.querySelector('[class*="promotion-card-content_root__"]');
                cardContent.insertBefore(promotionShields, cardContent.firstChild);
            }

            // Update promotion shield text with discount percentage
            // Replace the entire element with discount percentage
            promotionShields.innerHTML = ''; // Clear existing content

            let shieldP = document.createElement('p');
            shieldP.className = 'typography_root__Om3Wh typography_variant-paragraph__T5ZAU typography_hasMargin__4EaQi promotion-shield_root__mIDdK';
            shieldP.setAttribute('data-testhook', 'promotion-shield');

            let promotionShieldText = document.createElement('span');
            promotionShieldText.className = 'promotion-text_root__1sn7K promotion-text_large__lTZOA';
            promotionShieldText.setAttribute('data-testhook', 'promotion-text');

            // Set the discount percentage as text
            promotionShieldText.textContent = `${discountPercentage}%`;

            // Set background and text color based on discount percentage
            let { backgroundColor, textColor } = getDiscountColors(discountPercentage * 1.5);
            shieldP.style.backgroundColor = backgroundColor;
            shieldP.style.color = textColor; // Ensure text is readable

            shieldP.appendChild(promotionShieldText);
            promotionShields.appendChild(shieldP);

            // If unit is available, calculate price per unit
            if (count) {
                let pricePerUnit = priceNow / count;
                console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}, ${priceNow}, ${count}`);

                // Modify price element to include price per kg in small letters next to discounted price and old price
                let priceContainer = priceElement; // Assuming this is the correct container
                if (priceContainer) {
                    // Create or update price per kg element
                    let pricePerUnitElement = priceContainer.querySelector('.price-per-unit');
                    if (!pricePerUnitElement) {
                        pricePerUnitElement = document.createElement('div');
                        pricePerUnitElement.className = 'price-per-unit';
                        pricePerUnitElement.style.fontSize = 'smaller';
                        pricePerUnitElement.style.marginTop = '4px';
                        priceContainer.appendChild(pricePerUnitElement);
                    }
                    pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`;
                }
            }

            // Mark this promotion as processed
            processedProducts.add(promotionId);
            console.log(`Promotion ${promotionId} processed`);
        });

        console.log('Finished updateProductInfo');
    }

    function parsePromotionText(shieldText, pricePerItem) {
        let discountPercentage = null;
        shieldText = shieldText.toLowerCase(); // Normalize the text
        console.log(`parsing promotion price ${shieldText}, ${pricePerItem}.`);
        // Handle "% korting"
        if (shieldText.includes('%')) {
            // e.g., "25% korting"
            let discountMatch = shieldText.match(/(\d+)%\s*korting/i);
            if (discountMatch) {
                discountPercentage = parseFloat(discountMatch[1]);
            }
        } else if (shieldText.includes('gratis')) {
            // e.g., "1+1 gratis"
            if (shieldText.includes('1+1') || shieldText.includes('1 + 1') || shieldText.includes('1+1 gratis')) {
                discountPercentage = 50;
            } else if (shieldText.includes('2+1') || shieldText.includes('2 + 1')) {
                discountPercentage = 33.33;
            }
            // Add more cases as necessary
        } else if (shieldText.includes('2e halve prijs')) {
            discountPercentage = 25; // Assuming 25% off on 2 items
        } else {
            // Try to match "X VOOR Y"
            let match = shieldText.match(/^(\d*)\s*voor\s*(\d+[.,]?\d*)/i);
            if (match && pricePerItem) {
                let nItems = parseInt(match[1]) || 1;
                let totalPrice = parseFloat(match[2].replace(',', '.'));
                let normalTotalPrice = pricePerItem * nItems;
                discountPercentage = ((normalTotalPrice - totalPrice) / normalTotalPrice) * 100;
                discountPercentage = discountPercentage.toFixed(1);
                console.log(`Calculated discount percentage from "X VOOR Y": ${discountPercentage}%`);
            }
        }
        return discountPercentage;
    }

    function getDiscountColors(discountPercentage) {
        // Use predefined colors and text colors based on discount percentage ranges
        let backgroundColor, textColor;

        if (discountPercentage >= 80) {
            backgroundColor = '#008000'; // Dark Green
            textColor = '#FFFFFF'; // White
        } else if (discountPercentage >= 60) {
            backgroundColor = '#32CD32'; // Lime Green
            textColor = '#000000'; // Black
        } else if (discountPercentage >= 40) {
            backgroundColor = '#FFFF00'; // Yellow
            textColor = '#000000'; // Black
        } else if (discountPercentage >= 20) {
            backgroundColor = '#FFA500'; // Orange
            textColor = '#000000'; // Black
        } else {
            backgroundColor = '#FF0000'; // Red
            textColor = '#FFFFFF'; // White
        }

        return { backgroundColor, textColor };
    }

    // Run immediately
	window.setTimeout(updateProductInfo, 1000);
    // Run the update function every 5 seconds
    if(!DEBUG)setInterval(updateProductInfo, 5000);
})();