Last active
February 26, 2025 00:13
-
-
Save wolph/1e245ffc144af232be5d79fc9b44dc43 to your computer and use it in GitHub Desktop.
Albert Heijn AH.nl kortingspercentage en prijs per kg script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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 | |
// @downloadURL https://update.greasyfork.org/scripts/515067/Albert%20Heijn%20Korting.user.js | |
// @updateURL https://update.greasyfork.org/scripts/515067/Albert%20Heijn%20Korting.meta.js | |
// ==/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); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment