מעקב מלאי ומחירים עם התראות WhatsApp. תומך ב-Zara, Bershka, H&M, Nike, Amazon, eBay ועוד. כולל סינון, מיון והוספה אוטומטית לעגלה.
Size
151.2 KB
Version
4.1.37
Created
Nov 9, 2025
Updated
24 days ago
1// ==UserScript==
2// @name Stock Watch Pro + WhatsApp Alerts
3// @description מעקב מלאי ומחירים עם התראות WhatsApp. תומך ב-Zara, Bershka, H&M, Nike, Amazon, eBay ועוד. כולל סינון, מיון והוספה אוטומטית לעגלה.
4// @version 4.1.37
5// @match https://web.whatsapp.com/*
6// @match https://*/*
7// @match http://*/*
8// @icon https://www.svgrepo.com/show/331488/price-tag.svg
9// @grant GM_xmlhttpRequest
10// @grant GM.xmlHttpRequest
11// @grant GM.getValue
12// @grant GM.setValue
13// @grant GM.openInTab
14// @grant GM.setClipboard
15// @namespace io.robomonkey.stockwatch.plus
16// @connect *
17// ==/UserScript==
18(function() {
19 'use strict';
20
21 // Prevent multiple initializations
22 if (window.__SW_PLUS_BOOTED__) {
23 console.log('[Stock Tracker] Already initialized');
24 return;
25 }
26 window.__SW_PLUS_BOOTED__ = true;
27
28 const STORAGE_KEY = 'sw_plus_items_v4';
29 const PREFS_KEY = 'sw_plus_prefs_v4';
30
31 // Blocked domains - sites where the extension should NOT run
32 const BLOCKED_DOMAINS = [
33 'google.com', 'gmail.com', 'youtube.com', 'facebook.com', 'twitter.com', 'x.com',
34 'instagram.com', 'linkedin.com', 'reddit.com', 'wikipedia.org', 'github.com',
35 'stackoverflow.com', 'openai.com', 'chatgpt.com', 'chat.openai.com', 'claude.ai',
36 'anthropic.com', 'bing.com', 'yahoo.com', 'docs.google.com', 'drive.google.com',
37 'dropbox.com', 'notion.so', 'slack.com', 'discord.com', 'telegram.org',
38 'messenger.com', 'zoom.us', 'meet.google.com', 'teams.microsoft.com',
39 'office.com', 'microsoft.com', 'apple.com', 'icloud.com', 'netflix.com',
40 'spotify.com', 'twitch.tv', 'tiktok.com', 'pinterest.com', 'tumblr.com',
41 'medium.com', 'wordpress.com', 'blogger.com', 'vimeo.com', 'dailymotion.com',
42 'soundcloud.com', 'news.ycombinator.com', 'producthunt.com'
43 ];
44
45 // Check if current site is blocked
46 function isBlockedSite() {
47 const host = location.host.toLowerCase();
48 return BLOCKED_DOMAINS.some(domain => host.includes(domain));
49 }
50
51 // Check if site looks like an e-commerce site
52 function isEcommerceSite() {
53 // Check for common e-commerce indicators in the DOM
54 const indicators = [
55 '[itemprop="price"]', '[class*="price"]', '[data-price]', '[id*="price"]',
56 '[itemprop="product"]', '[class*="product"]', '[data-product]',
57 '[class*="cart"]', '[id*="cart"]', '[class*="basket"]', '[class*="bag"]',
58 'button[class*="add-to-cart"]', 'button[class*="buy"]', 'button[id*="add-to-cart"]',
59 'button[class*="add-to-bag"]', 'button[id*="buy"]',
60 '[class*="size"]', 'select[name*="size"]', '[data-size]',
61 'meta[property="og:type"][content*="product"]', 'meta[name="product"]',
62 '[class*="checkout"]', '[id*="checkout"]',
63 '[class*="wishlist"]', '[class*="favorite"]',
64 'button[class*="purchase"]', 'button[id*="purchase"]',
65 '[data-testid*="product"]', '[data-testid*="price"]', '[data-testid*="cart"]'
66 ];
67
68 // Check if any indicator exists
69 for (const selector of indicators) {
70 if (document.querySelector(selector)) {
71 console.log('[Stock Tracker] E-commerce indicator found:', selector);
72 return true;
73 }
74 }
75
76 // Check URL patterns
77 const url = location.href.toLowerCase();
78 const ecommercePatterns = [
79 '/product/', '/item/', '/p/', '/dp/', '/pd/', '/products/', '/items/',
80 'product-', 'item-', '/shop/', '/store/', '/buy/', '/cart/',
81 '/checkout/', 'product_id', 'item_id', 'sku=', '/catalog/',
82 '/merchandise/', '/goods/', '/article/'
83 ];
84
85 for (const pattern of ecommercePatterns) {
86 if (url.includes(pattern)) {
87 console.log('[Stock Tracker] E-commerce URL pattern found:', pattern);
88 return true;
89 }
90 }
91
92 // Check for common Israeli e-commerce domains
93 const host = location.host.toLowerCase();
94 const israeliEcommerce = [
95 'ksp.co.il', 'ivory.co.il', 'zap.co.il', 'bug.co.il', 'terminal-x.com',
96 'factory54.co.il', 'golf.co.il', 'castro.com', 'fox.co.il', 'hoodies.co.il',
97 'twentyfourseven.co.il', 'americaneagle.co.il', 'adidas.co.il', 'nike.co.il',
98 'renuar.co.il', 'mashlanu.co.il', 'shoez.co.il', 'delta.co.il', 'naama-bezalel.com',
99 'shilav.co.il', 'urbanica.co.il', 'mango.co.il', 'hmisrael.co.il'
100 ];
101
102 for (const domain of israeliEcommerce) {
103 if (host.includes(domain)) {
104 console.log('[Stock Tracker] Israeli e-commerce site detected:', domain);
105 return true;
106 }
107 }
108
109 // Check for common global e-commerce domains
110 const globalEcommerce = [
111 'amazon.', 'ebay.', 'aliexpress.', 'etsy.', 'walmart.', 'target.com',
112 'bestbuy.', 'newegg.', 'wayfair.', 'overstock.', 'zappos.',
113 'asos.', 'shein.', 'boohoo.', 'prettylittlething.', 'fashionnova.',
114 'zara.', 'hm.', 'uniqlo.', 'gap.', 'oldnavy.', 'bananarepublic.',
115 'nike.', 'adidas.', 'puma.', 'reebok.', 'underarmour.',
116 'macys.', 'nordstrom.', 'bloomingdales.', 'saksfifthavenue.',
117 'shopify.', 'bigcartel.', 'wix.com/stores', 'squarespace.com/commerce'
118 ];
119
120 for (const domain of globalEcommerce) {
121 if (host.includes(domain)) {
122 console.log('[Stock Tracker] Global e-commerce site detected:', domain);
123 return true;
124 }
125 }
126
127 console.log('[Stock Tracker] Not detected as e-commerce site');
128 return false;
129 }
130
131 // Storage helpers
132 const load = async (key, defaultValue) => {
133 try {
134 const val = await GM.getValue(key);
135 return val !== undefined ? JSON.parse(val) : defaultValue;
136 } catch(err) {
137 console.error('[Stock Tracker] Load error:', err);
138 return defaultValue;
139 }
140 };
141
142 const save = async (key, value) => {
143 try {
144 await GM.setValue(key, JSON.stringify(value));
145 } catch(err) {
146 console.error('[Stock Tracker] Save error:', err);
147 }
148 };
149
150 const getItems = () => load(STORAGE_KEY, []);
151 const saveItems = (items) => save(STORAGE_KEY, items);
152 const getPrefs = () => load(PREFS_KEY, { waPhone: '', checkIntervalMin: 5, throttleMinutesWA: 3, notifications: true, soundAlerts: true, autoAddToCart: true });
153 const savePrefs = (prefs) => save(PREFS_KEY, prefs);
154
155 // Countdown timer state
156 let nextCheckTime = null;
157 let countdownInterval = null;
158
159 // Time formatting helpers
160 function formatTimeAgo(timestamp) {
161 const now = Date.now();
162 const diff = now - timestamp;
163 const minutes = Math.floor(diff / 60000);
164 const hours = Math.floor(diff / 3600000);
165 const days = Math.floor(diff / 86400000);
166
167 if (minutes < 1) return 'עכשיו';
168 if (minutes < 60) return `לפני ${minutes} דקות`;
169 if (hours < 24) return `לפני ${hours} שעות`;
170 return `לפני ${days} ימים`;
171 }
172
173 // Parse price from text
174 function parsePrice(val) {
175 if (val == null) return null;
176 if (typeof val === 'number') return val;
177 let str = String(val);
178 str = str.replace(/[^\d.,\-]/g, '').trim();
179
180 const hasDot = str.includes('.');
181 const hasComma = str.includes(',');
182 if (hasDot && hasComma) {
183 if (str.lastIndexOf(',') < str.lastIndexOf('.')) {
184 str = str.replace(/,/g, '');
185 } else {
186 str = str.replace(/\./g, '').replace(',', '.');
187 }
188 } else {
189 if (hasComma && !hasDot) str = str.replace(',', '.');
190 }
191
192 const num = parseFloat(str);
193 return isNaN(num) ? null : num;
194 }
195
196 // Normalize size label for comparison
197 function normalizeSizeLabel(s) {
198 return String(s || '')
199 .toLowerCase()
200 .replace(/size|מידה|talla|größe|taglia|taille|尺码/gi, '')
201 .replace(/eu|uk|us|it|fr|de|men|women|unisex/gi, '')
202 .replace(/[()]/g, '')
203 .replace(/\s*\/\s*/g, ' ')
204 .replace(/\s+/g, ' ')
205 .trim();
206 }
207
208 // Generate size label variants for flexible matching
209 function labelVariants(s) {
210 const n = normalizeSizeLabel(s);
211 const toks = n.split(' ').filter(Boolean);
212
213 const variants = new Set([n]);
214 if (toks.length >= 2) {
215 variants.add(toks.find(t => /^[xsml]{1,3}$/.test(t)) || n);
216 const num = toks.find(t => /^\d{2,3}$/.test(t));
217 if (num) variants.add(num);
218 }
219 variants.add(n.replace(/\s+/g, ''));
220
221 return Array.from(variants).filter(Boolean);
222 }
223
224 // Check if two size labels match (flexible comparison)
225 function sizeMatches(jsonSize, savedLabel) {
226 const A = labelVariants(jsonSize);
227 const B = labelVariants(savedLabel);
228 return A.some(a => B.some(b => a === b || a.includes(b) || b.includes(a)));
229 }
230
231 // Extract site name from URL
232 function getSiteName(url) {
233 try {
234 const hostname = new URL(url).hostname;
235 const domain = hostname.replace(/^www\d?\./, '');
236
237 const siteNames = {
238 'zara.com': 'Zara', 'bershka.com': 'Bershka', 'hm.com': 'H&M', 'nike.com': 'Nike',
239 'amazon.com': 'Amazon', 'amazon.co.uk': 'Amazon UK', 'amazon.de': 'Amazon DE',
240 'ebay.com': 'eBay', 'aliexpress.com': 'AliExpress', 'asos.com': 'ASOS',
241 'shein.com': 'SHEIN', 'mango.com': 'Mango', 'pullandbear.com': 'Pull&Bear',
242 'stradivarius.com': 'Stradivarius', 'massimodutti.com': 'Massimo Dutti',
243 'gap.com': 'Gap', 'uniqlo.com': 'Uniqlo', 'adidas.com': 'Adidas', 'puma.com': 'Puma'
244 };
245
246 for (const [key, name] of Object.entries(siteNames)) {
247 if (domain.includes(key)) return name;
248 }
249
250 const mainDomain = domain.split('.')[0];
251 return mainDomain.charAt(0).toUpperCase() + mainDomain.slice(1);
252 } catch {
253 return 'Unknown';
254 }
255 }
256
257 // Play notification sound
258 function playNotificationSound() {
259 try {
260 const audioContext = new (window.AudioContext || window.webkitAudioContext)();
261 const oscillator = audioContext.createOscillator();
262 const gainNode = audioContext.createGain();
263
264 oscillator.connect(gainNode);
265 gainNode.connect(audioContext.destination);
266
267 oscillator.frequency.value = 800;
268 oscillator.type = 'sine';
269
270 gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
271 gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
272
273 oscillator.start(audioContext.currentTime);
274 oscillator.stop(audioContext.currentTime + 0.5);
275 } catch (err) {
276 console.error('[Stock Tracker] Sound error:', err);
277 }
278 }
279
280 // Update FAB badge
281 async function updateFabBadge() {
282 const items = await getItems();
283 const fab = document.querySelector('.st-fab');
284 if (!fab) return;
285
286 const outOfStockCount = items.reduce((sum, item) => {
287 if (item.mode === 'stock') return sum + item.sizes.filter(s => !s.inStock).length;
288 return sum;
289 }, 0);
290
291 const oldBadge = fab.querySelector('.st-badge');
292 if (oldBadge) oldBadge.remove();
293
294 if (outOfStockCount > 0) {
295 const badge = document.createElement('span');
296 badge.className = 'st-badge';
297 badge.textContent = outOfStockCount;
298 fab.appendChild(badge);
299 fab.classList.add('st-fab-alert');
300 } else {
301 fab.classList.remove('st-fab-alert');
302 }
303 }
304
305 // Universal site adapters
306 const adapters = {
307 'zara.com': {
308 mode: 'stock',
309 title: () => {
310 const titleEl = document.querySelector('h1.product-detail-info__header-name, .product-detail-info__header-name, h1[class*="product"]');
311 return titleEl?.textContent?.trim() || document.querySelector('h1')?.textContent?.trim();
312 },
313 price: () => {
314 const priceEl = document.querySelector('[data-qa*="price"], .money-amount, .price__amount, .current-price-elem');
315 return parsePrice(priceEl?.textContent);
316 },
317 image: () => {
318 return document.querySelector('.product-detail-images img, picture img, .media-image__image, img[class*="product"]')?.src ||
319 document.querySelector('main img')?.src;
320 },
321 getSizes: () => {
322 const addBtn = document.querySelector('button[data-qa-action="add-to-cart"]');
323 if (addBtn) addBtn.click();
324
325 return new Promise(resolve => {
326 setTimeout(() => {
327 const sizeElements = document.querySelectorAll('li.size-selector-sizes__size, .product-size-info__main-label');
328 const sizes = Array.from(sizeElements).map(li => {
329 const labelEl = li.querySelector('.size-selector-sizes-size__label') || li;
330 const label = labelEl?.textContent?.trim().replace(/coming\s?soon/gi, '').trim();
331
332 // Check if explicitly disabled/unavailable
333 const isExplicitlyDisabled = li.classList.contains('size-selector-sizes-size--disabled') ||
334 li.classList.contains('size-selector-sizes-size--unavailable') ||
335 li.textContent?.toLowerCase().includes('coming soon');
336
337 // If has --enabled class and NOT disabled, it's in stock
338 const isInStock = li.classList.contains('size-selector-sizes-size--enabled') && !isExplicitlyDisabled;
339
340 console.log(`[Stock Tracker] Zara size ${label}: enabled=${li.classList.contains('size-selector-sizes-size--enabled')}, disabled=${isExplicitlyDisabled}, inStock=${isInStock}`);
341
342 return { label, element: li, inStock: isInStock };
343 }).filter(s => s.label && s.label.length <= 12);
344
345 console.log('[Stock Tracker] Zara found sizes:', sizes.length, sizes.map(s => `${s.label}: ${s.inStock ? 'IN' : 'OUT'}`));
346 resolve(sizes);
347 }, 1500);
348 });
349 }
350 },
351
352 'bershka.com': {
353 mode: 'stock',
354 title: () => {
355 const titleEl = document.querySelector('h1.product-detail-info-layout__title, .product-detail-info-layout__title');
356 return titleEl?.textContent?.trim();
357 },
358 price: () => {
359 const priceEl = document.querySelector('[data-qa-anchor="productItemPrice"], .current-price-elem, .price-elem');
360 return parsePrice(priceEl?.textContent);
361 },
362 image: () => {
363 return document.querySelector('img[class*="product-detail"], img[class*="media-image"], picture img, main img')?.src;
364 },
365 getSizes: () => {
366 return new Promise(resolve => {
367 setTimeout(() => {
368 const sizeElements = document.querySelectorAll('button[data-qa-anchor="sizeListItem"], .ui--dot-item, button[aria-label*="מידה"]');
369 const sizes = Array.from(sizeElements).map(btn => {
370 const labelEl = btn.querySelector('.text__label') || btn;
371 const label = labelEl?.textContent?.trim() || btn.getAttribute('aria-label')?.replace('מידה ', '').trim();
372 const isOutOfStock = btn.disabled ||
373 btn.classList.contains('is-disabled') ||
374 btn.classList.contains('is-unavailable') ||
375 btn.getAttribute('aria-disabled') === 'true';
376
377 return { label, element: btn, inStock: !isOutOfStock };
378 }).filter(s => s.label && s.label.length <= 12);
379
380 console.log('[Stock Tracker] Bershka found sizes:', sizes.length);
381 resolve(sizes);
382 }, 1000);
383 });
384 }
385 },
386
387 'hm.com': {
388 mode: 'stock',
389 title: () => {
390 const titleEl = document.querySelector('h1.ProductName-module--productTitle, .ProductName-module--productTitle, h1[class*="product"]');
391 return titleEl?.textContent?.trim() || document.querySelector('h1')?.textContent?.trim();
392 },
393 price: () => {
394 const priceEl = document.querySelector('.ProductPrice-module--currentPrice, [data-testid="price"], .price');
395 return parsePrice(priceEl?.textContent);
396 },
397 image: () => {
398 return document.querySelector('.ProductImages-module--image img, figure img, img[class*="product"]')?.src ||
399 document.querySelector('main img')?.src;
400 },
401 getSizes: () => {
402 return new Promise(resolve => {
403 setTimeout(() => {
404 const sizeElements = document.querySelectorAll('div[data-testid^="sizeButton-"], div[role="radio"][id^="sizeButton"]');
405 const sizes = Array.from(sizeElements).map(div => {
406 const label = div.querySelector('div')?.textContent?.trim().replace(/\s+/g, ' ').trim();
407
408 const ariaLabel = div.getAttribute('aria-label') || '';
409 const isOutOfStock = ariaLabel.toLowerCase().includes('není skladem') ||
410 ariaLabel.toLowerCase().includes('out of stock') ||
411 ariaLabel.toLowerCase().includes('not available') ||
412 div.classList.contains('cf813c') ||
413 div.querySelector('.da0c1b');
414
415 return { label, element: div, inStock: !isOutOfStock };
416 }).filter(s => s.label && s.label.length <= 12);
417
418 console.log('[Stock Tracker] H&M found sizes:', sizes.length);
419 resolve(sizes);
420 }, 1000);
421 });
422 }
423 },
424
425 'nike.com': {
426 mode: 'stock',
427 title: () => {
428 const titleEl = document.querySelector('h1#pdp_product_title, #pdp_product_title, h1[class*="product"]');
429 return titleEl?.textContent?.trim() || document.querySelector('h1')?.textContent?.trim();
430 },
431 price: () => {
432 const priceEl = document.querySelector('[data-test="product-price"], .product-price, [class*="price"]');
433 return parsePrice(priceEl?.textContent);
434 },
435 image: () => {
436 return document.querySelector('.product-image img, picture img, img[class*="product"]')?.src ||
437 document.querySelector('main img')?.src;
438 },
439 getSizes: () => {
440 return new Promise(resolve => {
441 setTimeout(() => {
442 const sizeElements = document.querySelectorAll('[data-qa="size-available"], .size-grid-button, button[class*="size"]');
443 const sizes = Array.from(sizeElements).map(btn => {
444 const label = btn.textContent?.trim();
445 const isOutOfStock = btn.disabled || btn.classList.contains('unavailable');
446 return { label, element: btn, inStock: !isOutOfStock };
447 }).filter(s => s.label && s.label.length <= 12);
448
449 console.log('[Stock Tracker] Nike found sizes:', sizes.length);
450 resolve(sizes);
451 }, 1000);
452 });
453 }
454 },
455
456 'amazon.': {
457 mode: 'stock',
458 title: () => {
459 const titleEl = document.querySelector('#productTitle, h1.product-title, h1[id*="product"]');
460 return titleEl?.textContent?.trim() || document.querySelector('h1')?.textContent?.trim();
461 },
462 price: () => {
463 const priceEl = document.querySelector('.a-price .a-offscreen, #priceblock_ourprice, .a-price-whole, [class*="price"]');
464 return parsePrice(priceEl?.textContent);
465 },
466 image: () => {
467 return document.querySelector('#landingImage, #imgBlkFront, img[class*="product"]')?.src ||
468 document.querySelector('main img')?.src;
469 },
470 getSizes: () => {
471 return new Promise(resolve => {
472 setTimeout(() => {
473 // Check if product is out of stock or unavailable
474 const outOfStock = document.querySelector('#outOfStock, #availability .a-color-price, #availability .a-color-state');
475 if (outOfStock) {
476 const text = outOfStock.textContent?.toLowerCase() || '';
477 if (text.includes('out of stock') ||
478 text.includes('unavailable') ||
479 text.includes('cannot be dispatched') ||
480 text.includes('not available')) {
481 console.log('[Stock Tracker] Amazon - Product unavailable:', text.substring(0, 100));
482 resolve([{ label: 'OS', element: document.body, inStock: false }]);
483 return;
484 }
485 }
486
487 // First check for size dropdown
488 const sizeSelect = document.querySelector('#native_dropdown_selected_size_name, select[name*="size"], select[id*="size"]');
489 if (sizeSelect) {
490 const sizeElements = sizeSelect.querySelectorAll('option');
491 const sizes = Array.from(sizeElements).map(opt => {
492 const label = opt.textContent?.trim();
493 const isOutOfStock = opt.disabled || label.toLowerCase().includes('out of stock');
494 return { label, element: opt, inStock: !isOutOfStock };
495 }).filter(s => s.label && !s.label.toLowerCase().includes('select') && s.label.length <= 30);
496
497 if (sizes.length > 0) {
498 console.log('[Stock Tracker] Amazon found sizes:', sizes.length);
499 resolve(sizes);
500 return;
501 }
502 }
503
504 // Check for variation dropdowns (color, style, etc)
505 const variationSelects = document.querySelectorAll('select[name*="dropdown"], select[id*="native"]');
506 for (const select of variationSelects) {
507 const options = Array.from(select.querySelectorAll('option')).slice(1); // Skip first "Select" option
508 if (options.length > 0 && options.length < 50) {
509 const sizes = options.map(opt => {
510 const label = opt.textContent?.trim();
511 const isOutOfStock = opt.disabled || label.toLowerCase().includes('out of stock');
512 return { label, element: opt, inStock: !isOutOfStock };
513 }).filter(s => s.label && !s.label.toLowerCase().includes('select') && s.label.length <= 50);
514
515 if (sizes.length > 0) {
516 console.log('[Stock Tracker] Amazon found variation options:', sizes.length);
517 resolve(sizes);
518 return;
519 }
520 }
521 }
522
523 // No variations found - check if product is available
524 const addToCartBtn = document.querySelector('#add-to-cart-button, input[id="add-to-cart-button"], button[id*="add-to-cart"]');
525 const isAvailable = addToCartBtn && !addToCartBtn.disabled;
526
527 console.log('[Stock Tracker] Amazon - No variations, single product. Available:', isAvailable);
528 resolve([{ label: 'OS', element: document.body, inStock: isAvailable }]);
529 }, 1000);
530 });
531 }
532 },
533
534 'ebay.': {
535 mode: 'stock',
536 title: () => {
537 const titleEl = document.querySelector('h1.x-item-title__mainTitle, h1[class*="title"]');
538 return titleEl?.textContent?.trim();
539 },
540 price: () => {
541 const priceEl = document.querySelector('.x-price-primary, .x-price-approx, [itemprop="price"]');
542 return parsePrice(priceEl?.textContent);
543 },
544 image: () => {
545 return document.querySelector('.ux-image-carousel-item img, [id*="icImg"]')?.src;
546 },
547 getSizes: () => {
548 return new Promise(resolve => {
549 setTimeout(() => {
550 const select = document.querySelector('select[aria-label*="Select"], select[class*="msku"]');
551 if (select) {
552 const options = [...select.querySelectorAll('option')].slice(1);
553 const sizes = options.map(opt => {
554 const label = opt.textContent?.trim();
555 const disabled = opt.disabled;
556 return label && label.length <= 30 ? { label, element: opt, inStock: !disabled } : null;
557 }).filter(Boolean);
558 resolve(sizes);
559 } else {
560 const disabled = !!document.querySelector('.ux-action .btn[disabled], .vi-acc-del-range');
561 resolve([{ label: 'OS', element: document.body, inStock: !disabled }]);
562 }
563 }, 1000);
564 });
565 }
566 },
567
568 'ksp.co.il': {
569 mode: 'stock',
570 title: () => document.querySelector('h1, .product-title')?.textContent?.trim() || document.title,
571 price: () => parsePrice(document.querySelector('[itemprop="price"], .product-price, .price')?.textContent),
572 image: () => document.querySelector('img[itemprop="image"], .product-image img, picture img')?.src,
573 getSizes: () => {
574 const disabled = !!document.querySelector('button[disabled][id*="add"], button[disabled][class*="add"], .sold-out');
575 return Promise.resolve([{ label: 'OS', element: document.body, inStock: !disabled }]);
576 }
577 },
578
579 'ivory.co.il': {
580 mode: 'stock',
581 title: () => document.querySelector('h1, .product-name')?.textContent?.trim() || document.title,
582 price: () => parsePrice(document.querySelector('[itemprop="price"], .price, .product-price')?.textContent),
583 image: () => document.querySelector('img[itemprop="image"], .product-image img')?.src,
584 getSizes: () => {
585 const disabled = !!document.querySelector('button[disabled][class*="add"], .out-of-stock');
586 return Promise.resolve([{ label: 'OS', element: document.body, inStock: !disabled }]);
587 }
588 },
589
590 'gap.': {
591 mode: 'stock',
592 title: () => {
593 const titleEl = document.querySelector('h1, [data-testid*="product"] h1, .product-title');
594 return titleEl?.textContent?.trim() || document.title;
595 },
596 price: () => {
597 const priceEl = document.querySelector('span[data-testid="sf7"], [data-testid*="price"] span, .price, [class*="price"]');
598 return parsePrice(priceEl?.textContent);
599 },
600 image: () => {
601 return document.querySelector('img[alt*="product"], picture img, main img')?.src;
602 },
603 getSizes: () => {
604 return new Promise(resolve => {
605 setTimeout(() => {
606 // GAP uses links with role="radio" for sizes
607 const sizeLinks = document.querySelectorAll('a[role="radio"][href*="selectedVariant"], a[role="radio"][href*="size"]');
608
609 if (sizeLinks.length > 0) {
610 const sizes = Array.from(sizeLinks).map(link => {
611 const label = link.querySelector('span')?.textContent?.trim() || link.textContent?.trim();
612 const isOutOfStock = link.classList.contains('disabled') ||
613 link.classList.contains('unavailable') ||
614 link.getAttribute('aria-disabled') === 'true';
615
616 return { label, element: link, inStock: !isOutOfStock };
617 }).filter(s => s.label && s.label.length <= 12);
618
619 console.log('[Stock Tracker] GAP found sizes:', sizes.length);
620 resolve(sizes);
621 } else {
622 // Fallback to buttons
623 const sizeButtons = document.querySelectorAll('button[class*="size"], button[data-testid*="size"]');
624 const sizes = Array.from(sizeButtons).map(btn => {
625 const label = btn.textContent?.trim();
626 const isOutOfStock = btn.disabled || btn.classList.contains('unavailable');
627 return { label, element: btn, inStock: !isOutOfStock };
628 }).filter(s => s.label && s.label.length <= 12);
629
630 console.log('[Stock Tracker] GAP found sizes (fallback):', sizes.length);
631 resolve(sizes);
632 }
633 }, 1000);
634 });
635 }
636 },
637
638 'skyscanner.': {
639 mode: 'price',
640 title: () => document.querySelector('[data-test-id="search-summary"]')?.textContent?.trim() || document.title,
641 price: () => parsePrice(document.querySelector('[data-test-id*="price"], [class*="price"]')?.textContent),
642 image: () => null
643 },
644
645 'generic': {
646 mode: 'stock',
647 title: () => {
648 const selectors = [
649 'h1[class*="product"][class*="title"]', 'h1[class*="product"][class*="name"]',
650 'h1.product-detail-info-layout__title', '.product-detail-info-layout__title',
651 'h1[class*="product"]', '[itemprop="name"]', '.product-title', '.product-name',
652 '[data-testid*="product"][data-testid*="title"]', 'h1'
653 ];
654
655 for (const selector of selectors) {
656 const el = document.querySelector(selector);
657 const text = el?.textContent?.trim();
658 if (text && text.length > 0 && text.length < 200) return text;
659 }
660
661 return document.title;
662 },
663 price: () => {
664 const priceSelectors = [
665 '[data-qa-anchor="productItemPrice"]', '.current-price-elem', '[itemprop="price"]',
666 '[class*="price"]:not([class*="strike"]):not([class*="old"]):not([class*="original"])',
667 '[data-price]', '[data-testid*="price"]', '[id*="price"]',
668 'span[class*="current"]', 'div[class*="current"]', '.product-price', '.price'
669 ];
670
671 for (const selector of priceSelectors) {
672 const elements = document.querySelectorAll(selector);
673 for (const el of elements) {
674 const price = parsePrice(el.textContent || el.getAttribute('content') || el.getAttribute('data-price'));
675 if (price && price > 0) return price;
676 }
677 }
678 return null;
679 },
680 image: () => {
681 const imageSelectors = [
682 'img[class*="product-detail"]', 'img[class*="media-image"]', '[itemprop="image"]',
683 '.product-image img', '.product-img img', 'img[class*="product"]',
684 '[data-testid*="product"] img', 'picture img', 'main img', 'article img'
685 ];
686
687 for (const selector of imageSelectors) {
688 const img = document.querySelector(selector);
689 if (img?.src && !img.src.includes('data:image')) return img.src;
690 }
691 return null;
692 },
693 getSizes: () => {
694 return new Promise(resolve => {
695 setTimeout(() => {
696 // First try to find size links (like GAP)
697 const sizeLinks = document.querySelectorAll(
698 'a[role="radio"][href*="size"], a[role="radio"][href*="variant"], ' +
699 'a[role="radio"][href*="Size"], a[role="radio"][href*="Variant"]'
700 );
701
702 if (sizeLinks.length > 0) {
703 const sizes = Array.from(sizeLinks).map(link => {
704 const label = link.querySelector('span')?.textContent?.trim() || link.textContent?.trim();
705 const isOutOfStock = link.classList.contains('disabled') ||
706 link.classList.contains('unavailable') ||
707 link.getAttribute('aria-disabled') === 'true';
708
709 return { label, element: link, inStock: !isOutOfStock };
710 }).filter(s => s.label && s.label.length > 0 && s.label.length <= 20);
711
712 if (sizes.length > 0) {
713 console.log('[Stock Tracker] Found size links:', sizes.length);
714 resolve(sizes);
715 return;
716 }
717 }
718
719 // Then try buttons and other elements
720 const sizeElements = document.querySelectorAll(
721 'button[data-qa-anchor="sizeListItem"], ' +
722 'button[class*="size"]:not([class*="guide"]):not([class*="help"]), ' +
723 'button[aria-label*="מידה"], button[aria-label*="size"], ' +
724 '.ui--dot-item, .size-option, [data-size], ' +
725 '[data-testid*="size"]:not([data-testid*="guide"]), ' +
726 'select[name*="size"] option, select[id*="size"] option, ' +
727 'li[class*="size"]:not([class*="guide"]), ' +
728 'div[class*="size"][role="button"], span[class*="size"][role="button"]'
729 );
730
731 const sizes = Array.from(sizeElements).map(el => {
732 let label = el.textContent?.trim() || el.getAttribute('data-size') || el.value;
733
734 if (!label || label.length > 20) {
735 const ariaLabel = el.getAttribute('aria-label');
736 if (ariaLabel) {
737 label = ariaLabel.replace(/מידה\s*/gi, '').replace(/size\s*/gi, '').trim();
738 }
739 }
740
741 if (label) label = label.replace(/\s+/g, ' ').trim();
742
743 const isOutOfStock = el.disabled ||
744 el.classList.contains('disabled') ||
745 el.classList.contains('unavailable') ||
746 el.classList.contains('out-of-stock') ||
747 el.classList.contains('sold-out') ||
748 el.classList.contains('is-disabled') ||
749 el.classList.contains('is-unavailable') ||
750 el.getAttribute('aria-disabled') === 'true' ||
751 el.textContent?.toLowerCase().includes('out of stock') ||
752 el.textContent?.toLowerCase().includes('sold out') ||
753 el.textContent?.toLowerCase().includes('coming soon') ||
754 el.textContent?.toLowerCase().includes('unavailable');
755 return { label, element: el, inStock: !isOutOfStock };
756 }).filter(s => s.label && s.label.length > 0 && s.label.length <= 20 &&
757 !s.label.toLowerCase().includes('select') &&
758 !s.label.toLowerCase().includes('choose') &&
759 !s.label.toLowerCase().includes('guide'));
760
761 if (sizes.length === 0) {
762 const disabled = !!document.querySelector('.out-of-stock, .sold-out, button[disabled]');
763 resolve([{ label: 'OS', element: document.body, inStock: !disabled }]);
764 } else {
765 resolve(sizes);
766 }
767 }, 1000);
768 });
769 }
770 }
771 };
772
773 const getAdapter = () => {
774 const host = location.host.replace('www.', '').replace('www2.', '');
775
776 for (const [domain, adapter] of Object.entries(adapters)) {
777 if (domain !== 'generic' && host.includes(domain)) {
778 return adapter;
779 }
780 }
781
782 return adapters.generic;
783 };
784
785 // CSS selector builder
786 function buildSelector(node) {
787 if (!node) return '';
788 if (node.id) return `#${CSS.escape(node.id)}`;
789
790 const parts = [];
791 let el = node;
792 let depth = 0;
793
794 while (el && el.nodeType === 1 && depth < 5) {
795 let seg = el.tagName.toLowerCase();
796
797 if (el.classList.length) {
798 const stableClasses = Array.from(el.classList).filter(c =>
799 !c.includes('disabled') &&
800 !c.includes('unavailable') &&
801 !c.includes('enabled') &&
802 !c.includes('selected') &&
803 !c.includes('active') &&
804 !c.includes('is-') &&
805 !c.includes('--')
806 );
807 if (stableClasses.length > 0) {
808 seg += '.' + stableClasses.slice(0, 2).map(c => CSS.escape(c)).join('.');
809 }
810 }
811
812 const siblings = Array.from(el.parentElement?.children || []).filter(x => x.tagName === el.tagName);
813 if (siblings.length > 1) {
814 const idx = siblings.indexOf(el) + 1;
815 seg += `:nth-of-type(${idx})`;
816 }
817
818 parts.unshift(seg);
819 el = el.parentElement;
820 depth++;
821 }
822
823 return parts.join(' > ');
824 }
825
826 // Check stock from HTML
827 function checkStockFromHTML(html, selector) {
828 try {
829 const doc = new DOMParser().parseFromString(html, 'text/html');
830 const el = doc.querySelector(selector);
831 if (!el) {
832 console.log('[Stock Tracker] checkStockFromHTML: Element not found with selector:', selector);
833 return null;
834 }
835
836 // For Zara - check if element has --enabled class (means in stock)
837 if (el.classList.contains('size-selector-sizes-size')) {
838 const hasEnabled = el.classList.contains('size-selector-sizes-size--enabled');
839 const hasDisabled = el.classList.contains('size-selector-sizes-size--disabled');
840 const hasUnavailable = el.classList.contains('size-selector-sizes-size--unavailable');
841
842 console.log(`[Stock Tracker] Zara element classes: enabled=${hasEnabled}, disabled=${hasDisabled}, unavailable=${hasUnavailable}`);
843
844 // If has --enabled and NOT disabled/unavailable, it's in stock
845 if (hasEnabled && !hasDisabled && !hasUnavailable) {
846 console.log('[Stock Tracker] ✓ Zara size is IN STOCK (has --enabled, no --disabled)');
847 return true;
848 }
849
850 // If has --disabled or --unavailable, it's out of stock
851 if (hasDisabled || hasUnavailable) {
852 console.log('[Stock Tracker] ✗ Zara size is OUT OF STOCK (has --disabled or --unavailable)');
853 return false;
854 }
855 }
856
857 if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') return false;
858
859 const txt = (el.textContent || '').toLowerCase().replace(/\s+/g, ' ').trim();
860 const cls = (el.className || '').toLowerCase();
861
862 const NEG = /(not\s+available|unavailable|sold\s*out|out\s*of\s*stock|coming\s*soon|notify\s*me|לא\s*במלאי|אזל\s*המלאי|אין\s*מלאי|غير متوفر|agotado|non\s+disponibile|nicht\s+verfügbar|épuisé)/i;
863 if (NEG.test(txt) || /(out-of-stock|sold-out|unavailable|is-unavailable|oos)/i.test(cls)) return false;
864
865 const POS = /\b(in\s*stock|available\s+now|add\s*to\s*cart|buy\s*now)\b/i;
866 if (POS.test(txt) || /(available|is-available)/i.test(cls)) return true;
867
868 return null;
869 } catch (err) {
870 console.error('[Stock Tracker] checkStockFromHTML error:', err);
871 return null;
872 }
873 }
874
875 // Extract availability from JSON-LD structured data
876 function extractAvailabilityFromJSONLD(html) {
877 try {
878 const doc = new DOMParser().parseFromString(html, 'text/html');
879 const scripts = Array.from(doc.querySelectorAll('script[type="application/ld+json"]'));
880 const pairs = [];
881
882 for (const s of scripts) {
883 const txt = (s.textContent || '').trim();
884 if (!txt) continue;
885
886 const candidates = [];
887 try {
888 candidates.push(JSON.parse(txt));
889 } catch {
890 try {
891 candidates.push(JSON.parse('[' + txt.replace(/}\s*,\s*{/g, '},{') + ']'));
892 } catch {}
893 }
894
895 for (const root of candidates.flat()) {
896 const nodes = Array.isArray(root) ? root : [root];
897 for (const node of nodes) {
898 const offers = node?.offers ?? node?.Offers;
899 if (!offers) continue;
900
901 for (const off of (Array.isArray(offers) ? offers : [offers])) {
902 const rawLabel = off?.size || off?.sku || off?.name || '';
903 let avail = off?.availability ?? off?.Availability ?? '';
904 if (!rawLabel) continue;
905
906 let inStock = null;
907 if (typeof avail === 'string') {
908 const a = avail.toLowerCase();
909 if (a.includes('instock') || a.includes('limitedavailability')) inStock = true;
910 else if (a.includes('outofstock') || a.includes('soldout')) inStock = false;
911 } else if (typeof avail === 'boolean') {
912 inStock = !!avail;
913 }
914
915 if (inStock !== null) {
916 const clean = String(rawLabel).trim();
917 if (clean) pairs.push({ label: clean, inStock });
918 }
919 }
920 }
921 }
922 }
923
924 return pairs.length ? pairs : null;
925 } catch (e) {
926 console.warn('[Stock Tracker] JSON-LD parse error:', e);
927 return null;
928 }
929 }
930
931 // Extract Inditex (Zara/Bershka) offers from inline scripts
932 function extractInditexOffers(html) {
933 try {
934 const doc = new DOMParser().parseFromString(html, 'text/html');
935 const scripts = Array.from(doc.querySelectorAll('script'));
936 const pairs = [];
937
938 const AVAIL_MAP = {
939 IN_STOCK: true, LIMITED_STOCK: true,
940 OUT_OF_STOCK: false, SOLD_OUT: false, UNAVAILABLE: false
941 };
942
943 for (const s of scripts) {
944 const txt = s.textContent || '';
945 if (!txt || txt.length < 50) continue;
946
947 // Try to find JSON objects with size and availability data
948 // Pattern 1: Look for objects with "size" and "availability" or "stockStatus"
949 const jsonObjRegex = /\{[^{}]*?"(?:size|sizeName|name|label|variantSize)"\s*:\s*"([^"]+)"[^{}]*?"(?:availability|stockStatus)"\s*:\s*"([A-Z_]+)"[^{}]*?\}/g;
950 let m;
951 while ((m = jsonObjRegex.exec(txt)) !== null) {
952 const rawLabel = (m[1] || '').trim();
953 const rawAvail = (m[2] || '').toUpperCase();
954 if (!rawLabel) continue;
955 const inStock = AVAIL_MAP.hasOwnProperty(rawAvail) ? AVAIL_MAP[rawAvail] : null;
956 if (inStock !== null) {
957 console.log(`[Stock Tracker] 🔍 Inditex regex found: "${rawLabel}" = ${rawAvail} → ${inStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
958 pairs.push({ label: rawLabel, inStock });
959 }
960 }
961
962 // Pattern 2: Reverse order - availability before size
963 const reverseRegex = /\{[^{}]*?"(?:availability|stockStatus)"\s*:\s*"([A-Z_]+)"[^{}]*?"(?:size|sizeName|name|label|variantSize)"\s*:\s*"([^"]+)"[^{}]*?\}/g;
964 while ((m = reverseRegex.exec(txt)) !== null) {
965 const rawAvail = (m[1] || '').toUpperCase();
966 const rawLabel = (m[2] || '').trim();
967 if (!rawLabel) continue;
968 const inStock = AVAIL_MAP.hasOwnProperty(rawAvail) ? AVAIL_MAP[rawAvail] : null;
969 if (inStock !== null) {
970 console.log(`[Stock Tracker] 🔍 Inditex regex (reverse) found: "${rawLabel}" = ${rawAvail} → ${inStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
971 pairs.push({ label: rawLabel, inStock });
972 }
973 }
974
975 // Pattern 3: Look for skuStocks array
976 const arrMatch = txt.match(/"(?:skuStocks?|sizes|variants)"\s*:\s*(\[[\s\S]*?\])/i);
977 if (arrMatch) {
978 try {
979 const arr = JSON.parse(arrMatch[1]);
980 console.log(`[Stock Tracker] 🔍 Found array with ${arr.length} items`);
981 for (const r of arr) {
982 const rawLabel = (r.size || r.sizeName || r.name || r.label || r.variantSize || '').trim();
983 const rawAvail = String(r.availability || r.stockStatus || '').toUpperCase();
984 if (!rawLabel) continue;
985 const inStock = AVAIL_MAP.hasOwnProperty(rawAvail) ? AVAIL_MAP[rawAvail] : null;
986 if (inStock !== null) {
987 console.log(`[Stock Tracker] 🔍 Inditex array: "${rawLabel}" = ${rawAvail} → ${inStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
988 pairs.push({ label: rawLabel, inStock });
989 }
990 }
991 } catch (e) {
992 console.warn('[Stock Tracker] Failed to parse array:', e);
993 }
994 }
995
996 // Pattern 4: Look for window.__PRELOADED_STATE__ or similar global objects
997 if (txt.includes('__PRELOADED_STATE__') || txt.includes('window.zara') || txt.includes('window.bershka')) {
998 console.log('[Stock Tracker] 🔍 Found preloaded state, searching for product data...');
999
1000 // Try to extract the entire state object
1001 const stateMatch = txt.match(/(?:__PRELOADED_STATE__|window\.(?:zara|bershka))\s*=\s*(\{[\s\S]*?\});/);
1002 if (stateMatch) {
1003 try {
1004 const state = JSON.parse(stateMatch[1]);
1005 console.log('[Stock Tracker] 🔍 Parsed preloaded state');
1006
1007 // Recursively search for size/availability data
1008 function searchForSizes(obj, depth = 0) {
1009 if (depth > 10 || !obj || typeof obj !== 'object') return;
1010
1011 // Check if this object has size and availability
1012 if (obj.size && obj.availability) {
1013 const rawLabel = String(obj.size || obj.sizeName || obj.name || '').trim();
1014 const rawAvail = String(obj.availability || obj.stockStatus || '').toUpperCase();
1015 if (rawLabel) {
1016 const inStock = AVAIL_MAP.hasOwnProperty(rawAvail) ? AVAIL_MAP[rawAvail] : null;
1017 if (inStock !== null) {
1018 console.log(`[Stock Tracker] 🔍 Found in state: "${rawLabel}" = ${rawAvail} → ${inStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1019 pairs.push({ label: rawLabel, inStock });
1020 }
1021 }
1022 }
1023
1024 // Recurse into arrays and objects
1025 if (Array.isArray(obj)) {
1026 obj.forEach(item => searchForSizes(item, depth + 1));
1027 } else {
1028 Object.values(obj).forEach(val => searchForSizes(val, depth + 1));
1029 }
1030 }
1031
1032 searchForSizes(state);
1033 } catch (e) {
1034 console.warn('[Stock Tracker] Failed to parse preloaded state:', e);
1035 }
1036 }
1037 }
1038 }
1039
1040 const seen = new Map();
1041 for (const p of pairs) {
1042 const key = normalizeSizeLabel(p.label);
1043 if (!seen.has(key)) {
1044 seen.set(key, p);
1045 } else if (p.inStock === true) {
1046 seen.set(key, p);
1047 }
1048 }
1049
1050 const out = Array.from(seen.values());
1051 console.log(`[Stock Tracker] 🔍 Inditex extraction complete: ${out.length} unique sizes found`);
1052 if (out.length > 0) {
1053 console.log('[Stock Tracker] 🔍 Extracted sizes:', out.map(s => `${s.label}: ${s.inStock ? 'IN' : 'OUT'}`));
1054 }
1055 return out.length ? out : null;
1056 } catch (e) {
1057 console.warn('[Stock Tracker] extractInditexOffers error:', e);
1058 return null;
1059 }
1060 }
1061
1062 // Check stock from live DOM (when product page is open)
1063 function liveStockFromOpenTab(selector, url) {
1064 try {
1065 const here = location.href.split('#')[0].split('?')[0];
1066 if (here !== url) return null;
1067 const node = document.querySelector(selector);
1068 if (!node) return null;
1069
1070 if (node.classList.contains('size-selector-sizes-size--enabled')) {
1071 const isDisabled = node.classList.contains('size-selector-sizes-size--disabled') ||
1072 node.classList.contains('size-selector-sizes-size--unavailable') ||
1073 node.textContent?.toLowerCase().includes('coming soon');
1074 return !isDisabled;
1075 }
1076
1077 const disabled = node.hasAttribute('disabled') || node.getAttribute('aria-disabled') === 'true';
1078 const txt = (node.textContent||'').toLowerCase();
1079 const cls = (node.className||'').toLowerCase();
1080 if (disabled) return false;
1081 if (/(sold\s?out|out\s?of\s?stock|unavailable|coming\s?soon|notify\s?me)/i.test(txt)) return false;
1082 if (/(out-of-stock|sold-out|unavailable|is-unavailable)/i.test(cls)) return false;
1083
1084 if (/(in-stock|is-available|available)\b/i.test(cls)) return true;
1085
1086 return null;
1087 } catch { return null; }
1088 }
1089
1090 // Fetch page HTML
1091 async function fetchHTML(url) {
1092 try {
1093 console.log('[Stock Tracker] Fetching URL:', url);
1094
1095 return new Promise((resolve, reject) => {
1096 GM_xmlhttpRequest({
1097 method: 'GET',
1098 url: url,
1099 headers: {
1100 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
1101 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
1102 'Accept-Language': 'en-US,en;q=0.5',
1103 'Cache-Control': 'no-cache',
1104 'Pragma': 'no-cache'
1105 },
1106 timeout: 30000,
1107 onload: function(response) {
1108 if (response.status >= 200 && response.status < 300) {
1109 console.log('[Stock Tracker] Fetched successfully:', response.responseText.length, 'chars');
1110 resolve(response.responseText);
1111 } else {
1112 reject(new Error(`HTTP ${response.status}`));
1113 }
1114 },
1115 onerror: function(error) {
1116 console.error('[Stock Tracker] Fetch error:', error);
1117 reject(new Error('Network error'));
1118 },
1119 ontimeout: function() {
1120 console.error('[Stock Tracker] Fetch timeout');
1121 reject(new Error('Request timeout'));
1122 }
1123 });
1124 });
1125 } catch (err) {
1126 console.error('[Stock Tracker] Fetch error:', err);
1127 throw err;
1128 }
1129 }
1130
1131 // WhatsApp helpers
1132 function openWhatsApp(text, phone = '') {
1133 const url = new URL('https://web.whatsapp.com/send');
1134 if (phone) url.searchParams.set('phone', phone.replace(/[^\d]/g, ''));
1135 url.searchParams.set('text', text);
1136
1137 (async () => {
1138 await GM.setValue('wa_pending_text', text);
1139 await GM.setValue('wa_pending_send', 'true');
1140 await GM.setValue('wa_pending_time', Date.now().toString());
1141 GM.openInTab(url.toString(), false);
1142 })();
1143 }
1144
1145 async function autoSendWhatsApp() {
1146 if (!location.host.includes('web.whatsapp.com')) return;
1147
1148 const shouldSend = await GM.getValue('wa_pending_send');
1149 const text = await GM.getValue('wa_pending_text');
1150 const pendingTime = await GM.getValue('wa_pending_time');
1151
1152 if (shouldSend === 'true' && text) {
1153 if (pendingTime) {
1154 const age = Date.now() - parseInt(pendingTime);
1155 if (age > 120000) {
1156 console.log('[Stock Tracker] Message too old, clearing...');
1157 await GM.setValue('wa_pending_send', '');
1158 await GM.setValue('wa_pending_text', '');
1159 await GM.setValue('wa_pending_time', '');
1160 return;
1161 }
1162 }
1163
1164 await GM.setValue('wa_pending_send', '');
1165 await GM.setValue('wa_pending_text', '');
1166 await GM.setValue('wa_pending_time', '');
1167
1168 console.log('[Stock Tracker] Auto-sending WhatsApp message...');
1169
1170 let attempts = 0;
1171 const maxAttempts = 40;
1172
1173 const checkComposer = setInterval(async () => {
1174 attempts++;
1175 console.log(`[Stock Tracker] Attempt ${attempts}/${maxAttempts} - Looking for composer...`);
1176
1177 // Try multiple selectors for the composer
1178 const composerSelectors = [
1179 'footer div[contenteditable="true"][role="textbox"]',
1180 'div[contenteditable="true"][data-tab="10"]',
1181 'div[contenteditable="true"][data-lexical-editor="true"]',
1182 'div[contenteditable="true"][data-testid="conversation-compose-box-input"]',
1183 'div[contenteditable="true"][title*="Type a message"]',
1184 'div[contenteditable="true"][title*="הקלד הודעה"]',
1185 'footer div[contenteditable="true"]',
1186 'div[role="textbox"][contenteditable="true"]'
1187 ];
1188
1189 let composer = null;
1190 for (const selector of composerSelectors) {
1191 composer = document.querySelector(selector);
1192 if (composer && composer.offsetParent !== null) {
1193 console.log('[Stock Tracker] Found composer with selector:', selector);
1194 break;
1195 }
1196 }
1197
1198 if (composer && composer.offsetParent !== null) {
1199 clearInterval(checkComposer);
1200 console.log('[Stock Tracker] Composer found! Writing message...');
1201
1202 try {
1203 // Focus the composer
1204 composer.focus();
1205 await new Promise(resolve => setTimeout(resolve, 300));
1206
1207 // Clear existing content
1208 composer.innerHTML = '';
1209
1210 // Insert text with line breaks
1211 const lines = text.split('\n');
1212 lines.forEach((line, idx) => {
1213 if (idx > 0) composer.appendChild(document.createElement('br'));
1214 composer.appendChild(document.createTextNode(line));
1215 });
1216
1217 // Trigger input events
1218 composer.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true }));
1219 composer.dispatchEvent(new Event('change', { bubbles: true }));
1220 composer.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'a' }));
1221 composer.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: 'a' }));
1222
1223 console.log('[Stock Tracker] Message written, waiting before sending...');
1224 await new Promise(resolve => setTimeout(resolve, 2000));
1225
1226 // Try multiple selectors for the send button - IMPROVED
1227 const sendButtonSelectors = [
1228 'span[data-icon="wds-ic-send-filled"]',
1229 'span[data-icon="send"]',
1230 'button[aria-label*="שליחה"]',
1231 'button[aria-label*="Send"]',
1232 'div[aria-label*="שליחה"][role="button"]',
1233 'div[aria-label*="Send"][role="button"]',
1234 'footer button[aria-label*="Send"]',
1235 'footer button[aria-label*="שליחה"]',
1236 'footer div[aria-label*="Send"][role="button"]',
1237 'footer div[aria-label*="שליחה"][role="button"]',
1238 'button[data-testid="send"]',
1239 'footer button[data-tab="11"]',
1240 'footer button span[data-icon="send"]'
1241 ];
1242
1243 let sendBtn = null;
1244 for (const selector of sendButtonSelectors) {
1245 const element = document.querySelector(selector);
1246 if (element) {
1247 // If it's a span, get the parent button/div with role="button"
1248 if (element.tagName === 'SPAN') {
1249 sendBtn = element.closest('button') || element.closest('div[role="button"]');
1250 } else {
1251 sendBtn = element;
1252 }
1253 if (sendBtn) {
1254 console.log('[Stock Tracker] Found send button with selector:', selector);
1255 break;
1256 }
1257 }
1258 }
1259
1260 if (sendBtn) {
1261 console.log('[Stock Tracker] Clicking send button...');
1262 console.log('[Stock Tracker] Send button element:', sendBtn.tagName, sendBtn.className);
1263
1264 // Try multiple click methods
1265 sendBtn.click();
1266 await new Promise(resolve => setTimeout(resolve, 100));
1267
1268 // Dispatch mouse events as backup
1269 sendBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
1270 sendBtn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
1271 sendBtn.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
1272
1273 // Try pressing Enter key as final backup
1274 composer.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13 }));
1275
1276 console.log('[Stock Tracker] ✅ Message sent successfully!');
1277
1278 // Show success notification
1279 setTimeout(() => {
1280 const notification = document.createElement('div');
1281 notification.style.cssText = `
1282 position: fixed;
1283 top: 20px;
1284 right: 20px;
1285 background: #25D366;
1286 color: white;
1287 padding: 16px 24px;
1288 border-radius: 12px;
1289 font-weight: 700;
1290 z-index: 999999;
1291 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
1292 `;
1293 notification.textContent = '✅ הודעה נשלחה בהצלחה!';
1294 document.body.appendChild(notification);
1295
1296 setTimeout(() => {
1297 notification.style.transition = 'opacity 0.3s';
1298 notification.style.opacity = '0';
1299 setTimeout(() => notification.remove(), 300);
1300 }, 3000);
1301 }, 500);
1302 } else {
1303 console.error('[Stock Tracker] ❌ Send button not found!');
1304 console.error('[Stock Tracker] Available elements in footer:',
1305 Array.from(document.querySelectorAll('footer *[role="button"], footer button')).map(b => ({
1306 tag: b.tagName,
1307 ariaLabel: b.getAttribute('aria-label'),
1308 classes: b.className,
1309 dataIcon: b.getAttribute('data-icon') || b.querySelector('[data-icon]')?.getAttribute('data-icon')
1310 }))
1311 );
1312
1313 // Show error notification
1314 const notification = document.createElement('div');
1315 notification.style.cssText = `
1316 position: fixed;
1317 top: 20px;
1318 right: 20px;
1319 background: #ef4444;
1320 color: white;
1321 padding: 16px 24px;
1322 border-radius: 12px;
1323 font-weight: 700;
1324 z-index: 999999;
1325 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
1326 `;
1327 notification.textContent = '⚠️ לא מצאתי כפתור שליחה - שלח ידנית';
1328 document.body.appendChild(notification);
1329
1330 setTimeout(() => {
1331 notification.style.transition = 'opacity 0.3s';
1332 notification.style.opacity = '0';
1333 setTimeout(() => notification.remove(), 300);
1334 }, 5000);
1335 }
1336 } catch (err) {
1337 console.error('[Stock Tracker] Error during send process:', err);
1338 }
1339 }
1340
1341 if (attempts >= maxAttempts) {
1342 clearInterval(checkComposer);
1343 console.error('[Stock Tracker] ❌ Timeout: Could not find composer after', maxAttempts, 'attempts');
1344
1345 // Show timeout notification
1346 const notification = document.createElement('div');
1347 notification.style.cssText = `
1348 position: fixed;
1349 top: 20px;
1350 right: 20px;
1351 background: #ef4444;
1352 color: white;
1353 padding: 16px 24px;
1354 border-radius: 12px;
1355 font-weight: 700;
1356 z-index: 999999;
1357 box-shadow: 0 4px 12px rgba(0,0,0,0.3);
1358 `;
1359 notification.textContent = '⚠️ לא הצלחתי למצוא את תיבת ההודעות';
1360 document.body.appendChild(notification);
1361
1362 setTimeout(() => {
1363 notification.style.transition = 'opacity 0.3s';
1364 notification.style.opacity = '0';
1365 setTimeout(() => notification.remove(), 300);
1366 }, 5000);
1367 }
1368 }, 1000);
1369 }
1370 }
1371
1372 // Check stock for all items
1373 async function checkAllStock() {
1374 const items = await getItems();
1375 const prefs = await getPrefs();
1376
1377 if (items.length === 0) {
1378 showToast('אין מוצרים במעקב', 'error');
1379 return;
1380 }
1381
1382 const statusText = document.querySelector('#st-status-text');
1383 if (statusText) {
1384 statusText.textContent = '🟡 בודק מלאי...';
1385 statusText.style.color = '#fbbf24';
1386 }
1387
1388 showToast(`🔄 בודק ${items.length} מוצרים...`, 'info');
1389 console.log('[Stock Tracker] ========================================');
1390 console.log('[Stock Tracker] Starting stock check for', items.length, 'items...');
1391 console.log('[Stock Tracker] ========================================');
1392
1393 let foundInStock = 0;
1394 let priceDrops = 0;
1395 let updatedItems = [...items];
1396 let successCount = 0;
1397
1398 for (let i = 0; i < updatedItems.length; i++) {
1399 const item = updatedItems[i];
1400 console.log(`[Stock Tracker] [${i+1}/${updatedItems.length}] Checking: ${item.title}`);
1401
1402 let retries = 3;
1403 let success = false;
1404
1405 while (retries > 0 && !success) {
1406 try {
1407 const html = await fetchHTML(item.url);
1408 const isInditex = item.url.includes('bershka.com') || item.url.includes('zara.com');
1409 const inditexPairs = isInditex ? extractInditexOffers(html) : null;
1410 const jsonLdPairs = extractAvailabilityFromJSONLD(html);
1411
1412 if (item.mode === 'price') {
1413 const adapter = getAdapter();
1414 const liveNow = adapter.price?.() ?? null;
1415 let best = liveNow ?? item.priceNow ?? null;
1416
1417 if (best == null && jsonLdPairs && jsonLdPairs[0]) {
1418 best = parsePrice(jsonLdPairs[0].label);
1419 }
1420
1421 const oldPrice = item.priceNow;
1422 item.priceNow = best;
1423 item.lastChecked = Date.now();
1424
1425 if (item.priceNow != null && item.targetPrice != null && item.priceNow <= item.targetPrice) {
1426 foundInStock++;
1427 const msg = `✈️ ירידת מחיר!\n\n${item.title}\nמחיר נוכחי: ₪${item.priceNow}\nיעד: ₪${item.targetPrice}\n\n${item.url}`;
1428 if (prefs.waPhone) openWhatsApp(msg, prefs.waPhone);
1429 if (prefs.soundAlerts) playNotificationSound();
1430 } else if (oldPrice != null && item.priceNow != null && item.priceNow < oldPrice) {
1431 priceDrops++;
1432 const msg = `💰 ירידת מחיר!\n\n${item.title}\nמחיר קודם: ₪${oldPrice}\nמחיר חדש: ₪${item.priceNow}\nחיסכון: ₪${(oldPrice - item.priceNow).toFixed(2)}\n\n${item.url}`;
1433 if (prefs.waPhone && prefs.notifications) openWhatsApp(msg, prefs.waPhone);
1434 if (prefs.soundAlerts) playNotificationSound();
1435 }
1436 } else {
1437 for (const size of item.sizes) {
1438 let newStock = null;
1439
1440 // First check live DOM if we're on the same page
1441 const currentUrl = location.href.split('#')[0].split('?')[0];
1442 if (currentUrl === item.url) {
1443 console.log(`[Stock Tracker] 🔍 We're on the product page! Checking live DOM for "${size.label}"`);
1444 newStock = liveStockFromOpenTab(size.selector, item.url);
1445 if (newStock !== null) {
1446 console.log(`[Stock Tracker] ✓ Live DOM matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1447 }
1448 }
1449
1450 if (newStock === null && inditexPairs) {
1451 const hit = inditexPairs.find(p => sizeMatches(p.label, size.label));
1452 if (hit) {
1453 newStock = !!hit.inStock;
1454 console.log(`[Stock Tracker] ✓ Inditex matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1455 }
1456 }
1457
1458 if (newStock === null && jsonLdPairs) {
1459 const hit = jsonLdPairs.find(p => sizeMatches(p.label, size.label));
1460 if (hit) {
1461 newStock = !!hit.inStock;
1462 console.log(`[Stock Tracker] ✓ JSON-LD matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1463 }
1464 }
1465
1466 if (newStock === null && !isInditex) {
1467 newStock = checkStockFromHTML(html, size.selector);
1468 if (newStock !== null) {
1469 console.log(`[Stock Tracker] ✓ HTML selector matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1470 }
1471 }
1472
1473 // Store previous state BEFORE updating
1474 const previousStock = size.inStock;
1475
1476 // Update to new state (or keep old if we couldn't determine)
1477 const currentStock = newStock !== null ? newStock : previousStock;
1478
1479 console.log(`[Stock Tracker] 📊 Size "${size.label}": PREVIOUS=${previousStock === true ? 'IN' : previousStock === false ? 'OUT' : 'UNKNOWN'} → CURRENT=${currentStock === true ? 'IN' : currentStock === false ? 'OUT' : 'UNKNOWN'}`);
1480
1481 // Check if it became in stock (was out, now in)
1482 const becameInStock = (previousStock === false || previousStock === null || previousStock === undefined) && currentStock === true;
1483
1484 if (becameInStock) {
1485 const ts = Date.now();
1486 const lastAlert = size.lastAlertAt || 0;
1487 const throttleMs = (prefs.throttleMinutesWA || 3) * 60 * 1000;
1488 const tooSoon = (ts - lastAlert) < throttleMs;
1489
1490 console.log(`[Stock Tracker] 🎉 SIZE BACK IN STOCK: "${size.label}"!`);
1491 console.log(`[Stock Tracker] Last alert: ${lastAlert ? new Date(lastAlert).toLocaleTimeString() : 'never'}, Too soon: ${tooSoon}`);
1492
1493 if (!tooSoon) {
1494 foundInStock++;
1495 size.lastAlertAt = ts;
1496
1497 console.log('[Stock Tracker] 🔔 TRIGGERING ALL ALERTS for:', item.title, size.label);
1498
1499 const message = `🔔 חזרה למלאי!\n\n${item.title}\nמידה: ${size.label}\nמחיר: ${item.price}\n\n${item.url}`;
1500
1501 // 1. WhatsApp notification
1502 if (prefs.waPhone) {
1503 console.log('[Stock Tracker] 📱 Sending WhatsApp message...');
1504 openWhatsApp(message, prefs.waPhone);
1505 }
1506
1507 // 2. Browser notification
1508 if (prefs.notifications) {
1509 console.log('[Stock Tracker] 🔔 Showing browser notification...');
1510 showToast(`🔔 ${size.label} חזרה למלאי!`, 'success');
1511 }
1512
1513 // 3. Sound alert - ALWAYS play sound when item is back in stock
1514 console.log('[Stock Tracker] 🔊 Playing sound alert...');
1515 playNotificationSound();
1516
1517 // 4. Auto add to cart
1518 if (prefs.autoAddToCart) {
1519 console.log('[Stock Tracker] 🛒 Auto-adding to cart...');
1520 await addToCart(item.url, size.selector, size.label);
1521 }
1522 } else {
1523 console.log('[Stock Tracker] ⏰ Alert throttled - too soon since last alert');
1524 }
1525 }
1526
1527 // Update the size stock status
1528 size.inStock = currentStock;
1529 size.lastChecked = Date.now();
1530 }
1531 }
1532
1533 success = true;
1534 successCount++;
1535
1536 } catch (err) {
1537 retries--;
1538 console.error(`[Stock Tracker] Error (attempt ${3-retries}/3):`, err);
1539 console.error('[Stock Tracker] Error details:', err.message, err.stack);
1540
1541 if (retries > 0) {
1542 await new Promise(resolve => setTimeout(resolve, 2000));
1543 }
1544 }
1545 }
1546 }
1547
1548 await saveItems(updatedItems);
1549
1550 if (statusText) {
1551 statusText.textContent = '🟢 המערכת פעילה';
1552 statusText.style.color = '#22c55e';
1553 }
1554
1555 console.log('[Stock Tracker] Check completed! Found:', foundInStock, 'Price drops:', priceDrops, 'Success:', successCount);
1556
1557 if (foundInStock > 0 || priceDrops > 0) {
1558 const msg = [];
1559 if (foundInStock > 0) msg.push(`${foundInStock} מידות במלאי`);
1560 if (priceDrops > 0) msg.push(`${priceDrops} ירידות מחיר`);
1561 showToast(`✅ נמצאו: ${msg.join(', ')}!`, 'success');
1562 } else {
1563 showToast(`✅ בדיקה הושלמה (${successCount}/${items.length})`, 'info');
1564 }
1565
1566 await renderList();
1567 await renderStats();
1568 await updateFabBadge();
1569 }
1570
1571 // Check stock for a single item
1572 async function checkSingleItem(itemId) {
1573 const items = await getItems();
1574 const prefs = await getPrefs();
1575 const item = items.find(i => i.id === itemId);
1576
1577 if (!item) {
1578 showToast('⚠️ מוצר לא נמצא', 'error');
1579 return;
1580 }
1581
1582 showToast(`🔄 בודק: ${item.title}...`, 'info');
1583 console.log('[Stock Tracker] Checking single item:', item.title);
1584 console.log('[Stock Tracker] Item URL:', item.url);
1585 console.log('[Stock Tracker] Item mode:', item.mode);
1586 console.log('[Stock Tracker] Item sizes:', item.sizes?.length || 0);
1587
1588 let retries = 3;
1589 let success = false;
1590
1591 while (retries > 0 && !success) {
1592 try {
1593 console.log(`[Stock Tracker] Fetching item data (attempt ${4-retries}/3)...`);
1594 const html = await fetchHTML(item.url);
1595 console.log('[Stock Tracker] HTML fetched successfully, length:', html.length);
1596
1597 const isInditex = item.url.includes('bershka.com') || item.url.includes('zara.com');
1598 const inditexPairs = isInditex ? extractInditexOffers(html) : null;
1599 const jsonLdPairs = extractAvailabilityFromJSONLD(html);
1600
1601 console.log('[Stock Tracker] Inditex pairs found:', inditexPairs?.length || 0);
1602 console.log('[Stock Tracker] JSON-LD pairs found:', jsonLdPairs?.length || 0);
1603
1604 let foundInStock = 0;
1605
1606 if (item.mode === 'price') {
1607 const adapter = getAdapter();
1608 const liveNow = adapter.price?.() ?? null;
1609 let best = liveNow ?? item.priceNow ?? null;
1610
1611 item.priceNow = best;
1612 item.lastChecked = Date.now();
1613
1614 console.log('[Stock Tracker] Price check - Current:', item.priceNow, 'Target:', item.targetPrice);
1615
1616 if (item.priceNow != null && item.targetPrice != null && item.priceNow <= item.targetPrice) {
1617 foundInStock++;
1618 const msg = `✈️ ירידת מחיר!\n\n${item.title}\nמחיר נוכחי: ₪${item.priceNow}\nיעד: ₪${item.targetPrice}\n\n${item.url}`;
1619 if (prefs.waPhone) openWhatsApp(msg, prefs.waPhone);
1620 if (prefs.soundAlerts) playNotificationSound();
1621 }
1622 } else {
1623 console.log('[Stock Tracker] Checking', item.sizes.length, 'sizes...');
1624 for (const size of item.sizes) {
1625 let newStock = null;
1626
1627 // First check live DOM if we're on the same page
1628 const currentUrl = location.href.split('#')[0].split('?')[0];
1629 if (currentUrl === item.url) {
1630 console.log(`[Stock Tracker] 🔍 We're on the product page! Checking live DOM for "${size.label}"`);
1631 newStock = liveStockFromOpenTab(size.selector, item.url);
1632 if (newStock !== null) {
1633 console.log(`[Stock Tracker] ✓ Live DOM matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1634 }
1635 }
1636
1637 if (newStock === null && inditexPairs) {
1638 const hit = inditexPairs.find(p => sizeMatches(p.label, size.label));
1639 if (hit) {
1640 newStock = !!hit.inStock;
1641 console.log(`[Stock Tracker] ✓ Inditex matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1642 }
1643 }
1644
1645 if (newStock === null && jsonLdPairs) {
1646 const hit = jsonLdPairs.find(p => sizeMatches(p.label, size.label));
1647 if (hit) {
1648 newStock = !!hit.inStock;
1649 console.log(`[Stock Tracker] ✓ JSON-LD matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1650 }
1651 }
1652
1653 if (newStock === null && !isInditex) {
1654 newStock = checkStockFromHTML(html, size.selector);
1655 if (newStock !== null) {
1656 console.log(`[Stock Tracker] ✓ HTML selector matched "${size.label}" ⇢ ${newStock ? 'IN STOCK' : 'OUT OF STOCK'}`);
1657 }
1658 }
1659
1660 const prev = size.inStock;
1661 const now = newStock !== null ? newStock : prev;
1662
1663 console.log(`[Stock Tracker] Size "${size.label}": ${prev === true ? 'WAS IN' : 'WAS OUT'} → ${now === true ? 'NOW IN' : 'NOW OUT'}`);
1664
1665 if ((prev === false || prev == null) && now === true) {
1666 foundInStock++;
1667 const message = `🔔 חזרה למלאי!\n\n${item.title}\nמידה: ${size.label}\nמחיר: ${item.price}\n\n${item.url}`;
1668
1669 console.log('[Stock Tracker] 🎉 SIZE BACK IN STOCK!', size.label);
1670
1671 if (prefs.waPhone) openWhatsApp(message, prefs.waPhone);
1672 if (prefs.notifications) showToast(`🔔 ${size.label} חזרה למלאי!`, 'success');
1673 if (prefs.soundAlerts) playNotificationSound();
1674 }
1675
1676 size.inStock = now;
1677 size.lastChecked = Date.now();
1678 }
1679 }
1680
1681 await saveItems(items);
1682 success = true;
1683
1684 console.log('[Stock Tracker] ✅ Check completed successfully!');
1685 console.log('[Stock Tracker] Found in stock:', foundInStock);
1686
1687 if (foundInStock > 0) {
1688 showToast(`✅ נמצאו ${foundInStock} מידות במלאי!`, 'success');
1689 } else {
1690 showToast('✅ בדיקה הושלמה - אין שינויים', 'info');
1691 }
1692
1693 } catch (err) {
1694 retries--;
1695 console.error(`[Stock Tracker] ❌ Error checking item (attempt ${3-retries}/3):`, err);
1696 console.error('[Stock Tracker] Error details:', err.message, err.stack);
1697
1698 if (retries === 0) {
1699 showToast('❌ שגיאה בבדיקת המוצר', 'error');
1700 } else {
1701 console.log('[Stock Tracker] Retrying in 2 seconds...');
1702 await new Promise(resolve => setTimeout(resolve, 2000));
1703 }
1704 }
1705 }
1706
1707 await renderList();
1708 await renderStats();
1709 await updateFabBadge();
1710 }
1711
1712 // Remove item
1713 async function removeItem(id) {
1714 if (!confirm('האם אתה בטוח שברצונך למחוק מוצר זה?')) return;
1715
1716 const items = await getItems();
1717 const filtered = items.filter(item => item.id !== id);
1718
1719 await saveItems(filtered);
1720 await renderList();
1721 await renderStats();
1722 await updateFabBadge();
1723 showToast('🗑️ מוצר נמחק בהצלחה', 'success');
1724 }
1725
1726 // Clear all data
1727 async function clearAllData() {
1728 if (!confirm('האם אתה בטוח שברצונך למחוק את כל הנתונים?')) return;
1729
1730 await saveItems([]);
1731 await renderList();
1732 await renderStats();
1733 await updateFabBadge();
1734 showToast('🗑️ כל הנתונים נמחקו', 'info');
1735 }
1736
1737 // Quick add from current page
1738 async function quickAddAllSizes() {
1739 const adapter = getAdapter();
1740 if (!adapter) {
1741 showToast('⚠️ אתר זה לא נתמך כרגע', 'error');
1742 return;
1743 }
1744
1745 const title = adapter.title();
1746 const price = adapter.price();
1747 const image = adapter.image();
1748 const url = location.href.split('#')[0].split('?')[0];
1749
1750 if (!title) {
1751 showToast('⚠️ לא הצלחתי לזהות את המוצר', 'error');
1752 return;
1753 }
1754
1755 showToast('⏳ מוסיף את כל המידות...', 'info');
1756
1757 const sizes = await adapter.getSizes();
1758
1759 if (!sizes || sizes.length === 0) {
1760 showToast('⚠️ לא נמצאו מידות', 'error');
1761 return;
1762 }
1763
1764 const items = await getItems();
1765
1766 const selectedSizeData = sizes.map(size => ({
1767 label: size.label,
1768 selector: buildSelector(size.element),
1769 inStock: size.inStock,
1770 lastChecked: Date.now(),
1771 lastAlertAt: 0
1772 }));
1773
1774 items.push({
1775 id: Date.now().toString(),
1776 mode: adapter.mode || 'stock',
1777 title,
1778 price: price ? `₪${price}` : 'לא זמין',
1779 image,
1780 url,
1781 sizes: selectedSizeData,
1782 addedAt: Date.now()
1783 });
1784
1785 await saveItems(items);
1786 await renderList();
1787 await renderStats();
1788 await updateFabBadge();
1789 showToast(`✅ נוספו ${sizes.length} מידות למעקב!`, 'success');
1790 }
1791
1792 // Copy product link
1793 async function copyProductLink(url) {
1794 try {
1795 await GM.setClipboard(url);
1796 showToast('✅ הקישור הועתק ללוח!', 'success');
1797 } catch (err) {
1798 console.error('[Stock Tracker] Copy error:', err);
1799 showToast('❌ שגיאה בהעתקת הקישור', 'error');
1800 }
1801 }
1802
1803 // Add to cart - Universal function for all sites
1804 async function addToCart(productUrl, sizeSelector, sizeLabel) {
1805 try {
1806 console.log('[Stock Tracker] 🛒 Attempting to add to cart:', sizeLabel);
1807 console.log('[Stock Tracker] Product URL:', productUrl);
1808 console.log('[Stock Tracker] Size selector:', sizeSelector);
1809
1810 // Open product page in new tab
1811 GM.openInTab(productUrl, false);
1812
1813 // Store the add-to-cart task
1814 await GM.setValue('pending_cart_add', JSON.stringify({
1815 url: productUrl,
1816 selector: sizeSelector,
1817 label: sizeLabel,
1818 timestamp: Date.now()
1819 }));
1820
1821 console.log('[Stock Tracker] ✅ Opened product page for cart addition');
1822 showToast(`🛒 פותח דף מוצר להוספה לסל: ${sizeLabel}`, 'info');
1823
1824 } catch (err) {
1825 console.error('[Stock Tracker] Add to cart error:', err);
1826 showToast('⚠️ לא הצלחתי להוסיף לסל - פתח ידנית', 'error');
1827 }
1828 }
1829
1830 // Auto add to cart when product page loads
1831 async function autoAddToCartOnLoad() {
1832 try {
1833 const pendingTask = await GM.getValue('pending_cart_add');
1834 if (!pendingTask) return;
1835
1836 const task = JSON.parse(pendingTask);
1837 const age = Date.now() - task.timestamp;
1838
1839 // Task expired after 30 seconds
1840 if (age > 30000) {
1841 await GM.setValue('pending_cart_add', '');
1842 return;
1843 }
1844
1845 const currentUrl = location.href.split('#')[0].split('?')[0];
1846 if (currentUrl !== task.url) return;
1847
1848 // Clear the task
1849 await GM.setValue('pending_cart_add', '');
1850
1851 console.log('[Stock Tracker] 🛒 Auto-adding to cart:', task.label);
1852
1853 // Wait for page to load
1854 await new Promise(resolve => setTimeout(resolve, 3000));
1855
1856 // For Zara - first click "Add to Cart" to open size selector
1857 if (task.url.includes('zara.com')) {
1858 console.log('[Stock Tracker] Zara detected - opening size selector first');
1859 const addToCartBtn = document.querySelector('button[data-qa-action="add-to-cart"]');
1860 if (addToCartBtn) {
1861 console.log('[Stock Tracker] Clicking add to cart to open size selector...');
1862 addToCartBtn.click();
1863 await new Promise(resolve => setTimeout(resolve, 1500));
1864 }
1865 }
1866
1867 // Find and click the size button
1868 const sizeBtn = document.querySelector(task.selector);
1869 if (sizeBtn) {
1870 console.log('[Stock Tracker] Found size button, clicking...');
1871 sizeBtn.click();
1872 await new Promise(resolve => setTimeout(resolve, 1000));
1873 } else {
1874 console.warn('[Stock Tracker] Size button not found with selector:', task.selector);
1875
1876 // Fallback: try to find by label
1877 const sizeElements = document.querySelectorAll('li.size-selector-sizes__size, button[class*="size"], [data-size]');
1878 for (const el of sizeElements) {
1879 const label = el.textContent?.trim() || el.getAttribute('data-size');
1880 if (label === task.label) {
1881 console.log('[Stock Tracker] Found size by label, clicking...');
1882 const btn = el.querySelector('button') || el;
1883 btn.click();
1884 await new Promise(resolve => setTimeout(resolve, 1000));
1885 break;
1886 }
1887 }
1888 }
1889
1890 // Wait a bit more for size selection to register
1891 await new Promise(resolve => setTimeout(resolve, 500));
1892
1893 // Find and click add to cart button - Universal selectors
1894 const addToCartSelectors = [
1895 // Zara
1896 'button[data-qa-action="add-to-cart"]',
1897 'button[class*="add-to-cart"]',
1898 // Bershka
1899 'button[data-qa-anchor="addToBag"]',
1900 'button[class*="add-to-bag"]',
1901 // H&M
1902 'button[data-testid="add-to-bag"]',
1903 'button[class*="addToBag"]',
1904 // Nike
1905 'button[aria-label*="Add to Bag"]',
1906 'button[data-qa="add-to-cart"]',
1907 // Amazon
1908 '#add-to-cart-button',
1909 'input[id="add-to-cart-button"]',
1910 // Generic
1911 'button[id*="add-to-cart"]',
1912 'button[id*="add-to-bag"]',
1913 'button[class*="add-to-basket"]',
1914 'button[aria-label*="הוסף לסל"]',
1915 'button[aria-label*="הוספה לסל"]',
1916 'button:has-text("הוסף לסל")',
1917 'button:has-text("Add to Cart")',
1918 'button:has-text("Add to Bag")',
1919 '[data-testid*="add-to-cart"]',
1920 '[data-testid*="add-to-bag"]'
1921 ];
1922
1923 let addBtn = null;
1924 for (const selector of addToCartSelectors) {
1925 addBtn = document.querySelector(selector);
1926 if (addBtn && !addBtn.disabled) {
1927 console.log('[Stock Tracker] Found add to cart button:', selector);
1928 break;
1929 }
1930 }
1931
1932 // Fallback: search by text content
1933 if (!addBtn) {
1934 const buttons = document.querySelectorAll('button');
1935 for (const btn of buttons) {
1936 const text = btn.textContent?.trim().toLowerCase();
1937 if (text && (
1938 text.includes('add to cart') ||
1939 text.includes('add to bag') ||
1940 text.includes('הוסף לסל') ||
1941 text.includes('הוספה לסל') ||
1942 text.includes('añadir a la bolsa') ||
1943 text.includes('in den warenkorb')
1944 )) {
1945 addBtn = btn;
1946 console.log('[Stock Tracker] Found add button by text:', text);
1947 break;
1948 }
1949 }
1950 }
1951
1952 if (addBtn && !addBtn.disabled) {
1953 console.log('[Stock Tracker] Clicking add to cart button...');
1954 addBtn.click();
1955 console.log('[Stock Tracker] ✅ Added to cart successfully!');
1956
1957 // Show success message after a delay
1958 setTimeout(() => {
1959 showToast(`✅ ${task.label} נוסף לסל!`, 'success');
1960 playNotificationSound();
1961 }, 1500);
1962 } else {
1963 console.warn('[Stock Tracker] Add to cart button not found or disabled');
1964 showToast('⚠️ לא מצאתי כפתור הוספה לסל', 'error');
1965 }
1966
1967 } catch (err) {
1968 console.error('[Stock Tracker] Auto add to cart error:', err);
1969 }
1970 }
1971
1972 // UI Styles
1973 const styles = `
1974 .st-fab {
1975 position: fixed;
1976 right: 20px;
1977 bottom: 20px;
1978 z-index: 2147483646;
1979 background: linear-gradient(135deg, #25D366 0%, #22c55e 100%);
1980 color: #fff;
1981 border: none;
1982 border-radius: 50px;
1983 padding: 16px 24px;
1984 font-weight: 800;
1985 font-size: 16px;
1986 cursor: pointer;
1987 box-shadow: 0 8px 24px rgba(37, 211, 102, 0.4);
1988 transition: all 0.3s ease;
1989 }
1990 .st-fab:hover {
1991 transform: translateY(-2px);
1992 box-shadow: 0 12px 32px rgba(37, 211, 102, 0.6);
1993 }
1994
1995 .st-fab-alert {
1996 animation: pulse 2s infinite;
1997 }
1998
1999 @keyframes pulse {
2000 0%, 100% { box-shadow: 0 8px 24px rgba(37, 211, 102, 0.4); }
2001 50% { box-shadow: 0 8px 24px rgba(239, 68, 68, 0.6); }
2002 }
2003
2004 .st-badge {
2005 position: absolute;
2006 top: -8px;
2007 right: -8px;
2008 background: #ef4444;
2009 color: #fff;
2010 border-radius: 50%;
2011 width: 24px;
2012 height: 24px;
2013 display: flex;
2014 align-items: center;
2015 justify-content: center;
2016 font-size: 12px;
2017 font-weight: 800;
2018 border: 2px solid #fff;
2019 animation: bounce 1s infinite;
2020 }
2021
2022 @keyframes bounce {
2023 0%, 100% { transform: scale(1); }
2024 50% { transform: scale(1.1); }
2025 }
2026
2027 .st-panel {
2028 position: fixed;
2029 right: 20px;
2030 top: 50%;
2031 transform: translateY(-50%);
2032 z-index: 2147483646;
2033 background: linear-gradient(180deg, #1a1a1a 0%, #0a0a0a 100%);
2034 color: #fff;
2035 border: 1px solid #333;
2036 border-radius: 20px;
2037 width: 520px;
2038 max-height: 90vh;
2039 overflow: hidden;
2040 display: none;
2041 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8);
2042 }
2043
2044 .st-header {
2045 padding: 24px;
2046 background: linear-gradient(135deg, #25D366 0%, #22c55e 100%);
2047 display: flex;
2048 justify-content: space-between;
2049 align-items: center;
2050 font-weight: 800;
2051 font-size: 20px;
2052 }
2053
2054 .st-body {
2055 padding: 24px;
2056 max-height: calc(90vh - 90px);
2057 overflow-y: auto;
2058 }
2059
2060 .st-body::-webkit-scrollbar { width: 8px; }
2061 .st-body::-webkit-scrollbar-track { background: #1a1a1a; }
2062 .st-body::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
2063 .st-body::-webkit-scrollbar-thumb:hover { background: #555; }
2064
2065 .st-input {
2066 width: 100%;
2067 background: #2a2a2a;
2068 color: #fff;
2069 border: 1px solid #444;
2070 border-radius: 10px;
2071 padding: 14px 16px;
2072 margin: 8px 0;
2073 font-size: 14px;
2074 box-sizing: border-box;
2075 transition: all 0.2s;
2076 }
2077
2078 .st-input:focus {
2079 outline: none;
2080 border-color: #25D366;
2081 box-shadow: 0 0 0 3px rgba(37, 211, 102, 0.1);
2082 }
2083
2084 .st-checkbox {
2085 display: flex;
2086 align-items: center;
2087 gap: 12px;
2088 margin: 12px 0;
2089 cursor: pointer;
2090 padding: 10px;
2091 border-radius: 8px;
2092 transition: background 0.2s;
2093 }
2094
2095 .st-checkbox:hover { background: rgba(255, 255, 255, 0.05); }
2096 .st-checkbox input { width: 20px; height: 20px; cursor: pointer; accent-color: #25D366; }
2097
2098 .st-btn {
2099 background: linear-gradient(135deg, #25D366 0%, #22c55e 100%);
2100 color: #fff;
2101 border: none;
2102 border-radius: 10px;
2103 padding: 14px 20px;
2104 font-weight: 700;
2105 cursor: pointer;
2106 width: 100%;
2107 margin: 8px 0;
2108 font-size: 14px;
2109 transition: all 0.2s ease;
2110 }
2111 .st-btn:hover {
2112 transform: translateY(-2px);
2113 box-shadow: 0 6px 16px rgba(37, 211, 102, 0.4);
2114 }
2115
2116 .st-btn-secondary {
2117 background: #2a2a2a;
2118 border: 1px solid #444;
2119 }
2120 .st-btn-secondary:hover {
2121 background: #333;
2122 box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1);
2123 }
2124
2125 .st-btn-danger {
2126 background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
2127 }
2128 .st-btn-danger:hover {
2129 box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
2130 }
2131
2132 .st-btn-purple {
2133 background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
2134 }
2135
2136 .st-btn-blue {
2137 background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
2138 }
2139
2140 .st-item {
2141 background: linear-gradient(135deg, #2a2a2a 0%, #252525 100%);
2142 border: 1px solid #444;
2143 border-radius: 16px;
2144 padding: 20px;
2145 margin: 16px 0;
2146 transition: all 0.3s;
2147 }
2148
2149 .st-item:hover {
2150 border-color: #25D366;
2151 box-shadow: 0 8px 24px rgba(37, 211, 102, 0.15);
2152 transform: translateY(-2px);
2153 }
2154
2155 .st-size {
2156 display: inline-block;
2157 background: #1a1a1a;
2158 border: 2px solid #444;
2159 border-radius: 10px;
2160 padding: 10px 14px;
2161 margin: 6px 4px;
2162 font-size: 13px;
2163 font-weight: 700;
2164 cursor: pointer;
2165 transition: all 0.2s;
2166 }
2167
2168 .st-size.in-stock {
2169 border-color: #22c55e;
2170 color: #22c55e;
2171 background: rgba(34, 197, 94, 0.1);
2172 }
2173
2174 .st-size.out-of-stock {
2175 border-color: #ef4444;
2176 color: #ef4444;
2177 background: rgba(239, 68, 68, 0.1);
2178 }
2179
2180 .st-size:hover { transform: scale(1.08); }
2181
2182 .st-toast {
2183 position: fixed;
2184 top: 20px;
2185 right: 20px;
2186 background: #1a1a1a;
2187 color: #fff;
2188 padding: 16px 20px;
2189 border-radius: 12px;
2190 z-index: 2147483647;
2191 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
2192 font-weight: 700;
2193 border-left: 4px solid #25D366;
2194 animation: slideIn 0.3s ease;
2195 }
2196
2197 @keyframes slideIn {
2198 from { transform: translateX(400px); opacity: 0; }
2199 to { transform: translateX(0); opacity: 1; }
2200 }
2201
2202 .st-toast.error { border-left-color: #ef4444; }
2203 .st-toast.info { border-left-color: #3b82f6; }
2204
2205 .st-stats {
2206 background: linear-gradient(135deg, #2a2a2a 0%, #252525 100%);
2207 border: 1px solid #444;
2208 border-radius: 16px;
2209 padding: 20px;
2210 margin: 16px 0;
2211 display: grid;
2212 grid-template-columns: repeat(3, 1fr);
2213 gap: 16px;
2214 text-align: center;
2215 }
2216
2217 .st-stat {
2218 padding: 16px;
2219 background: #1a1a1a;
2220 border-radius: 12px;
2221 transition: all 0.2s;
2222 }
2223
2224 .st-stat:hover {
2225 background: #222;
2226 transform: translateY(-2px);
2227 }
2228
2229 .st-stat-value {
2230 font-size: 28px;
2231 font-weight: 800;
2232 color: #25D366;
2233 margin-bottom: 4px;
2234 }
2235
2236 .st-stat-label {
2237 font-size: 12px;
2238 color: #999;
2239 margin-top: 4px;
2240 }
2241
2242 .st-info-box {
2243 background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
2244 border: 1px solid #3b82f6;
2245 border-radius: 12px;
2246 padding: 16px;
2247 margin: 16px 0;
2248 }
2249
2250 .st-info-title {
2251 font-weight: 700;
2252 margin-bottom: 8px;
2253 color: #60a5fa;
2254 font-size: 14px;
2255 }
2256
2257 .st-info-content {
2258 font-size: 12px;
2259 color: #93c5fd;
2260 line-height: 1.6;
2261 }
2262
2263 .st-countdown-box {
2264 text-align: center;
2265 padding: 16px;
2266 background: linear-gradient(135deg, #2a2a2a 0%, #252525 100%);
2267 border-radius: 12px;
2268 margin: 16px 0;
2269 border: 1px solid #444;
2270 }
2271
2272 .st-countdown-label {
2273 color: #999;
2274 font-size: 12px;
2275 margin-bottom: 8px;
2276 }
2277
2278 .st-countdown-time {
2279 color: #25D366;
2280 font-weight: 700;
2281 font-size: 24px;
2282 }
2283
2284 .st-button-grid {
2285 display: grid;
2286 grid-template-columns: repeat(5, 1fr);
2287 gap: 8px;
2288 margin-top: 16px;
2289 }
2290
2291 .st-button-grid button {
2292 padding: 10px 8px;
2293 font-size: 11px;
2294 white-space: nowrap;
2295 }
2296
2297 .st-tag {
2298 display: inline-block;
2299 padding: 4px 8px;
2300 border-radius: 999px;
2301 background: #1f2937;
2302 color: #9ca3af;
2303 margin: 2px;
2304 font-size: 11px;
2305 }
2306 `;
2307
2308 // Create UI
2309 function createUI() {
2310 const styleEl = document.createElement('style');
2311 styleEl.textContent = styles;
2312 document.head.appendChild(styleEl);
2313
2314 const fab = document.createElement('button');
2315 fab.className = 'st-fab';
2316 fab.innerHTML = '📦 מעקב מלאי/מחיר';
2317 document.body.appendChild(fab);
2318
2319 // Make FAB draggable
2320 let isDragging = false;
2321 let hasMoved = false;
2322 let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
2323
2324 fab.addEventListener('mousedown', dragStart);
2325 document.addEventListener('mousemove', drag);
2326 document.addEventListener('mouseup', dragEnd);
2327 fab.addEventListener('touchstart', dragStart);
2328 document.addEventListener('touchmove', drag);
2329 document.addEventListener('touchend', dragEnd);
2330
2331 function dragStart(e) {
2332 if (e.type === 'touchstart') {
2333 initialX = e.touches[0].clientX - xOffset;
2334 initialY = e.touches[0].clientY - yOffset;
2335 } else {
2336 initialX = e.clientX - xOffset;
2337 initialY = e.clientY - yOffset;
2338 }
2339
2340 if (e.target === fab || fab.contains(e.target)) {
2341 isDragging = true;
2342 hasMoved = false;
2343 }
2344 }
2345
2346 function drag(e) {
2347 if (isDragging) {
2348 e.preventDefault();
2349
2350 if (e.type === 'touchmove') {
2351 currentX = e.touches[0].clientX - initialX;
2352 currentY = e.touches[0].clientY - initialY;
2353 } else {
2354 currentX = e.clientX - initialX;
2355 currentY = e.clientY - initialY;
2356 }
2357
2358 const distance = Math.sqrt(Math.pow(currentX - xOffset, 2) + Math.pow(currentY - yOffset, 2));
2359 if (distance > 5) hasMoved = true;
2360
2361 xOffset = currentX;
2362 yOffset = currentY;
2363 fab.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`;
2364 }
2365 }
2366
2367 function dragEnd() {
2368 if (isDragging) {
2369 initialX = currentX;
2370 initialY = currentY;
2371 isDragging = false;
2372 (async () => { await GM.setValue('fab_position', JSON.stringify({ x: xOffset, y: yOffset })); })();
2373 }
2374 }
2375
2376 (async () => {
2377 const savedPos = await GM.getValue('fab_position');
2378 if (savedPos) {
2379 const pos = JSON.parse(savedPos);
2380 xOffset = pos.x;
2381 yOffset = pos.y;
2382 fab.style.transform = `translate3d(${pos.x}px, ${pos.y}px, 0)`;
2383 }
2384 })();
2385
2386 const panel = document.createElement('div');
2387 panel.className = 'st-panel';
2388 panel.innerHTML = `
2389 <div class="st-header">
2390 <div>📦 WhatsApp Alerts + Filters</div>
2391 <button class="st-btn-secondary" id="st-close" style="padding:8px 12px;border-radius:50%;width:auto;border:0">✕</button>
2392 </div>
2393 <div class="st-body">
2394 <div id="st-stats"></div>
2395
2396 <div style="margin-bottom:20px">
2397 <label style="display:block;margin-bottom:8px;font-weight:700">📱 מספר WhatsApp</label>
2398 <input type="text" class="st-input" id="st-phone" placeholder="972501234567">
2399 </div>
2400
2401 <div style="margin-bottom:20px">
2402 <label style="display:block;margin-bottom:8px;font-weight:700">⏱️ בדוק מלאי כל (דקות)</label>
2403 <input type="number" class="st-input" id="st-interval" placeholder="5" min="1" value="5">
2404 </div>
2405
2406 <label class="st-checkbox">
2407 <input type="checkbox" id="st-notifications" checked>
2408 <span>🔔 הצג התראות בדפדפן</span>
2409 </label>
2410
2411 <label class="st-checkbox">
2412 <input type="checkbox" id="st-sound" checked>
2413 <span>🔊 השמע צליל התראה</span>
2414 </label>
2415
2416 <label class="st-checkbox">
2417 <input type="checkbox" id="st-autocart" checked>
2418 <span>🛒 הוסף לסל אוטומטית</span>
2419 </label>
2420
2421 <button class="st-btn" id="st-add">➕ הוסף מהעמוד</button>
2422 <button class="st-btn st-btn-purple" id="st-quick-add">⚡ הוסף את כל המידות</button>
2423 <button class="st-btn st-btn-secondary" id="st-check">🔄 בדוק עכשיו</button>
2424 <button class="st-btn st-btn-blue" id="st-test">🧪 בדיקת WhatsApp</button>
2425
2426 <div class="st-countdown-box">
2427 <div class="st-countdown-label">⏱️ בדיקה הבאה בעוד:</div>
2428 <div class="st-countdown-time" id="st-countdown-text">--:--</div>
2429 </div>
2430
2431 <button class="st-btn st-btn-secondary st-btn-danger" id="st-clear" style="font-size:12px">🗑️ מחק הכל</button>
2432
2433 <div style="margin-top:20px">
2434 <h3 style="margin-bottom:12px">מוצרים במעקב:</h3>
2435
2436 <div class="st-info-box">
2437 <div class="st-info-title">ℹ️ איך זה עובד?</div>
2438 <div class="st-info-content">
2439 • המערכת בודקת מלאי אוטומטית כל <span id="st-interval-display">5</span> דקות<br>
2440 • <strong>חשוב:</strong> השאר דף אחד פתוח בדפדפן<br>
2441 • המערכת תשלח התראת WhatsApp כשמידה חוזרת למלאי<br>
2442 • <span id="st-status-text" style="color:#22c55e;font-weight:700">🟢 המערכת פעילה</span>
2443 </div>
2444 </div>
2445
2446 <div style="margin-bottom:12px">
2447 <input type="text" class="st-input" id="st-search" placeholder="🔍 חפש מוצר..." style="margin:0">
2448 </div>
2449
2450 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
2451 <select class="st-input" id="st-filter" style="margin:0">
2452 <option value="all">הכל</option>
2453 <option value="stock-in">במלאי (הכל במלאי)</option>
2454 <option value="stock-out">לא במלאי (הכל חסר)</option>
2455 <option value="stock-partial">חלקי במלאי</option>
2456 <option value="mode-stock">מצב: מלאי</option>
2457 <option value="mode-price">מצב: מחיר</option>
2458 </select>
2459 <select class="st-input" id="st-sort" style="margin:0">
2460 <option value="date-desc">חדש → ישן</option>
2461 <option value="date-asc">ישן → חדש</option>
2462 <option value="title-asc">שם א׳→ת׳</option>
2463 <option value="title-desc">שם ת׳→א׳</option>
2464 <option value="site-asc">דומיין א׳→ת׳</option>
2465 <option value="price-asc">מחיר נמוך→גבוה</option>
2466 <option value="price-desc">מחיר גבוה→נמוך</option>
2467 <option value="stock-desc">יותר במלאי</option>
2468 <option value="stock-asc">פחות במלאי</option>
2469 </select>
2470 </div>
2471
2472 <div id="st-list"></div>
2473 </div>
2474 </div>
2475 `;
2476 document.body.appendChild(panel);
2477
2478 // Event listeners
2479 fab.onclick = async () => {
2480 if (hasMoved) {
2481 hasMoved = false;
2482 return;
2483 }
2484 const isOpen = panel.style.display === 'block';
2485 panel.style.display = isOpen ? 'none' : 'block';
2486 if (!isOpen) {
2487 await loadPrefsToUI();
2488 await renderList();
2489 await renderStats();
2490 applyDarkMode();
2491 }
2492 };
2493
2494 panel.querySelector('#st-close').onclick = () => {
2495 panel.style.display = 'none';
2496 };
2497
2498 panel.querySelector('#st-add').onclick = addCurrentProduct;
2499 panel.querySelector('#st-quick-add').onclick = quickAddAllSizes;
2500 panel.querySelector('#st-check').onclick = async () => {
2501 await checkAllStock();
2502 const prefs = await getPrefs();
2503 const interval = (prefs.checkIntervalMin || 5) * 60 * 1000;
2504 nextCheckTime = Date.now() + interval;
2505 };
2506 panel.querySelector('#st-test').onclick = async () => {
2507 const phoneInput = document.querySelector('#st-phone');
2508 const phoneValue = phoneInput ? phoneInput.value.trim() : '';
2509
2510 if (!phoneValue) {
2511 showToast('⚠️ הזן מספר WhatsApp קודם', 'error');
2512 return;
2513 }
2514
2515 const prefs = await getPrefs();
2516 prefs.waPhone = phoneValue;
2517 await savePrefs(prefs);
2518
2519 const testMessage = '🧪 בדיקת מערכת\n\nזוהי הודעת בדיקה.\nאם קיבלת הודעה זו - המערכת עובדת תקין! ✅';
2520 openWhatsApp(testMessage, prefs.waPhone);
2521 showToast('📱 פותח WhatsApp...', 'info');
2522 };
2523 panel.querySelector('#st-clear').onclick = clearAllData;
2524
2525 panel.querySelector('#st-phone').onchange = async (e) => {
2526 const prefs = await getPrefs();
2527 prefs.waPhone = e.target.value.trim();
2528 await savePrefs(prefs);
2529 showToast('✅ מספר WhatsApp נשמר', 'success');
2530 };
2531
2532 panel.querySelector('#st-interval').onchange = async (e) => {
2533 const prefs = await getPrefs();
2534 prefs.checkIntervalMin = Math.max(1, parseInt(e.target.value) || 5);
2535 await savePrefs(prefs);
2536 const display = document.querySelector('#st-interval-display');
2537 if (display) display.textContent = prefs.checkIntervalMin;
2538 showToast('✅ זמן ריענון עודכן', 'success');
2539 await startBackgroundCheck();
2540 };
2541
2542 panel.querySelector('#st-notifications').onchange = async (e) => {
2543 const prefs = await getPrefs();
2544 prefs.notifications = e.target.checked;
2545 await savePrefs(prefs);
2546 showToast(e.target.checked ? '🔔 התראות מופעלות' : '🔕 התראות כבויות', 'info');
2547 };
2548
2549 panel.querySelector('#st-sound').onchange = async (e) => {
2550 const prefs = await getPrefs();
2551 prefs.soundAlerts = e.target.checked;
2552 await savePrefs(prefs);
2553 if (e.target.checked) {
2554 playNotificationSound();
2555 showToast('🔊 צלילי התראה מופעלים', 'info');
2556 } else {
2557 showToast('🔇 צלילי התראה כבויים', 'info');
2558 }
2559 };
2560
2561 panel.querySelector('#st-autocart').onchange = async (e) => {
2562 const prefs = await getPrefs();
2563 prefs.autoAddToCart = e.target.checked;
2564 await savePrefs(prefs);
2565 showToast(e.target.checked ? '🛒 הוספה אוטומטית הופעל' : '🛒 הוספה אוטומטית כובה', 'info');
2566 };
2567
2568 const searchEl = panel.querySelector('#st-search');
2569 const sortEl = panel.querySelector('#st-sort');
2570 const filterEl = panel.querySelector('#st-filter');
2571
2572 if (searchEl) searchEl.oninput = () => renderList();
2573 if (sortEl) sortEl.onchange = () => renderList();
2574 if (filterEl) filterEl.onchange = () => renderList();
2575 }
2576
2577 // Toast notification
2578 function showToast(message, type = 'success') {
2579 const toast = document.createElement('div');
2580 toast.className = `st-toast ${type}`;
2581 toast.textContent = message;
2582 document.body.appendChild(toast);
2583
2584 setTimeout(() => {
2585 toast.style.transition = 'opacity 0.3s';
2586 toast.style.opacity = '0';
2587 setTimeout(() => toast.remove(), 300);
2588 }, 3500);
2589 }
2590
2591 // Load preferences to UI
2592 async function loadPrefsToUI() {
2593 const prefs = await getPrefs();
2594 const phoneEl = document.querySelector('#st-phone');
2595 const intervalEl = document.querySelector('#st-interval');
2596 const notificationsEl = document.querySelector('#st-notifications');
2597 const soundEl = document.querySelector('#st-sound');
2598 const autoCartEl = document.querySelector('#st-autocart');
2599 const displayEl = document.querySelector('#st-interval-display');
2600
2601 if (phoneEl) phoneEl.value = prefs.waPhone || '';
2602 if (intervalEl) intervalEl.value = prefs.checkIntervalMin || 5;
2603 if (notificationsEl) notificationsEl.checked = prefs.notifications !== false;
2604 if (soundEl) soundEl.checked = prefs.soundAlerts !== false;
2605 if (autoCartEl) autoCartEl.checked = prefs.autoAddToCart !== false;
2606 if (displayEl) displayEl.textContent = prefs.checkIntervalMin || 5;
2607 }
2608
2609 // Render statistics
2610 async function renderStats() {
2611 const items = await getItems();
2612 const statsEl = document.querySelector('#st-stats');
2613
2614 if (!statsEl) return;
2615
2616 const totalItems = items.length;
2617 const totalSizes = items.reduce((sum, item) => item.mode === 'stock' ? sum + item.sizes.length : sum, 0);
2618 const inStockSizes = items.reduce((sum, item) =>
2619 item.mode === 'stock' ? sum + item.sizes.filter(s => s.inStock).length : sum, 0
2620 );
2621
2622 let totalPrice = 0;
2623 let priceCount = 0;
2624 items.forEach(item => {
2625 const price = item.mode === 'price' ? item.priceNow : parsePrice(item.price);
2626 if (price) {
2627 totalPrice += price;
2628 priceCount++;
2629 }
2630 });
2631
2632 const avgPrice = priceCount > 0 ? (totalPrice / priceCount).toFixed(2) : 0;
2633
2634 statsEl.innerHTML = `
2635 <div class="st-stat">
2636 <div class="st-stat-value">${totalItems}</div>
2637 <div class="st-stat-label">מוצרים</div>
2638 </div>
2639 <div class="st-stat">
2640 <div class="st-stat-value">${totalSizes}</div>
2641 <div class="st-stat-label">מידות</div>
2642 </div>
2643 <div class="st-stat">
2644 <div class="st-stat-value">${inStockSizes}</div>
2645 <div class="st-stat-label">במלאי</div>
2646 </div>
2647 <div class="st-stat" style="grid-column: span 3">
2648 <div class="st-stat-value" style="color:#fbbf24">₪${totalPrice.toFixed(2)}</div>
2649 <div class="st-stat-label">סה"כ מחיר | ממוצע: ₪${avgPrice}</div>
2650 </div>
2651 `;
2652 }
2653
2654 // Add current product
2655 async function addCurrentProduct() {
2656 const adapter = getAdapter();
2657 if (!adapter) {
2658 showToast('⚠️ אתר זה לא נתמך כרגע', 'error');
2659 return;
2660 }
2661
2662 const title = adapter.title();
2663 const price = adapter.price();
2664 const image = adapter.image();
2665 const url = location.href.split('#')[0].split('?')[0];
2666
2667 if (!title) {
2668 showToast('⚠️ לא הצלחתי לזהות את המוצר', 'error');
2669 return;
2670 }
2671
2672 if (adapter.mode === 'price') {
2673 const target = prompt('הזן מחיר יעד (לדוגמה 750):', price ?? '');
2674 const tNum = target ? parsePrice(String(target)) : null;
2675 if (!tNum) {
2676 showToast('⚠️ מחיר יעד לא תקין');
2677 return;
2678 }
2679 const items = await getItems();
2680 items.push({
2681 id: Date.now().toString(),
2682 mode: 'price',
2683 title, url, image,
2684 priceNow: price ?? null,
2685 targetPrice: tNum,
2686 lastChecked: 0,
2687 addedAt: Date.now()
2688 });
2689 await saveItems(items);
2690 showToast('✅ נוסף מעקב מחיר');
2691 await renderList();
2692 await renderStats();
2693 return;
2694 }
2695
2696 showToast('⏳ מחפש מידות...', 'info');
2697
2698 const sizes = await adapter.getSizes();
2699
2700 if (!sizes || sizes.length === 0) {
2701 showToast('⚠️ לא נמצאו מידות', 'error');
2702 return;
2703 }
2704
2705 showSizeSelection(title, price ? `₪${price}` : 'לא זמין', image, url, sizes);
2706 }
2707
2708 // Show size selection modal
2709 function showSizeSelection(title, price, image, url, sizes) {
2710 const overlay = document.createElement('div');
2711 overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);z-index:2147483647;display:flex;align-items:center;justify-content:center';
2712
2713 const modal = document.createElement('div');
2714 modal.style.cssText = 'background:#1a1a1a;border:1px solid #333;border-radius:16px;padding:24px;max-width:500px;width:90%;max-height:80vh;overflow-y:auto';
2715
2716 modal.innerHTML = `
2717 <h2 style="margin:0 0 16px 0;color:#fff">🧵 בחר מידות למעקב</h2>
2718 <p style="color:#999;margin-bottom:20px">${title}</p>
2719 <p style="color:#999;margin-bottom:20px">תקבל התראת WhatsApp כשהמידה חוזרת למלאי</p>
2720 <div id="size-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:10px;margin-bottom:20px"></div>
2721 <button class="st-btn" id="confirm-sizes">✅ התחל מעקב</button>
2722 <button class="st-btn st-btn-secondary" id="cancel-sizes" style="margin-top:0">❌ ביטול</button>
2723 `;
2724
2725 overlay.appendChild(modal);
2726 document.body.appendChild(overlay);
2727
2728 const sizeGrid = modal.querySelector('#size-grid');
2729 const selectedSizes = new Set();
2730
2731 sizes.forEach((size, idx) => {
2732 const btn = document.createElement('button');
2733 btn.style.cssText = `
2734 background: #2a2a2a;
2735 color: #fff;
2736 border: 2px solid ${size.inStock ? '#22c55e' : '#ef4444'};
2737 border-radius: 8px;
2738 padding: 12px;
2739 font-weight: 700;
2740 cursor: pointer;
2741 transition: all 0.2s;
2742 `;
2743 btn.textContent = `${size.label} ${size.inStock ? '✅' : '❌'}`;
2744
2745 btn.onclick = () => {
2746 if (selectedSizes.has(idx)) {
2747 selectedSizes.delete(idx);
2748 btn.style.background = '#2a2a2a';
2749 btn.style.color = '#fff';
2750 } else {
2751 selectedSizes.add(idx);
2752 btn.style.background = '#25D366';
2753 btn.style.color = '#000';
2754 }
2755 };
2756
2757 sizeGrid.appendChild(btn);
2758 });
2759
2760 modal.querySelector('#confirm-sizes').onclick = async () => {
2761 if (selectedSizes.size === 0) {
2762 showToast('⚠️ בחר לפחות מידה אחת', 'error');
2763 return;
2764 }
2765
2766 const items = await getItems();
2767
2768 const selectedSizeData = Array.from(selectedSizes).map(idx => ({
2769 label: sizes[idx].label,
2770 selector: buildSelector(sizes[idx].element),
2771 inStock: sizes[idx].inStock,
2772 lastChecked: Date.now(),
2773 lastAlertAt: 0
2774 }));
2775
2776 items.push({
2777 id: Date.now().toString(),
2778 mode: 'stock',
2779 title, price, image, url,
2780 sizes: selectedSizeData,
2781 addedAt: Date.now()
2782 });
2783
2784 await saveItems(items);
2785 overlay.remove();
2786 await renderList();
2787 await renderStats();
2788 await updateFabBadge();
2789 showToast(`✅ נוספו ${selectedSizes.size} מידות למעקב!`, 'success');
2790 };
2791
2792 modal.querySelector('#cancel-sizes').onclick = () => {
2793 overlay.remove();
2794 };
2795
2796 overlay.onclick = (e) => {
2797 if (e.target === overlay) overlay.remove();
2798 };
2799 }
2800
2801 // Render items list
2802 async function renderList() {
2803 const items = await getItems();
2804 const listEl = document.querySelector('#st-list');
2805
2806 if (!listEl) return;
2807
2808 if (items.length === 0) {
2809 listEl.innerHTML = '<p style="text-align:center;color:#999;padding:20px">אין מוצרים במעקב</p>';
2810 return;
2811 }
2812
2813 const searchQuery = document.querySelector('#st-search')?.value?.toLowerCase() || '';
2814 const sortValue = document.querySelector('#st-sort')?.value || 'date-desc';
2815 const filterValue = document.querySelector('#st-filter')?.value || 'all';
2816
2817 let filteredItems = [...items];
2818
2819 if (searchQuery) {
2820 filteredItems = filteredItems.filter(item =>
2821 item.title.toLowerCase().includes(searchQuery) ||
2822 item.url.toLowerCase().includes(searchQuery)
2823 );
2824 }
2825
2826 filteredItems = filteredItems.filter(item => {
2827 if (filterValue === 'mode-stock') return item.mode === 'stock';
2828 if (filterValue === 'mode-price') return item.mode === 'price';
2829 if (item.mode === 'stock') {
2830 const inC = item.sizes.filter(s => s.inStock).length;
2831 const total = item.sizes.length;
2832 if (filterValue === 'stock-in') return inC === total;
2833 if (filterValue === 'stock-out') return inC === 0;
2834 if (filterValue === 'stock-partial') return inC > 0 && inC < total;
2835 }
2836 return true;
2837 });
2838
2839 filteredItems.sort((a, b) => {
2840 if (sortValue === 'date-desc') return (b.addedAt || 0) - (a.addedAt || 0);
2841 if (sortValue === 'date-asc') return (a.addedAt || 0) - (b.addedAt || 0);
2842 if (sortValue === 'title-asc') return (a.title || '').localeCompare(b.title || '');
2843 if (sortValue === 'title-desc') return (b.title || '').localeCompare(a.title || '');
2844 if (sortValue === 'site-asc') {
2845 const da = new URL(a.url).hostname;
2846 const db = new URL(b.url).hostname;
2847 return da.localeCompare(db);
2848 }
2849 if (sortValue === 'price-asc' || sortValue === 'price-desc') {
2850 const pa = a.mode === 'price' ? (a.priceNow ?? 9e15) : parsePrice(a.price) || 9e15;
2851 const pb = b.mode === 'price' ? (b.priceNow ?? 9e15) : parsePrice(b.price) || 9e15;
2852 return sortValue === 'price-asc' ? (pa - pb) : (pb - pa);
2853 }
2854 if (sortValue === 'stock-desc' || sortValue === 'stock-asc') {
2855 const ca = a.mode === 'stock' ? a.sizes.filter(x => x.inStock).length : -1;
2856 const cb = b.mode === 'stock' ? b.sizes.filter(x => x.inStock).length : -1;
2857 return sortValue === 'stock-desc' ? (cb - ca) : (ca - cb);
2858 }
2859 return 0;
2860 });
2861
2862 if (filteredItems.length === 0) {
2863 listEl.innerHTML = '<p style="text-align:center;color:#999;padding:20px">לא נמצאו מוצרים מתאימים</p>';
2864 return;
2865 }
2866
2867 listEl.innerHTML = filteredItems.map(item => {
2868 if (item.mode === 'price') {
2869 return `<div class="st-item" data-item-id="${item.id}">
2870 ${item.image ? `<img src="${item.image}" style="width:60px;height:60px;object-fit:cover;border-radius:8px;margin-bottom:12px">` : ''}
2871 <div style="font-weight:700;margin-bottom:4px">${item.title}</div>
2872 <div style="color:#999;font-size:11px;margin-bottom:4px">🏪 ${getSiteName(item.url)}</div>
2873 <div class="st-tag">✈️ מצב: מחיר</div>
2874 <div class="st-tag">🎯 יעד: ${item.targetPrice ? `₪${item.targetPrice}` : '—'}</div>
2875 <div class="st-tag">מחיר נוכחי: ${item.priceNow != null ? `₪${item.priceNow}` : '—'}</div>
2876 <div class="st-tag">נוסף: ${formatTimeAgo(item.addedAt)}</div>
2877 ${item.lastChecked ? `<div class="st-tag">🔄 נבדק: ${formatTimeAgo(item.lastChecked)}</div>` : ''}
2878 <div class="st-button-grid">
2879 <button class="st-btn st-btn-secondary st-action-btn" data-action="check" data-id="${item.id}">🔍 בדוק</button>
2880 <button class="st-btn st-btn-secondary st-action-btn" data-action="open" data-url="${item.url}">🔗 פתח</button>
2881 <button class="st-btn st-btn-secondary st-action-btn" data-action="copy" data-url="${item.url}">📋 העתק</button>
2882 <button class="st-btn st-btn-secondary st-action-btn" data-action="share" data-id="${item.id}">📱 שתף</button>
2883 <button class="st-btn st-btn-secondary st-action-btn" data-action="remove" data-id="${item.id}">🗑️ מחק</button>
2884 </div>
2885 </div>`;
2886 } else {
2887 const inStockCount = item.sizes.filter(s => s.inStock).length;
2888 const totalCount = item.sizes.length;
2889 const siteName = getSiteName(item.url);
2890
2891 return `<div class="st-item" data-item-id="${item.id}">
2892 <div style="display:flex;gap:12px;margin-bottom:12px">
2893 ${item.image ? `<img src="${item.image}" style="width:60px;height:60px;object-fit:cover;border-radius:8px">` : ''}
2894 <div style="flex:1">
2895 <div style="font-weight:700;margin-bottom:4px">${item.title}</div>
2896 <div style="color:#999;font-size:11px;margin-bottom:4px">🏪 ${siteName}</div>
2897 <div class="st-tag">🧵 מצב: מלאי</div>
2898 <div class="st-tag">📊 ${inStockCount}/${totalCount} במלאי</div>
2899 <div class="st-tag">💰 ${item.price || 'לא זמין'}</div>
2900 <div class="st-tag">נוסף: ${formatTimeAgo(item.addedAt)}</div>
2901 ${item.sizes[0]?.lastChecked ? `<div class="st-tag">🔄 נבדק: ${formatTimeAgo(item.sizes[0].lastChecked)}</div>` : ''}
2902 </div>
2903 </div>
2904 <div style="margin:8px 0">
2905 ${item.sizes.map(s => `
2906 <span class="st-size ${s.inStock ? 'in-stock' : 'out-of-stock'}" title="לחץ לפתיחת המוצר" onclick="window.open('${item.url}', '_blank')">
2907 ${s.label} ${s.inStock ? '✅' : '❌'}
2908 </span>
2909 `).join('')}
2910 </div>
2911 <div class="st-button-grid">
2912 <button class="st-btn st-btn-secondary st-action-btn" data-action="check" data-id="${item.id}">🔍 בדוק</button>
2913 <button class="st-btn st-btn-secondary st-action-btn" data-action="open" data-url="${item.url}">🔗 פתח</button>
2914 <button class="st-btn st-btn-secondary st-action-btn" data-action="edit" data-id="${item.id}">✏️ ערוך</button>
2915 <button class="st-btn st-btn-secondary st-action-btn" data-action="copy" data-url="${item.url}">📋 העתק</button>
2916 <button class="st-btn st-btn-secondary st-action-btn" data-action="share" data-id="${item.id}">📱 שתף</button>
2917 <button class="st-btn st-btn-secondary st-action-btn" data-action="remove" data-id="${item.id}">🗑️ מחק</button>
2918 </div>
2919 </div>`;
2920 }
2921 }).join('');
2922
2923 listEl.querySelectorAll('.st-action-btn').forEach(btn => {
2924 btn.addEventListener('click', async function(e) {
2925 e.preventDefault();
2926 e.stopPropagation();
2927
2928 const action = this.getAttribute('data-action');
2929 const id = this.getAttribute('data-id');
2930 const url = this.getAttribute('data-url');
2931
2932 if (action === 'check') {
2933 await checkSingleItem(id);
2934 } else if (action === 'open') {
2935 window.open(url, '_blank');
2936 } else if (action === 'edit') {
2937 window.open(url, '_blank');
2938 } else if (action === 'copy') {
2939 await copyProductLink(url);
2940 } else if (action === 'share') {
2941 const items = await getItems();
2942 const item = items.find(i => i.id === id);
2943 if (item) await shareProductOnWhatsApp(item);
2944 } else if (action === 'remove') {
2945 await removeItem(id);
2946 }
2947 });
2948 });
2949
2950 // Render recommendations at the bottom
2951 await renderRecommendations();
2952 }
2953
2954 // Remove item
2955 async function removeItem(id) {
2956 if (!confirm('האם אתה בטוח שברצונך למחוק מוצר זה?')) return;
2957
2958 const items = await getItems();
2959 const filtered = items.filter(item => item.id !== id);
2960
2961 await saveItems(filtered);
2962 await renderList();
2963 await renderStats();
2964 await updateFabBadge();
2965 showToast('🗑️ מוצר נמחק בהצלחה', 'success');
2966 }
2967
2968 // Start background checking
2969 let checkInterval = null;
2970 async function startBackgroundCheck() {
2971 if (checkInterval) clearInterval(checkInterval);
2972 if (countdownInterval) clearInterval(countdownInterval);
2973
2974 const prefs = await getPrefs();
2975 const interval = (prefs.checkIntervalMin || 5) * 60 * 1000;
2976
2977 // Check if there's a saved next check time
2978 const savedNextCheck = await GM.getValue('next_check_time');
2979 if (savedNextCheck) {
2980 const savedTime = parseInt(savedNextCheck);
2981 const now = Date.now();
2982
2983 // If saved time is in the future, use it
2984 if (savedTime > now) {
2985 nextCheckTime = savedTime;
2986 console.log('[Stock Tracker] Restored next check time from storage');
2987 } else {
2988 // Saved time has passed, check now and set new time
2989 nextCheckTime = now + interval;
2990 await GM.setValue('next_check_time', nextCheckTime.toString());
2991 console.log('[Stock Tracker] Saved time passed, checking now...');
2992 setTimeout(() => checkAllStock(), 2000);
2993 }
2994 } else {
2995 // No saved time, set new one
2996 nextCheckTime = Date.now() + interval;
2997 await GM.setValue('next_check_time', nextCheckTime.toString());
2998 }
2999
3000 countdownInterval = setInterval(updateCountdown, 1000);
3001 updateCountdown();
3002
3003 checkInterval = setInterval(async () => {
3004 const now = Date.now();
3005 if (now >= nextCheckTime) {
3006 nextCheckTime = now + interval;
3007 await GM.setValue('next_check_time', nextCheckTime.toString());
3008 await checkAllStock();
3009 }
3010 }, 5000); // Check every 5 seconds if it's time to run
3011
3012 console.log('[Stock Tracker] Background check started. Interval:', interval / 60000, 'minutes');
3013 console.log('[Stock Tracker] Next check at:', new Date(nextCheckTime).toLocaleTimeString('he-IL'));
3014 }
3015
3016 // Update countdown display
3017 function updateCountdown() {
3018 const countdownEl = document.querySelector('#st-countdown-text');
3019 if (!countdownEl || !nextCheckTime) return;
3020
3021 const now = Date.now();
3022 const diff = nextCheckTime - now;
3023
3024 if (diff <= 0) {
3025 countdownEl.textContent = '00:00';
3026 return;
3027 }
3028
3029 const minutes = Math.floor(diff / 60000);
3030 const seconds = Math.floor((diff % 60000) / 1000);
3031
3032 countdownEl.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
3033 }
3034
3035 // Keep-alive mechanism to prevent tab from sleeping
3036 function keepAlive() {
3037 setInterval(() => {
3038 // Ping to keep the script active
3039 console.log('[Stock Tracker] Keep-alive ping:', new Date().toLocaleTimeString('he-IL'));
3040 }, 60000); // Every minute
3041 }
3042
3043 // Visibility change handler - resume checking when tab becomes visible
3044 function handleVisibilityChange() {
3045 document.addEventListener('visibilitychange', async () => {
3046 if (!document.hidden) {
3047 console.log('[Stock Tracker] Tab became visible, resuming...');
3048 const items = await getItems();
3049 if (items.length > 0) {
3050 await startBackgroundCheck();
3051 await updateFabBadge();
3052 }
3053 }
3054 });
3055 }
3056
3057 // Initialize
3058 async function init() {
3059 console.log('[Stock Tracker] Initializing...');
3060
3061 // Always run on WhatsApp for auto-send functionality
3062 if (location.host.includes('web.whatsapp.com')) {
3063 console.log('[Stock Tracker] WhatsApp detected - running auto-send');
3064 autoSendWhatsApp();
3065 return;
3066 }
3067
3068 if (isBlockedSite()) {
3069 console.log('[Stock Tracker] Blocked site detected');
3070 return;
3071 }
3072
3073 const items = await getItems();
3074 const isEcommerce = isEcommerceSite();
3075
3076 // Track page view for recommendations
3077 if (isEcommerce) {
3078 await trackPageView();
3079 }
3080
3081 // Check if we need to auto-add to cart
3082 await autoAddToCartOnLoad();
3083
3084 // Fix old selectors
3085 if (items.length > 0) {
3086 let needsSave = false;
3087 items.forEach(item => {
3088 if (item.mode === 'stock') {
3089 item.sizes.forEach(size => {
3090 if (typeof size.selector === 'string' &&
3091 (size.selector.includes('--disabled') ||
3092 size.selector.includes('--unavailable') ||
3093 size.selector.includes('--enabled'))) {
3094 size.selector = size.selector
3095 .replace(/\.size-selector-sizes-size--disabled/g, '')
3096 .replace(/\.size-selector-sizes-size--unavailable/g, '')
3097 .replace(/\.size-selector-sizes-size--enabled/g, '')
3098 .replace(/\.is-disabled/g, '')
3099 .replace(/\.is-unavailable/g, '')
3100 .replace(/\.disabled/g, '')
3101 .replace(/\.unavailable/g, '')
3102 .replace(/\.enabled/g, '');
3103 needsSave = true;
3104 }
3105 });
3106 }
3107 });
3108
3109 if (needsSave) {
3110 await saveItems(items);
3111 console.log('[Stock Tracker] Fixed old selectors');
3112 }
3113 }
3114
3115 if (items.length === 0 && !isEcommerce) {
3116 console.log('[Stock Tracker] Not an e-commerce site and no tracked items');
3117 return;
3118 }
3119
3120 if (!document.body) {
3121 await new Promise(resolve => {
3122 if (document.readyState === 'loading') {
3123 document.addEventListener('DOMContentLoaded', resolve);
3124 } else {
3125 resolve();
3126 }
3127 });
3128 }
3129
3130 createUI();
3131
3132 if (items.length > 0) {
3133 await startBackgroundCheck();
3134 keepAlive(); // Start keep-alive mechanism
3135 handleVisibilityChange(); // Handle tab visibility changes
3136 }
3137
3138 await updateFabBadge();
3139
3140 console.log('[Stock Tracker] Ready!');
3141 console.log('[Stock Tracker] 💡 TIP: Keep this tab open for automatic stock checking');
3142 }
3143
3144 // Share product on WhatsApp
3145 async function shareProductOnWhatsApp(item) {
3146 const prefs = await getPrefs();
3147
3148 let message = `🛍️ ${item.title}\n\n`;
3149
3150 if (item.mode === 'price') {
3151 message += `💰 מחיר: ${item.priceNow ? `₪${item.priceNow}` : 'לא זמין'}\n`;
3152 if (item.targetPrice) message += `🎯 יעד: ₪${item.targetPrice}\n`;
3153 } else {
3154 message += `💰 ${item.price}\n`;
3155 const inStock = item.sizes.filter(s => s.inStock);
3156 const outStock = item.sizes.filter(s => !s.inStock);
3157
3158 if (inStock.length > 0) {
3159 message += `\n✅ במלאי (${inStock.length}):\n`;
3160 message += inStock.map(s => `• ${s.label}`).join('\n');
3161 }
3162
3163 if (outStock.length > 0) {
3164 message += `\n\n❌ לא במלאי (${outStock.length}):\n`;
3165 message += outStock.map(s => `• ${s.label}`).join('\n');
3166 }
3167 }
3168
3169 message += `\n\n🔗 ${item.url}`;
3170
3171 openWhatsApp(message, prefs.waPhone);
3172 showToast('📱 פותח WhatsApp...', 'info');
3173 }
3174
3175 // Track page view for recommendations
3176 async function trackPageView() {
3177 try {
3178 const adapter = getAdapter();
3179 const title = adapter.title();
3180 const price = adapter.price();
3181 const image = adapter.image();
3182 const url = location.href.split('#')[0].split('?')[0];
3183
3184 if (!title || !url) return;
3185
3186 const viewHistory = await load('sw_plus_view_history', []);
3187
3188 // Check if this URL was already viewed recently (within 1 hour)
3189 const recentView = viewHistory.find(v => v.url === url && (Date.now() - v.timestamp) < 3600000);
3190 if (recentView) {
3191 recentView.viewCount++;
3192 recentView.timestamp = Date.now();
3193 } else {
3194 viewHistory.push({
3195 url,
3196 title,
3197 price: price ? `₪${price}` : 'לא זמין',
3198 image,
3199 siteName: getSiteName(url),
3200 timestamp: Date.now(),
3201 viewCount: 1
3202 });
3203 }
3204
3205 // Keep only last 50 views
3206 if (viewHistory.length > 50) {
3207 viewHistory.sort((a, b) => b.timestamp - a.timestamp);
3208 viewHistory.splice(50);
3209 }
3210
3211 await save('sw_plus_view_history', viewHistory);
3212 console.log('[Stock Tracker] Page view tracked:', title);
3213 } catch (err) {
3214 console.error('[Stock Tracker] Track view error:', err);
3215 }
3216 }
3217
3218 // Get recommended products based on view history
3219 async function getRecommendedProducts() {
3220 try {
3221 const viewHistory = await load('sw_plus_view_history', []);
3222 const items = await getItems();
3223
3224 // Filter out products already in tracking
3225 const trackedUrls = new Set(items.map(i => i.url));
3226 const recommendations = viewHistory
3227 .filter(v => !trackedUrls.has(v.url))
3228 .sort((a, b) => b.viewCount - a.viewCount)
3229 .slice(0, 6);
3230
3231 return recommendations;
3232 } catch (err) {
3233 console.error('[Stock Tracker] Get recommendations error:', err);
3234 return [];
3235 }
3236 }
3237
3238 // Render recommendations section
3239 async function renderRecommendations() {
3240 const listEl = document.querySelector('#st-list');
3241 if (!listEl) return;
3242
3243 const recommendations = await getRecommendedProducts();
3244
3245 if (recommendations.length === 0) return;
3246
3247 const recsHTML = `
3248 <div style="margin-top:32px;padding-top:24px;border-top:2px solid #333">
3249 <h3 style="margin-bottom:16px;color:#fbbf24;font-size:16px">⭐ מוצרים שצפית בהם</h3>
3250 <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px">
3251 ${recommendations.map(rec => `
3252 <div class="st-rec-card" data-url="${rec.url}" style="
3253 background:linear-gradient(135deg,#2a2a2a 0%,#252525 100%);
3254 border:1px solid #444;
3255 border-radius:12px;
3256 padding:12px;
3257 cursor:pointer;
3258 transition:all 0.3s;
3259 ">
3260 ${rec.image ? `<img src="${rec.image}" style="width:100%;height:120px;object-fit:cover;border-radius:8px;margin-bottom:8px">` : ''}
3261 <div style="font-weight:700;font-size:12px;margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${rec.title}</div>
3262 <div style="color:#999;font-size:10px;margin-bottom:4px">🏪 ${rec.siteName}</div>
3263 <div style="color:#fbbf24;font-size:11px;font-weight:700">${rec.price}</div>
3264 <div style="color:#666;font-size:10px;margin-top:4px">👁️ צפיות: ${rec.viewCount}</div>
3265 </div>
3266 `).join('')}
3267 </div>
3268 </div>
3269 `;
3270
3271 listEl.insertAdjacentHTML('beforeend', recsHTML);
3272
3273 // Add click handlers to recommendation cards
3274 document.querySelectorAll('.st-rec-card').forEach(card => {
3275 card.addEventListener('click', function() {
3276 const url = this.getAttribute('data-url');
3277 window.open(url, '_blank');
3278 });
3279
3280 card.addEventListener('mouseenter', function() {
3281 this.style.borderColor = '#25D366';
3282 this.style.transform = 'translateY(-2px)';
3283 this.style.boxShadow = '0 8px 24px rgba(37, 211, 102, 0.15)';
3284 });
3285
3286 card.addEventListener('mouseleave', function() {
3287 this.style.borderColor = '#444';
3288 this.style.transform = 'translateY(0)';
3289 this.style.boxShadow = 'none';
3290 });
3291 });
3292 }
3293
3294 // Check if dark mode should be enabled
3295 function shouldUseDarkMode() {
3296 const hour = new Date().getHours();
3297 return hour >= 20 || hour < 6; // 8 PM to 6 AM
3298 }
3299
3300 // Apply dark mode to panel
3301 function applyDarkMode() {
3302 const panel = document.querySelector('.st-panel');
3303 if (!panel) return;
3304
3305 if (shouldUseDarkMode()) {
3306 panel.style.background = 'linear-gradient(180deg, #0a0a0a 0%, #000000 100%)';
3307 panel.style.borderColor = '#222';
3308 } else {
3309 panel.style.background = 'linear-gradient(180deg, #1a1a1a 0%, #0a0a0a 100%)';
3310 panel.style.borderColor = '#333';
3311 }
3312 }
3313
3314 init().catch(err => console.error('[Stock Tracker] Init error:', err));
3315
3316})();