Albert Heijn Kortingspercentage en prijs per kilogram

Add price per gram and discount percentage to products

当前为 2024-10-31 提交的版本,查看 最新版本

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