Albert Heijn Korting

Add price per gram and discount percentage to products

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

  1. // ==UserScript==
  2. // @name Albert Heijn Korting
  3. // @namespace https://wol.ph/
  4. // @version 1.0.3
  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.debug(`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.debug(`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 unitMatch = unitSizeText.match(/(ca\. |)([\d.,]+)\s*(g|kg)/i);
  76. let countMatch = unitSizeText.match(/(\d+)\s*(stuk)s/i);
  77. if (!unitMatch && !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(unitMatch){
  86. let weight = parseFloat(unitMatch[2].replace(',', '.'));
  87. unit = unitMatch[3].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.debug(`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.debug(`Promotion ${promotionId} already processed, skipping`);
  231. return;
  232. }
  233.  
  234. // Try to extract from description and title
  235. let cardDescription = promotionCard.querySelector('[data-testhook="card-description"]');
  236. let cardTitle = promotionCard.querySelector('[data-testhook="card-title"]');
  237. let descriptionText = cardDescription?.textContent.trim() || '';
  238. let titleText = cardTitle?.textContent.trim() || '';
  239.  
  240. //console.log(`Processing promotion ${promotionId}`);
  241.  
  242. // Extract current price and previous price
  243. let priceElement = promotionCard.querySelector('[data-testhook="price"]');
  244. if (!priceElement) {
  245. console.log(`Price element not found for ${titleText}, skipping`);
  246. return;
  247. }
  248. let priceNow = parseFloat(priceElement.getAttribute('data-testpricenow'));
  249. let priceWas = parseFloat(priceElement.getAttribute('data-testpricewas'));
  250. console.log(`Price now: ${priceNow}, Price was: ${priceWas} for ${titleText}`);
  251.  
  252. // Extract unit size, if available
  253. let count = null;
  254. let unit = null;
  255.  
  256. console.log(`Description text: ${descriptionText}`);
  257. console.log(`Title text: ${titleText}`);
  258.  
  259. // Try to extract weight from description or title
  260. const unitRe = /(^|\s|-)([\d.,]+|\bPer)\s+\b(gram|zak|kilo|ml|stuk)s?(\s|$)/i;
  261. let unitMatch;
  262. unitMatch = titleText.match(unitRe);
  263. if(!unitMatch)
  264. unitMatch = descriptionText.match(unitRe);
  265.  
  266. console.log(unitMatch);
  267. if (unitMatch) {
  268. if(unitMatch[2] == 'Per'){
  269. count = 1;
  270. }else{
  271. count = parseFloat(unitMatch[2].replace(',', '.'));
  272. }
  273. unit = unitMatch[3].toLowerCase();
  274. if(unit === 'gram'){
  275. count *= 0.001;
  276. }
  277. if (unit === 'kilo' || unit == 'gram') {
  278. unit = 'kg';
  279. }
  280.  
  281. console.log(`${count} ${unit}s`);
  282. } else {
  283. console.log('Unit not found in description or title');
  284. }
  285.  
  286. // Calculate discount percentage
  287. let discountPercentage = null;
  288. if (priceWas && priceNow) {
  289. discountPercentage = ((priceWas - priceNow) / priceWas) * 100;
  290. discountPercentage = discountPercentage.toFixed(1);
  291. console.log(`Calculated discount percentage: ${discountPercentage}%`);
  292. } else {
  293. // Try extracting from promotion shield
  294. let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]');
  295. if (promotionShields) {
  296. let shieldText = promotionShields.textContent.trim();
  297. console.log(`Promotion shield text: ${shieldText}`);
  298. discountPercentage = parsePromotionText(shieldText);
  299. if (discountPercentage) {
  300. console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
  301. }
  302. }
  303. }
  304.  
  305. if (!discountPercentage) {
  306. discountPercentage = 0;
  307. console.log('Discount percentage not found, setting to 0%');
  308. }
  309.  
  310. // Add or update promotion shield with discount percentage
  311. let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]');
  312. if (!promotionShields) {
  313. // Create promotion shields element
  314. console.log('Creating promotion shields element');
  315. promotionShields = document.createElement('div');
  316. promotionShields.className = 'promotion-shields_root__cVEfN';
  317. promotionShields.setAttribute('data-testhook', 'promotion-shields');
  318. promotionShields.style = promotionCard.querySelector('.promotion-card-content_root__A5Fda')?.style || '';
  319.  
  320. let cardContent = promotionCard.querySelector('.promotion-card-content_root__A5Fda');
  321. cardContent.insertBefore(promotionShields, cardContent.firstChild);
  322. }
  323.  
  324. // Update promotion shield text with discount percentage
  325. // Replace the entire element with discount percentage
  326. promotionShields.innerHTML = ''; // Clear existing content
  327.  
  328. let shieldP = document.createElement('p');
  329. shieldP.className = 'typography_root__Om3Wh typography_variant-paragraph__T5ZAU typography_hasMargin__4EaQi promotion-shield_root__mIDdK';
  330. shieldP.setAttribute('data-testhook', 'promotion-shield');
  331.  
  332. let promotionShieldText = document.createElement('span');
  333. promotionShieldText.className = 'promotion-text_root__1sn7K promotion-text_large__lTZOA';
  334. promotionShieldText.setAttribute('data-testhook', 'promotion-text');
  335.  
  336. // Set the discount percentage as text
  337. promotionShieldText.textContent = `${discountPercentage}%`;
  338.  
  339. // Set background and text color based on discount percentage
  340. let { backgroundColor, textColor } = getDiscountColors(discountPercentage * 1.5);
  341. shieldP.style.backgroundColor = backgroundColor;
  342. shieldP.style.color = textColor; // Ensure text is readable
  343.  
  344. shieldP.appendChild(promotionShieldText);
  345. promotionShields.appendChild(shieldP);
  346.  
  347. // If unit is available, calculate price per unit
  348. if (count) {
  349. let pricePerUnit = priceNow / count;
  350. console.log(`Price per ${unit}: ${pricePerUnit.toFixed(2)}`);
  351.  
  352. // Modify price element to include price per kg in small letters next to discounted price and old price
  353. let priceContainer = priceElement; // Assuming this is the correct container
  354. if (priceContainer) {
  355. // Create or update price per kg element
  356. let pricePerUnitElement = priceContainer.querySelector('.price-per-unit');
  357. if (!pricePerUnitElement) {
  358. pricePerUnitElement = document.createElement('div');
  359. pricePerUnitElement.className = 'price-per-unit';
  360. pricePerUnitElement.style.fontSize = 'smaller';
  361. pricePerUnitElement.style.marginTop = '4px';
  362. priceContainer.appendChild(pricePerUnitElement);
  363. }
  364. pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`;
  365. }
  366. }
  367.  
  368. // Mark this promotion as processed
  369. processedProducts.add(promotionId);
  370. console.log(`Promotion ${promotionId} processed`);
  371. });
  372.  
  373. console.log('Finished updateProductInfo');
  374. }
  375.  
  376. function parsePromotionText(shieldText) {
  377. let discountPercentage = null;
  378. if (shieldText.includes('%')) {
  379. // e.g., "25% korting"
  380. let discountMatch = shieldText.match(/(\d+)%\s*korting/i);
  381. if (discountMatch) {
  382. discountPercentage = parseFloat(discountMatch[1]);
  383. }
  384. } else if (shieldText.toLowerCase().includes('gratis')) {
  385. if (shieldText.includes('1+1') || shieldText.includes('1 + 1') || shieldText.includes('1+1 gratis')) {
  386. discountPercentage = 50;
  387. } else if (shieldText.includes('2+1') || shieldText.includes('2 + 1')) {
  388. discountPercentage = 33.33;
  389. }
  390. // Add more cases as necessary
  391. } else if (shieldText.includes('2e halve prijs')) {
  392. discountPercentage = 25; // Assuming 25% off on 2 items
  393. }
  394. return discountPercentage;
  395. }
  396.  
  397. function getDiscountColors(discountPercentage) {
  398. // Use predefined colors and text colors based on discount percentage ranges
  399. let backgroundColor, textColor;
  400.  
  401. if (discountPercentage >= 80) {
  402. backgroundColor = '#008000'; // Dark Green
  403. textColor = '#FFFFFF'; // White
  404. } else if (discountPercentage >= 60) {
  405. backgroundColor = '#32CD32'; // Lime Green
  406. textColor = '#000000'; // Black
  407. } else if (discountPercentage >= 40) {
  408. backgroundColor = '#FFFF00'; // Yellow
  409. textColor = '#000000'; // Black
  410. } else if (discountPercentage >= 20) {
  411. backgroundColor = '#FFA500'; // Orange
  412. textColor = '#000000'; // Black
  413. } else {
  414. backgroundColor = '#FF0000'; // Red
  415. textColor = '#FFFFFF'; // White
  416. }
  417.  
  418. return { backgroundColor, textColor };
  419. }
  420.  
  421. // Run immediately
  422. window.setTimeout(updateProductInfo, 1000);
  423. // Run the update function every 5 seconds
  424. if(!DEBUG)setInterval(updateProductInfo, 5000);
  425. })();