Albert Heijn Korting Updated (with Promotion Parsing)

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

当前为 2025-02-26 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();