Albert Heijn Korting

Add price per gram and discount percentage to products

当前为 2024-11-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Albert Heijn Korting
  3. // @namespace https://wol.ph/
  4. // @version 1.0.2
  5. // @description Add price per gram and discount percentage to products
  6. // @author wolph
  7. // @match https://www.ah.nl/*
  8. // @icon https://icons.duckduckgo.com/ip2/ah.nl.ico
  9. // @grant none
  10. // @license BSD
  11. // ==/UserScript==
  12.  
  13. const DEBUG = false;
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // Set to keep track of processed products
  19. const processedProducts = new Set();
  20.  
  21. function updateProductInfo() {
  22. console.log('Starting updateProductInfo');
  23.  
  24. // Handle normal product cards
  25. let productCards;
  26. if(DEBUG)
  27. productCards = [document.querySelector('article[data-testhook="product-card"]')];
  28. else
  29. productCards = document.querySelectorAll('article[data-testhook="product-card"]');
  30.  
  31. console.log(`Found ${productCards.length} product cards`);
  32.  
  33. productCards.forEach(function(productCard, index) {
  34. console.log(`Processing product card ${index}`);
  35.  
  36. // Use a unique identifier for the product
  37. let productId = productCard.getAttribute('data-product-id') || productCard.querySelector('a.link_root__EqRHd')?.getAttribute('href');
  38. if (!productId) {
  39. console.log('Product ID not found, skipping');
  40. return;
  41. }
  42.  
  43. if (processedProducts.has(productId)) {
  44. console.log(`Product ${productId} already processed, skipping`);
  45. return;
  46. }
  47.  
  48. console.log(`Processing product ${productId}`);
  49.  
  50. // Extract price
  51. let priceElement = productCard.querySelector('[data-testhook="price-amount"]');
  52. if (!priceElement) {
  53. console.log('Price element not found, skipping');
  54. return;
  55. }
  56. let priceInt = priceElement.querySelector('.price-amount_integer__\\+e2XO');
  57. let priceFrac = priceElement.querySelector('.price-amount_fractional__kjJ7u');
  58. if (!priceInt || !priceFrac) {
  59. console.log('Price integer or fractional part not found, skipping');
  60. return;
  61. }
  62. let price = parseFloat(priceInt.textContent + '.' + priceFrac.textContent);
  63. console.log(`Price: ${price}`);
  64.  
  65. // Extract unit size
  66. let unitSizeElement = productCard.querySelector('[data-testhook="product-unit-size"]');
  67. if (!unitSizeElement) {
  68. console.log('Unit size element not found, skipping');
  69. return;
  70. }
  71. let unitSizeText = unitSizeElement.textContent.trim(); // e.g., "300 g"
  72. console.log(`Unit size text: ${unitSizeText}`);
  73.  
  74. // Parse unit size
  75. let weightMatch = unitSizeText.match(/([\d.,]+)\s*(g|kg)/i);
  76. let countMatch = unitSizeText.match(/(\d+)\s*(stuk)s/i);
  77. if (!weightMatch && !countMatch) {
  78. console.log('Weight and count not found in unit size text, skipping');
  79. return;
  80. }
  81.  
  82. let pricePerUnit = 0;
  83. let unit = '';
  84.  
  85. if(weightMatch){
  86. let weight = parseFloat(weightMatch[1].replace(',', '.'));
  87. unit = weightMatch[2].toLowerCase();
  88. console.log(`Weight: ${weight}, Unit: ${unit}`);
  89.  
  90. // Convert weight to grams
  91. if (unit === 'kg') {
  92. weight = weight * 1000;
  93. console.log(`Converted weight to grams: ${weight}`);
  94. } else {
  95. // We calculate with grams but we display per kg in all cases.
  96. unit = 'kg';
  97. }
  98.  
  99. // Calculate price per kg
  100. pricePerUnit = (price * 1000) / weight; // Price per kg
  101. }
  102. if(countMatch){
  103. let count = parseInt(countMatch[1]);
  104. unit = countMatch[2].toLowerCase();
  105. console.log(`Count: ${count}`);
  106.  
  107. // Calculate price per item
  108. pricePerUnit = price / count; // Price per item
  109. }
  110. console.log(`Price per ${unit}: ${pricePerUnit.toFixed(2)}`);
  111.  
  112. // Extract old price if available
  113. let oldPrice = null;
  114. // Assuming there's an element for the old price
  115. let oldPriceElement = priceElement.querySelector('.price-amount_original__A6_Ck');
  116. if (oldPriceElement) {
  117. let oldPriceInt = oldPriceElement.querySelector('.price-amount_integer__+e2XO');
  118. let oldPriceFrac = oldPriceElement.querySelector('.price-amount_fractional__kjJ7u');
  119. if (oldPriceInt && oldPriceFrac) {
  120. oldPrice = parseFloat(oldPriceInt.textContent + '.' + oldPriceFrac.textContent);
  121. console.log(`Old price: ${oldPrice}`);
  122. }
  123. }
  124.  
  125. // Calculate discount percentage
  126. let discountPercentage = null;
  127. if (oldPrice) {
  128. discountPercentage = ((oldPrice - price) / oldPrice) * 100;
  129. discountPercentage = discountPercentage.toFixed(1);
  130. console.log(`Calculated discount percentage: ${discountPercentage}%`);
  131. } else {
  132. // Try extracting from shield
  133. let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
  134. if (shieldElement) {
  135. let shieldTextElement = shieldElement.querySelector('.shield_text__kNeiW');
  136. if (shieldTextElement) {
  137. let shieldText = shieldTextElement.textContent.trim();
  138. console.log(`Shield text: ${shieldText}`);
  139. discountPercentage = parsePromotionText(shieldText);
  140. if (discountPercentage) {
  141. console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
  142. }
  143. }
  144. }
  145. }
  146.  
  147. // If discount percentage is not found, set it to 0%
  148. if (!discountPercentage) {
  149. discountPercentage = 0;
  150. console.log('Discount percentage not found, setting to 0%');
  151. }
  152.  
  153. // Add or update shield element with discount percentage
  154. let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
  155. if (!shieldElement) {
  156. // Create shield element
  157. console.log('Creating shield element');
  158. shieldElement = document.createElement('div');
  159. shieldElement.className = 'shield_root__SmhpN';
  160. shieldElement.setAttribute('data-testhook', 'product-shield');
  161.  
  162. let shieldTextElement = document.createElement('span');
  163. shieldTextElement.className = 'shield_text__kNeiW';
  164. shieldElement.appendChild(shieldTextElement);
  165.  
  166. let shieldContainer = productCard.querySelector('.product-card-portrait_shieldProperties__+JZJI');
  167. if (!shieldContainer) {
  168. // Create shield container
  169. console.log('Creating shield container');
  170. shieldContainer = document.createElement('div');
  171. shieldContainer.className = 'product-card-portrait_shieldProperties__+JZJI';
  172. let header = productCard.querySelector('.header_root__ilMls');
  173.  
  174. if(header === null) // Albert Heijn Ad products, skipping for now.
  175. return;
  176. header.appendChild(shieldContainer);
  177. }
  178. shieldContainer.appendChild(shieldElement);
  179. }
  180.  
  181. // Update shield text with discount percentage
  182. let shieldTextElement = shieldElement.querySelector('.shield_text__kNeiW');
  183. if (!shieldTextElement) {
  184. shieldTextElement = document.createElement('span');
  185. shieldTextElement.className = 'shield_text__kNeiW';
  186. shieldElement.appendChild(shieldTextElement);
  187. }
  188. // Just set the text to the discount percentage
  189. shieldTextElement.textContent = `${discountPercentage}%`;
  190.  
  191. // Set background and text color based on discount percentage
  192. let { backgroundColor, textColor } = getDiscountColors(discountPercentage);
  193. shieldElement.style.backgroundColor = backgroundColor;
  194. shieldElement.style.color = textColor; // Ensure text is readable
  195.  
  196. // Modify price element to include price per unit in small letters next to discounted price and old price
  197. let priceContainer = priceElement.parentElement; // Should be '.price_portrait__pcgwD'
  198. if (priceContainer) {
  199. // Create or update price per unit element
  200. let pricePerUnitElement = priceContainer.querySelector('.price-per-unit');
  201. if (!pricePerUnitElement) {
  202. pricePerUnitElement = document.createElement('div');
  203. pricePerUnitElement.className = 'price-per-unit';
  204. pricePerUnitElement.style.fontSize = 'smaller';
  205. priceContainer.appendChild(pricePerUnitElement);
  206. }
  207. pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`;
  208. }
  209.  
  210. // Mark this product as processed
  211. processedProducts.add(productId);
  212. console.log(`Product ${productId} processed`);
  213. });
  214.  
  215. // Handle promotion cards
  216. let promotionCards = document.querySelectorAll('.promotion-card_root__ENX4w');
  217. console.log(`Found ${promotionCards.length} promotion cards`);
  218.  
  219. promotionCards.forEach(function(promotionCard, index) {
  220. console.log(`Processing promotion card ${index}`);
  221.  
  222. // Use a unique identifier for the promotion
  223. let promotionId = promotionCard.getAttribute('id') || promotionCard.querySelector('a').getAttribute('href');
  224. if (!promotionId) {
  225. console.log('Promotion ID not found, skipping');
  226. return;
  227. }
  228.  
  229. if (processedProducts.has(promotionId)) {
  230. console.log(`Promotion ${promotionId} already processed, skipping`);
  231. return;
  232. }
  233.  
  234. console.log(`Processing promotion ${promotionId}`);
  235.  
  236. // Extract current price and previous price
  237. let priceElement = promotionCard.querySelector('[data-testhook="price"]');
  238. if (!priceElement) {
  239. console.log('Price element not found, skipping');
  240. return;
  241. }
  242. let priceNow = parseFloat(priceElement.getAttribute('data-testpricenow'));
  243. let priceWas = parseFloat(priceElement.getAttribute('data-testpricewas'));
  244. console.log(`Price now: ${priceNow}, Price was: ${priceWas}`);
  245.  
  246. // Extract unit size, if available
  247. let weight = null;
  248. let unit = null;
  249.  
  250. // Try to extract from description and title
  251. let cardDescription = promotionCard.querySelector('[data-testhook="card-description"]');
  252. let cardTitle = promotionCard.querySelector('[data-testhook="card-title"]');
  253. let descriptionText = cardDescription?.textContent.trim() || '';
  254. let titleText = cardTitle?.textContent.trim() || '';
  255.  
  256. console.log(`Description text: ${descriptionText}`);
  257. console.log(`Title text: ${titleText}`);
  258.  
  259. // Try to extract weight from description or title
  260. let weightMatch = descriptionText.match(/([\d.,]+)\s*(g|kg)/i) || titleText.match(/([\d.,]+)\s*(g|kg)/i);
  261. if (weightMatch) {
  262. weight = parseFloat(weightMatch[1].replace(',', '.'));
  263. unit = weightMatch[2].toLowerCase();
  264. if (unit === 'kg') {
  265. weight = weight * 1000;
  266. }
  267. console.log(`Weight: ${weight} grams`);
  268. } else {
  269. console.log('Weight not found in description or title');
  270. }
  271.  
  272. // Calculate discount percentage
  273. let discountPercentage = null;
  274. if (priceWas && priceNow) {
  275. discountPercentage = ((priceWas - priceNow) / priceWas) * 100;
  276. discountPercentage = discountPercentage.toFixed(1);
  277. console.log(`Calculated discount percentage: ${discountPercentage}%`);
  278. } else {
  279. // Try extracting from promotion shield
  280. let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]');
  281. if (promotionShields) {
  282. let shieldText = promotionShields.textContent.trim();
  283. console.log(`Promotion shield text: ${shieldText}`);
  284. discountPercentage = parsePromotionText(shieldText);
  285. if (discountPercentage) {
  286. console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
  287. }
  288. }
  289. }
  290.  
  291. if (!discountPercentage) {
  292. discountPercentage = 0;
  293. console.log('Discount percentage not found, setting to 0%');
  294. }
  295.  
  296. // Add or update promotion shield with discount percentage
  297. let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]');
  298. if (!promotionShields) {
  299. // Create promotion shields element
  300. console.log('Creating promotion shields element');
  301. promotionShields = document.createElement('div');
  302. promotionShields.className = 'promotion-shields_root__cVEfN';
  303. promotionShields.setAttribute('data-testhook', 'promotion-shields');
  304. promotionShields.style = promotionCard.querySelector('.promotion-card-content_root__A5Fda')?.style || '';
  305.  
  306. let cardContent = promotionCard.querySelector('.promotion-card-content_root__A5Fda');
  307. cardContent.insertBefore(promotionShields, cardContent.firstChild);
  308. }
  309.  
  310. // Update promotion shield text with discount percentage
  311. // Replace the entire element with discount percentage
  312. promotionShields.innerHTML = ''; // Clear existing content
  313.  
  314. let shieldP = document.createElement('p');
  315. shieldP.className = 'typography_root__Om3Wh typography_variant-paragraph__T5ZAU typography_hasMargin__4EaQi promotion-shield_root__mIDdK';
  316. shieldP.setAttribute('data-testhook', 'promotion-shield');
  317.  
  318. let promotionShieldText = document.createElement('span');
  319. promotionShieldText.className = 'promotion-text_root__1sn7K promotion-text_large__lTZOA';
  320. promotionShieldText.setAttribute('data-testhook', 'promotion-text');
  321.  
  322. // Set the discount percentage as text
  323. promotionShieldText.textContent = `${discountPercentage}%`;
  324.  
  325. // Set background and text color based on discount percentage
  326. let { backgroundColor, textColor } = getDiscountColors(discountPercentage * 1.5);
  327. shieldP.style.backgroundColor = backgroundColor;
  328. shieldP.style.color = textColor; // Ensure text is readable
  329.  
  330. shieldP.appendChild(promotionShieldText);
  331. promotionShields.appendChild(shieldP);
  332.  
  333. // If weight is available, calculate price per kg
  334. if (weight) {
  335. let pricePerUnit = (priceNow * 1000) / weight;
  336. console.log(`Price per kg: ${pricePerUnit.toFixed(2)}`);
  337.  
  338. // Modify price element to include price per kg in small letters next to discounted price and old price
  339. let priceContainer = priceElement; // Assuming this is the correct container
  340. if (priceContainer) {
  341. // Create or update price per kg element
  342. let pricePerUnitElement = priceContainer.querySelector('.price-per-kg');
  343. if (!pricePerUnitElement) {
  344. pricePerUnitElement = document.createElement('div');
  345. pricePerUnitElement.className = 'price-per-kg';
  346. pricePerUnitElement.style.fontSize = 'smaller';
  347. pricePerUnitElement.style.marginTop = '4px';
  348. priceContainer.appendChild(pricePerUnitElement);
  349. }
  350. pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per kg`;
  351. }
  352. }
  353.  
  354. // Mark this promotion as processed
  355. processedProducts.add(promotionId);
  356. console.log(`Promotion ${promotionId} processed`);
  357. });
  358.  
  359. console.log('Finished updateProductInfo');
  360. }
  361.  
  362. function parsePromotionText(shieldText) {
  363. let discountPercentage = null;
  364. if (shieldText.includes('%')) {
  365. // e.g., "25% korting"
  366. let discountMatch = shieldText.match(/(\d+)%\s*korting/i);
  367. if (discountMatch) {
  368. discountPercentage = parseFloat(discountMatch[1]);
  369. }
  370. } else if (shieldText.toLowerCase().includes('gratis')) {
  371. if (shieldText.includes('1+1') || shieldText.includes('1 + 1') || shieldText.includes('1+1 gratis')) {
  372. discountPercentage = 50;
  373. } else if (shieldText.includes('2+1') || shieldText.includes('2 + 1')) {
  374. discountPercentage = 33.33;
  375. }
  376. // Add more cases as necessary
  377. } else if (shieldText.includes('2e halve prijs')) {
  378. discountPercentage = 25; // Assuming 25% off on 2 items
  379. }
  380. return discountPercentage;
  381. }
  382.  
  383. function getDiscountColors(discountPercentage) {
  384. // Use predefined colors and text colors based on discount percentage ranges
  385. let backgroundColor, textColor;
  386.  
  387. if (discountPercentage >= 80) {
  388. backgroundColor = '#008000'; // Dark Green
  389. textColor = '#FFFFFF'; // White
  390. } else if (discountPercentage >= 60) {
  391. backgroundColor = '#32CD32'; // Lime Green
  392. textColor = '#000000'; // Black
  393. } else if (discountPercentage >= 40) {
  394. backgroundColor = '#FFFF00'; // Yellow
  395. textColor = '#000000'; // Black
  396. } else if (discountPercentage >= 20) {
  397. backgroundColor = '#FFA500'; // Orange
  398. textColor = '#000000'; // Black
  399. } else {
  400. backgroundColor = '#FF0000'; // Red
  401. textColor = '#FFFFFF'; // White
  402. }
  403.  
  404. return { backgroundColor, textColor };
  405. }
  406.  
  407. // Run immediately
  408. window.setTimeout(updateProductInfo, 1000);
  409. // Run the update function every 5 seconds
  410. if(!DEBUG)setInterval(updateProductInfo, 5000);
  411. })();