Albert Heijn Korting Updated (with Promotion Parsing)

Add price per unit and discount percentage to products and promotion cards (updated for new page structures)

// ==UserScript==
// @name         Albert Heijn Korting Updated (with Promotion Parsing)
// @namespace    https://wol.ph/
// @version      1.0.6
// @description  Add price per unit and discount percentage to products and promotion cards (updated for new page structures)
// @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 and cards to avoid duplicate processing.
    const processedProducts = new Set();

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

        // --- PROCESS PRODUCT CARDS ---
        let productCards = document.querySelectorAll('article[data-testhook="product-card"]');
        console.log(`Found ${productCards.length} product cards`);
        productCards.forEach(function (productCard) {
            // 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 product-card");
                return;
            }

            if (processedProducts.has(productId)) {
                return;
            }

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

            // --- Extract current price ---
            // Look for the highlighted price element
            let priceElement = productCard.querySelector('[data-testhook="price-amount"].price-amount_highlight__ekL92');
            if (!priceElement) {
                console.log("Highlighted price element not found, skipping product-card");
                return;
            }
            // Extract integer and fractional parts
            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 product-card");
                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 product-card");
                return;
            }
            let unitSizeText = unitSizeElement.textContent.trim(); // e.g., "ca. 755 g" or "750 g"
            console.log(`Unit size text: ${unitSizeText}`);

            // Parse unit size (supports weight and count).
            let unitMatch = unitSizeText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i);
            let countMatch = unitSizeText.match(/(\d+)\s*(stuk|stuks)/i);
            if (!unitMatch && !countMatch) {
                console.log("Weight and count not found in unit size text, skipping product-card");
                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 needed; we calculate per kg
                if (unit === "kg") {
                    weight = weight * 1000;
                    console.log(`Converted weight to grams: ${weight}`);
                } else {
                    // We display per kg even if provided in grams.
                    unit = "kg";
                }
                // Calculate price per kg
                pricePerUnit = (price * 1000) / weight;
            }
            if (countMatch) {
                let count = parseInt(countMatch[1]);
                unit = countMatch[2].toLowerCase();
                console.log(`Count: ${count}`);
                // Calculate price per item
                pricePerUnit = price / count;
            }
            console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}`);

            // --- Extract old price, if available ---
            let oldPrice = null;
            let oldPriceElement = productCard.querySelector('[data-testhook="price-amount"]:not(.price-amount_highlight__ekL92)');
            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 && oldPrice > price) {
                discountPercentage = (((oldPrice - price) / oldPrice) * 100).toFixed(1);
                console.log(`Calculated discount from prices: ${discountPercentage}%`);
            } else {
                // Try extracting discount percentage from shield promotion text
                let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
                if (shieldElement) {
                    let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');
                    if (shieldTextElement) {
                        let shieldText = shieldTextElement.textContent.trim();
                        discountPercentage = parsePromotionText(shieldText, price) || "0";
                        console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
                    } else {
                        discountPercentage = "0";
                        console.log("No shield text found, setting discount to 0%");
                    }
                } else {
                    discountPercentage = "0";
                    console.log("Shield element not found, setting discount to 0%");
                }
            }

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

                let newShieldTextElement = document.createElement("span");
                newShieldTextElement.className = "shield_text__kNeiW";
                shieldElement.appendChild(newShieldTextElement);

                let shieldContainer = productCard.querySelector('[class*="product-card-portrait_shieldProperties__"]');
                if (!shieldContainer) {
                    console.log("Creating shield container for product-card");
                    shieldContainer = document.createElement("div");
                    shieldContainer.className = "product-card-portrait_shieldProperties__+JZJI";
                    let header = productCard.querySelector('[class*="header_root__"]');
                    if (header === null)
                        return;
                    header.appendChild(shieldContainer);
                }
                shieldContainer.appendChild(shieldElement);
            }
            let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');
            if (!shieldTextElement) {
                shieldTextElement = document.createElement("span");
                shieldTextElement.className = "shield_text__kNeiW";
                shieldElement.appendChild(shieldTextElement);
            }
            // Update the shield text with 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;

            // --- Update price element to include price per unit info ---
            let priceContainer = priceElement.parentElement;
            if (priceContainer) {
                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`);
        });

        // --- PROCESS PROMOTION CARDS (NEW STRUCTURE) ---
        let promotionCards = document.querySelectorAll('a.promotion-card_root__tQA3z');
        console.log(`Found ${promotionCards.length} promotion cards`);
        promotionCards.forEach(function (card) {
            // Use the element's id or href as a unique identifier
            let cardId = card.getAttribute("id") || card.getAttribute("href");
            if (!cardId) {
                console.log("Promotion card unique id not found, skipping");
                return;
            }
            if (processedProducts.has(cardId)) {
                return;
            }
            console.log(`Processing promotion card ${cardId}`);

            // --- Extract current price ---
            let priceElem = card.querySelector('[data-testhook="price"]');
            if (!priceElem) {
                console.log("Promotion price element not found, skipping promotion card");
                return;
            }
            let priceNow = priceElem.getAttribute("data-testpricenow");
            if (!priceNow) {
                console.log("Promotion current price attribute missing, skipping");
                return;
            }
            let currentPrice = parseFloat(priceNow);
            console.log(`Promotion current price: ${currentPrice}`);

            // --- Extract old price, if available ---
            let oldPriceAttr = priceElem.getAttribute("data-testpricewas");
            let oldPrice = oldPriceAttr ? parseFloat(oldPriceAttr) : null;
            if (oldPrice) {
                console.log(`Promotion old price: ${oldPrice}`);
            }

            // --- Parse unit size from title ---
            // We assume that the card title contains unit information (e.g. "400 g", "1 l", "2 stuks")
            let titleElem = card.querySelector('[data-testhook="card-title"]');
            if (!titleElem) {
                console.log("Promotion card title not found, skipping unit extraction");
                return;
            }
            let titleText = titleElem.textContent.trim();
            console.log(`Promotion card title: "${titleText}"`);

            let unitMatch = titleText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i);
            let countMatch = titleText.match(/(\d+)\s*(stuk|stuks)/i);
            let ppu = 0;
            let unit = "";
            if (unitMatch) {
                let weight = parseFloat(unitMatch[2].replace(",", "."));
                unit = unitMatch[3].toLowerCase();
                console.log(`Parsed weight: ${weight} ${unit}`);
                if (unit === "kg") {
                    weight *= 1000;
                } else {
                    unit = "kg";  // we display per kg even if provided in grams
                }
                ppu = (currentPrice * 1000) / weight;
            } else if (countMatch) {
                let count = parseInt(countMatch[1]);
                unit = "stuk";
                console.log(`Parsed count: ${count} ${unit}(s)`);
                ppu = currentPrice / count;
            } else {
                console.log("No unit size found in promotion card title, skipping price per unit calculation");
            }

            // --- Calculate discount percentage ---
            let discountPercentage = null;
            if (oldPrice && oldPrice > currentPrice) {
                discountPercentage = (((oldPrice - currentPrice) / oldPrice) * 100).toFixed(1);
                console.log(`Calculated discount from promotion prices: ${discountPercentage}%`);
            } else {
                discountPercentage = "0";
                console.log("No discount available on promotion card");
            }

            // --- Update shield element ---
            // Promotion cards usually include a shield container with promotion texts.
            let shieldElem = card.querySelector('[data-testhook="promotion-shields"]');
            if (shieldElem) {
                // Look for an element that displays promotion text within shield.
                let shieldTextElem = shieldElem.querySelector('[data-testhook="promotion-text"]');
                if (shieldTextElem) {
                    shieldTextElem.textContent = discountPercentage + "%";
                } else {
                    shieldTextElem = document.createElement("span");
                    shieldTextElem.setAttribute("data-testhook", "promotion-text");
                    shieldTextElem.textContent = discountPercentage + "%";
                    shieldElem.appendChild(shieldTextElem);
                }
                let colors = getDiscountColors(discountPercentage);
                shieldElem.style.backgroundColor = colors.backgroundColor;
                shieldElem.style.color = colors.textColor;
            }

            // --- Update or create price-per-unit element ---
            let priceContainer = priceElem.parentElement;
            if (priceContainer) {
                let ppuElem = priceContainer.querySelector(".price-per-unit");
                if (!ppuElem) {
                    ppuElem = document.createElement("div");
                    ppuElem.className = "price-per-unit";
                    ppuElem.style.fontSize = "smaller";
                    priceContainer.appendChild(ppuElem);
                }
                if (ppu > 0) {
                    ppuElem.textContent = "€" + ppu.toFixed(2) + " per " + unit;
                }
            }

            processedProducts.add(cardId);
            console.log(`Promotion card ${cardId} processed`);
        });

        console.log("Finished updateProductInfo");
    }

    // Helper function to parse promotion text (e.g., "1 + 1 gratis")
    function parsePromotionText(shieldText, pricePerItem) {
        shieldText = shieldText.toLowerCase();
        let discountPercentage = null;
        if (shieldText.includes("%")) {
            let discountMatch = shieldText.match(/(\d+)%\s*korting/i);
            if (discountMatch) {
                discountPercentage = parseFloat(discountMatch[1]);
            }
        } else if (shieldText.includes("gratis")) {
            if (shieldText.includes("1+1") || shieldText.includes("1 + 1")) {
                discountPercentage = 50;
            } else if (shieldText.includes("2+1") || shieldText.includes("2 + 1")) {
                discountPercentage = 33.33;
            }
        } else if (shieldText.includes("2e halve prijs")) {
            discountPercentage = 25;
        }
        return discountPercentage;
    }

    function getDiscountColors(discountPercentage) {
        let backgroundColor, textColor;
        discountPercentage = parseFloat(discountPercentage);
        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 };
    }

    // Initial run and periodic update
    window.setTimeout(updateProductInfo, 1000);
    if (!DEBUG) setInterval(updateProductInfo, 5000);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址