PageSourceSearch

https://bep40-vai-avatar.hf.space/big_sale.js?v=15

js bep40-vai-avatar.hf.space collected 2026-10-03 16:43:26 UTC 40,509 bytes, 776 lines download raw bytes

1// BIG SALE Floating Promotion Module v1.1
2// Integrates BIG SALE promotion button with product detail modal
3// Compatible with bep40/vai-avatar2 space architecture
4//
5// CHANGES v1.1:
6//  - Removed 6 BIG SALE products that have NO image and NO description in the
7//    catalog dataset (bep40/grob-products-updated): K136A, MST-6667,
8//    MOVS-10128, "BỘ LÀM SẠCH NHÀ BẾP", 901X, 642X. These fell back to emoji
9//    placeholders / empty body in the detail modal. 25 -> 19 products.
10//  - Fixed the share link. Previously it pointed to the production space
11//    https://bep40-vai-avatar.hf.space/?product=<slug>, which (a) does not load
12//    this big_sale module at all and (b) opens the catalog product at its
13//    LISTED price (no BIG SALE discount). Now the share link is self-hosted
14//    (?bigsale=<code> on the CURRENT page/origin), and a deep-link handler opens
15//    the BIG SALE detail modal with the correct BIG SALE (discounted) price.
16
17console.log('[BIG SALE] Module loaded (v1.1)');
18
19// BIG SALE Product Data — 19 products with discount info
20// (removed: K136A, MST-6667, MOVS-10128, BỘ LÀM SẠCH NHÀ BẾP, 901X, 642X)
21const DEFAULT_BIG_SALE_PRODUCTS = [
22  { stt: 1, code: "MC-7086HS",  name: "MÁY HÚT", category: "MÁY HÚT", qty: 3,  price: "11.389.000", discount: "45%", bigsale: "6.263.950", status: "HÀNG MỚI" },
23  { stt: 2, code: "MH-70SMC",  name: "MÁY HÚT ECO", category: "MÁY HÚT", qty: 2,  price: "4.620.000", discount: "50%", bigsale: "2.310.000", status: "HÀNG MỚI" },
24  { stt: 3, code: "MH-90SMC",  name: "MÁY HÚT ECO", category: "MÁY HÚT", qty: 4,  price: "5.060.000", discount: "50%", bigsale: "2.530.000", status: "HÀNG MỚI" },
25  { stt: 4, code: "MH-600GT",  name: "MÁY HÚT", category: "MÁY HÚT", qty: 5,  price: "8.208.000", discount: "40%", bigsale: "4.924.800", status: "HÀNG MỚI" },
26  { stt: 5, code: "AS-9602DG",  name: "BẾP 2 GAS", category: "BẾP GAS", qty: 2,  price: "7.815.000", discount: "40%", bigsale: "4.689.000", status: "HÀNG MỚI" },
27  { stt: 6, code: "SMART K-450",  name: "CHẬU ĐÁ", category: "CHẬU ĐÁ", qty: 1,  price: "21.109.000", discount: "45%", bigsale: "11.609.950", status: "HÀNG MỚI" },
28  { stt: 7, code: "MS-1022D",  name: "CHẬU RỰA", category: "CHẬU RỰA", qty: 4,  price: "5.852.000", discount: "40%", bigsale: "3.511.200", status: "HÀNG MỚI" },
29  { stt: 8, code: "MS-2025",  name: "CHẬU RỰA", category: "CHẬU RỰA", qty: 1,  price: "5.700.000", discount: "45%", bigsale: "3.135.000", status: "HÀNG MỚI" },
30  { stt: 9, code: "MS-6070",  name: "CHẬU RỰA", category: "CHẬU RỰA", qty: 11,  price: "6.464.000", discount: "40%", bigsale: "3.878.400", status: "HÀNG MỚI" },
31  { stt: 10, code: "MS-5066W",  name: "CHẬU RỰA", category: "CHẬU RỰA", qty: 8,  price: "4.385.000", discount: "40%", bigsale: "2.631.000", status: "HÀNG MỚI" },
32  { stt: 11, code: "MS-5082W",  name: "CHẬU RỰA", category: "CHẬU RỰA", qty: 3,  price: "6.350.000", discount: "40%", bigsale: "3.810.000", status: "HÀNG MỚI" },
33  { stt: 12, code: "MS-862",  name: "CHẬU RỰA", category: "CHẬU RỰA", qty: 3,  price: "12.960.000", discount: "45%", bigsale: "7.128.000", status: "HÀNG MỚI" },
34  { stt: 13, code: "MPC-5KCB",  name: "BỘ LỌC NƯỚC", category: "LỌC NƯỚC", qty: 1,  price: "10.702.000", discount: "40%", bigsale: "6.421.200", status: "HÀNG MỚI" },
35  { stt: 14, code: "K061S",  name: "VÒI", category: "VÒI", qty: 10,  price: "3.056.000", discount: "40%", bigsale: "1.833.600", status: "HÀNG MỚI" },
36  { stt: 15, code: "K569SN",  name: "VÒI ECO", category: "VÒI", qty: 2,  price: "3.520.000", discount: "45%", bigsale: "1.936.000", status: "HÀNG MỚI" },
37  { stt: 16, code: "K525B",  name: "VÒI", category: "VÒI", qty: 2,  price: "2.916.000", discount: "45%", bigsale: "1.603.800", status: "HÀNG MỚI" },
38  { stt: 17, code: "K130S",  name: "VÒI", category: "VÒI", qty: 4,  price: "4.104.000", discount: "45%", bigsale: "2.257.200", status: "HÀNG MỚI" },
39  { stt: 18, code: "K501B",  name: "VÒI", category: "VÒI", qty: 2,  price: "5.076.000", discount: "45%", bigsale: "2.791.800", status: "HÀNG MỚI" },
40  { stt: 19, code: "MMPM-657A",  name: "XAY TIÊU", category: "PHỤ KIỆN", qty: 34,  price: "295.000", discount: "60%", bigsale: "118.000", status: "HÀNG MỚI" }
41].map(function(p, i){ return Object.assign({}, p, { stt: i + 1 }); });
42
43// Runtime list (starts from DEFAULT, replaced by /api/promos when available).
44let BIG_SALE_PRODUCTS = DEFAULT_BIG_SALE_PRODUCTS.slice().map(function(p, i){ return Object.assign({}, p, { stt: i + 1 }); });
45
46// Expose BIG SALE data + opener so the self-hosted deep-link handler below and
47// any other module can open a BIG SALE product by code.
48window.BIG_SALE_PRODUCTS = BIG_SALE_PRODUCTS;
49
50// Format VND currency
51function formatVND(price) {
52  const num = parseInt(price.replace(/\./g, ''));
53  if (isNaN(num)) return price;
54  return new Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' }).format(num);
55}
56// Plain digit-group format WITHOUT currency symbol (for BIGSALE price fields).
57function fmtNum(n) {
58  const num = Number(n || 0);
59  if (!num) return '0';
60  return num.toLocaleString('vi-VN');
61}
62
63// Build a product card HTML string
64function buildProductCard(p) {
65  const discountClass = p.discount.includes('7') ? 'discount-high' : 'discount-mid';
66  // Try to get full product name from catalog
67  var fullName = p.name;
68  var imgSrc = p.image || '';
69  try {
70    if (window.vaix && typeof window.vaix.findProduct === 'function') {
71      var matched = window.vaix.findProduct(String(p.code || ''));
72      if (matched && (matched.title_clean || matched.name)) fullName = matched.title_clean || matched.name;
73      if (!imgSrc) imgSrc = matched && (matched.image || (matched.images && matched.images[0])) ? (matched.image || matched.images[0]) : '';
74    }
75  } catch(e) {}
76  return `
77    <div class="big-sale-product" data-code="${p.code}" data-stt="${p.stt}">
78      <div class="big-sale-product-header">
79        <span class="big-sale-stt">S${p.stt}</span>
80        <span class="big-sale-discount ${discountClass}">-${p.discount}</span>
81      </div>
82      ${imgSrc ? '<div class="big-sale-product-img" style="padding:2px 10px 0"><img src="'+imgSrc+'" alt="'+p.code+'" style="width:100%;height:96px;object-fit:cover;border-radius:10px;display:block;background:#f1f5f9" onerror="this.style.display=\'none\'"></div>' : ''}
83      <div class="big-sale-product-name">${fullName}</div>
84      <div class="big-sale-product-code">${p.code}</div>
85      <div class="big-sale-product-meta">
86        <span class="big-sale-qty">SL: ${p.qty}</span>
87        <span class="big-sale-status">HÀNG MỚI</span>
88      </div>
89      <div class="big-sale-price-row">
90        <span class="big-sale-price-original">${formatVND(p.price)}</span>
91        <span class="big-sale-price-new">${formatVND(p.bigsale)}</span>
92      </div>
93      <div class="big-sale-card-actions" id="bsa-${p.code}" style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap"></div>
94    </div>`;
95}
96
97// Create the floating button + panel
98function createBigSaleElements() {
99  // Check if already exists
100  if (document.getElementById('big-sale-floating')) return;
101
102  const floating = document.createElement('button');
103  floating.id = 'big-sale-floating';
104  floating.setAttribute('aria-label', 'Chương trình BIG SALE');
105  floating.setAttribute('title', 'Chương trình BIG SALE');
106  floating.innerHTML = `
107    <div class="big-sale-btn-content">
108      <span class="big-sale-badge">BIG SALE</span>
109      <span class="big-sale-count">${BIG_SALE_PRODUCTS.length} SP</span>
110    </div>
111    <div class="big-sale-chevron">▼</div>
112  `;
113
114  const panel = document.createElement('div');
115  panel.id = 'big-sale-panel';
116  panel.setAttribute('role', 'dialog');
117  panel.setAttribute('aria-label', 'Chương trình khuyến mãi BIG SALE');
118  panel.innerHTML = `
119    <div class="big-sale-panel-header">
120      <div class="big-sale-panel-title">
121        <span class="big-sale-panel-icon">🔥</span>
122        <span>CHƯƠNG TRÌNH BIG SALE</span>
123      </div>
124      <div style="display:flex;align-items:center;gap:8px">
125        <span class="big-sale-product-count">${BIG_SALE_PRODUCTS.length} sản phẩm</span>
126        <button type="button" class="big-sale-list-share-btn" onclick="event.stopPropagation();window.shareBigSaleList()" style="padding:5px 10px;border:none;border-radius:8px;background:#f97316;color:#fff;font-size:0.68rem;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap"><i class="fas fa-share-alt"></i> Chia sẻ danh sách</button>
127      </div>
128    </div>
129    <div class="big-sale-product-list">
130      ${BIG_SALE_PRODUCTS.map(p => buildProductCard(p)).join('')}
131    </div>
132    <div class="big-sale-panel-footer">
133      <button class="big-sale-view-all-btn">Xem tất cả sản phẩm</button>
134    </div>
135  `;
136
137    // Inject inline style for BIGSALE positioning
138  var es = document.getElementById('bigsale-pos-style');
139  if (!es) {
140    es = document.createElement('style');
141    es.id = 'bigsale-pos-style';
142    es.textContent = '#big-sale-floating{position:fixed!important;right:18px!important;bottom:100px!important;z-index:9999!important;}#big-sale-panel{display:none;}#big-sale-panel.open{display:flex!important;}';
143    document.head.appendChild(es);
144  }
145  document.body.appendChild(floating);
146  document.body.appendChild(panel);
147
148  attachBigSaleEventListeners(floating, panel);
149}
150
151// Event listeners
152function attachBigSaleEventListeners(floating, panel) {
153  let isOpen = false;
154
155  floating.addEventListener('click', function(e) {
156    e.stopPropagation();
157    isOpen = !isOpen;
158    panel.classList.toggle('open', isOpen);
159    floating.classList.toggle('open', isOpen);
160  });
161
162  // Close when clicking outside
163  document.addEventListener('click', function(e) {
164    if (isOpen && !panel.contains(e.target) && !floating.contains(e.target)) {
165      panel.classList.remove('open');
166      floating.classList.remove('open');
167      isOpen = false;
168    }
169  });
170
171  // Product click → open detail modal (event delegation, survives panel refresh)
172  panel.addEventListener('click', function(e) {
173    var card = e.target.closest('.big-sale-product');
174    if (!card) return;
175    e.stopPropagation();
176    var code = card.getAttribute('data-code');
177    if (!code) return;
178    var norm = function (s) { return String(s || '').trim().toUpperCase().replace(/\s+/g, ' '); };
179    var target = norm(code);
180    var product = BIG_SALE_PRODUCTS.find(function(p) { return norm(p.code) === target; });
181    if (!product && window.BIG_SALE_PRODUCTS) {
182      product = window.BIG_SALE_PRODUCTS.find(function(p) { return norm(p.code) === target; });
183    }
184    if (product) {
185      openBigSaleDetail(product);
186    }
187  });
188
189  // "Xem tất cả" button
190  const viewAllBtn = panel.querySelector('.big-sale-view-all-btn');
191  if (viewAllBtn) {
192    viewAllBtn.addEventListener('click', function(e) {
193      e.stopPropagation();
194      closeBigSalePanel();
195      if (window.vaistudioToggle && typeof window.vaistudioToggle === 'function') {
196        window.vaistudioToggle();
197      } else {
198        const toggle = document.getElementById('vaistudio-toggle');
199        if (toggle) toggle.click();
200      }
201    });
202  }
203}
204
205// Open vaistudio detail modal with BIG SALE product info
206function openBigSaleDetail(product) {
207  const overlay = document.getElementById('vaistudio-detail-overlay');
208  const modal = document.getElementById('vaistudio-detail-modal');
209  if (!overlay || !modal) return;
210  // ── BIG SALE → Malloca product match ──
211  // If this BIG SALE code exists in the live catalog, reuse the real product's
212  // gallery/specs/subtitle but keep the BIG SALE price. Falls back to the
213  // standalone BIG SALE card when there is no match.
214  let matched = null;
215  try { if (window.vaix && typeof window.vaix.findProduct === 'function' && product && product.code) matched = window.vaix.findProduct(String(product.code)); } catch (e) { matched = null; }
216
217  // Set product title and code
218  const titleEl = document.getElementById('detail-title');
219  const modelEl = document.getElementById('detail-model');
220  const brandEl = document.getElementById('detail-brand');
221  const priceEl = document.getElementById('detail-price');
222
223  if (titleEl) titleEl.textContent = matched ? (matched.title_clean || matched.name || product.name) : product.name;
224  if (modelEl) modelEl.textContent = `Mã: ${product.code}`;
225  if (brandEl) {
226    brandEl.textContent = matched ? (matched.brand || product.category) : product.category;
227    brandEl.style.background = '#fff1f2';
228    brandEl.style.color = '#e11d48';
229  }
230  if (priceEl) {
231    priceEl.innerHTML = `
232      <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
233        <span style="color:#c2410c;font-size:1.3rem;font-weight:800">${formatVND(product.bigsale)}</span>
234        <span style="font-size:0.8rem;color:#64748b;text-decoration:line-through">${formatVND(product.price)}</span>
235        <span style="font-size:0.85rem;background:#fef3c7;color:#92400e;padding:4px 10px;border-radius:8px;font-weight:700">-${product.discount}</span>
236      </div>
237    `;
238  }
239
240  // Set images: full gallery via renderDetailImages (scrollable + zoom)
241  const imagesEl = document.getElementById('detail-images');
242  if (imagesEl) {
243    const gal = (matched && (matched.images && matched.images.length ? matched.images : (matched.image ? [matched.image] : []))) || (product.image ? [product.image] : []);
244    const synthetic = { image: gal[0] || '', images: gal, slug: (matched&&matched.slug)||'', title_clean: (matched&&matched.title_clean)||product.name };
245    if (typeof renderDetailImages === 'function') {
246      renderDetailImages(synthetic);
247      if (window.vaix && typeof window.vaix.ensureGallery === 'function') {
248        setTimeout(function(){ try { window.vaix.ensureGallery().then(function(){ renderDetailImages(synthetic); }); } catch(e){} }, 200);
249      }
250    } else if (gal.length) {
251      imagesEl.innerHTML = gal.map(function(u){ return '<img src="'+u+'" alt="'+product.code+'" style="flex:1;min-width:120px;height:120px;border-radius:10px;object-fit:cover;border:1px solid #e2e8f0;background:#f1f5f9" onerror="this.style.display=\'none\'">'; }).join('');
252    } else {
253      imagesEl.innerHTML = '<div style="flex:1;min-width:120px;height:120px;background:linear-gradient(135deg,#f1f5f9,#e2e8f0);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:2.5rem">'+(getCategoryEmoji(product.category))+'</div>';
254    }
255  }
256
257  // Show BIG SALE pricing info in summary
258  const summaryEl = document.getElementById('detail-summary');
259  if (summaryEl) {
260    summaryEl.style.display = 'block';
261    summaryEl.innerHTML = `
262      <div style="margin-bottom:8px"><strong>🔥 Giá BigSale:</strong> ${formatVND(product.bigsale)}</div>
263      <div style="margin-bottom:8px"><strong>💰 Giá bán lẻ:</strong> ${formatVND(product.price)} → <span style="color:#c2410d;font-weight:700">${formatVND(product.bigsale)}</span></div>
264      <div style="margin-bottom:8px"><strong>🏷️ Chiết khấu:</strong> <span style="color:#dc2626;font-weight:700">-${product.discount}</span></div>
265      <div style="margin-bottom:8px"><strong>📦 Tình trạng:</strong> <span style="color:#16a34a;font-weight:600">${product.status}</span></div>
266      <div><strong>💡 Danh mục:</strong> ${product.category}</div>
267    `;
268  }
269
270  // Hide features and specs sections
271  const featuresEl = document.getElementById('detail-features');
272  const specsEl = document.getElementById('detail-specs');
273  if (featuresEl) featuresEl.style.display = 'none';
274  if (specsEl) specsEl.style.display = 'none';
275
276  // ── Action buttons: Chia sẻ / Hỏi AI / Thêm giỏ hàng / Báo giá ──
277  // BIG SALE products open through this modal but, unlike catalog products
278  // (renderDetail in vaix-rag.js), they were NOT being given the share /
279  // ask-AI / add-to-cart action row. Wire it here so every BIG SALE product
280  // gets the same actionable buttons as catalog products.
281  try { setupBigSaleDetailButtons(product, matched); } catch (e) { console.warn('[BIG SALE] detail buttons:', e); }
282
283  // CRITICAL: keep the BIG SALE price as THE detail price. The catalog modal's
284  // reapplyCk() re-renders #detail-price from window.lastShownProduct when the
285  // customer CK changes — if lastShownProduct were the matched catalog product
286  // (no promo flag), reapplyCk would overwrite the BIG SALE price with a CK
287  // price. Point it at the synthetic BIG SALE product (promo:'bigsale',
288  // noCk:true) so _ckPrice() returns null and the discount price survives.
289  window.lastShownProduct = bigSaleAsProduct(product, matched);
290  window.currentContextProduct = window.lastShownProduct;
291
292  overlay.style.display = 'flex';
293
294  // Close vaistudio panel if open
295  const vaistudioPanel = document.getElementById('vaistudio-panel');
296  if (vaistudioPanel && vaistudioPanel.classList.contains('open')) {
297    vaistudioPanel.classList.remove('open');
298    const toggle = document.getElementById('vaistudio-toggle');
299    if (toggle) toggle.classList.remove('open');
300  }
301}
302
303// Build a synthetic product object for the BIG SALE card so we can reuse the
304// catalog modal's own button helpers (share / ask-AI / add-to-cart / quote).
305function bigSaleAsProduct(product, matched) {
306  const priceNum = parseInt(String(product.bigsale || '').replace(/\./g, ''), 10) || 0;
307  const code = product.code || '';
308  const slug = (matched && (matched.slug || matched.sku)) || code;
309  return {
310    slug: slug,
311    sku: matched && (matched.sku || matched.model) ? (matched.sku || matched.model) : code,
312    model: (matched && matched.model) || '',
313    title_clean: (matched && (matched.title_clean || matched.name)) || product.name || code,
314    name: (matched && (matched.title_clean || matched.name)) || product.name || code,
315    brand: (matched && matched.brand) || product.category || '',
316    priceNum: priceNum,
317    price: product.bigsale || '',
318    image: (matched && (matched.image || (matched.images && matched.images[0]))) || '',
319    summary: (matched && (matched.summary || matched.description)) || ('🔥 Giá BIG SALE: ' + product.bigsale + ' (giảm ' + product.discount + ')'),
320    features: [],
321    specs: {},
322    noCk: true,
323    promo: 'bigsale'
324  };
325}
326
327// Build the self-hosted BIG SALE share link for a given BIG SALE product.
328// Points to the CURRENT page/origin with ?bigsale=<code> so it opens the BIG
329// SALE detail modal at the discounted price (NOT the catalog listed price and
330// NOT the unrelated production space).
331function bigSaleShareUrl(product) {
332  var base = window.location.origin + window.location.pathname;
333  return base + '?bigsale=' + encodeURIComponent((product && product.code) || '');
334}
335
336// Per-card "Chia sẻ link" (SEO share). Copies the self-hosted SEO link to the
337// clipboard and shows a toast. Exposed on window because the list cards are
338// built as HTML strings with inline onclick.
339window.shareBigSale = function (code) {
340  try {
341    var url = bigSaleShareUrl({ code: code });
342    var copy = function (u) {
343      if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
344        navigator.clipboard.writeText(u).then(function(){
345          if (typeof window.toast === 'function') window.toast('✓ Đã copy link chia sẻ BIG SALE');
346          else alert('Link chia sẻ BIG SALE (SEO):\n' + u);
347        }).catch(function(){ window.prompt('Link chia sẻ BIG SALE:', u); });
348      } else { window.prompt('Link chia sẻ BIG SALE:', u); }
349    };
350    copy(url);
351  } catch (err) {
352    alert('Lỗi chia sẻ: ' + (err && err.message || err));
353  }
354};
355
356// Create/re-ensure the share + add-to-cart + quote buttons in the BIG SALE
357// detail modal. These mirror the catalog modal's detail-cart-section /
358// detail-share-section so behaviour (addToCart / openQuotation / shareProduct /
359// openAskAIForProduct) is identical for BIG SALE products.
360function setupBigSaleDetailButtons(product, matched) {
361  const p = bigSaleAsProduct(product, matched);
362
363  // Share button
364  let shareSection = document.getElementById('detail-share-section');
365  if (!shareSection) {
366    shareSection = document.createElement('div');
367    shareSection.id = 'detail-share-section';
368    shareSection.style.cssText = 'padding:10px 16px 4px;display:block';
369    shareSection.innerHTML = '<button id="detail-share-btn" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:#f0f9ff;color:#003f62;border:1px solid #b8d4e8;border-radius:8px;
369cursor:pointer;font-size:0.78rem;font-family:inherit;font-weight:600;transition:all 0.2s"><i class="fas fa-share-alt"></i> Chia sẻ sản phẩm</button>';
370    const specsEl = document.getElementById('detail-specs');
371    if (specsEl && specsEl.nextSibling) specsEl.parentNode.insertBefore(shareSection, specsEl.nextSibling);
372    else document.getElementById('vaistudio-detail-modal').appendChild(shareSection);
373  }
374
375  // Cart + Quote buttons
376  let cartSection = document.getElementById('detail-cart-section');
377  if (!cartSection) {
378    cartSection = document.createElement('div');
379    cartSection.id = 'detail-cart-section';
380    cartSection.style.cssText = 'padding:8px 16px 2px;display:flex;gap:8px;flex-wrap:wrap';
381    cartSection.innerHTML =
382      '<button id="detail-addcart-btn" style="flex:1;min-width:140px;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:12px 16px;background:#25d366;color:#fff;border:none;border-radius:10px;cursor:pointer;font-size:0.85rem;font-family:inherit;font-weight:700;transition:all 0.2s"><i class="fas fa-shopping-cart"></i> Thêm giỏ hàng</button>'
383      + '<button id="detail-quote-btn" style="flex:1;min-width:120px;display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:12px 16px;background:#003f62;color:#fff;border:none;border-radius:10px;cursor:pointer;font-size:0.85rem;font-family:inherit;font-weight:700;transition:all 0.2s"><i class="fas fa-file-invoice"></i> Báo giá</button>';
384    const specsEl2 = document.getElementById('detail-specs');
385    if (specsEl2 && specsEl2.nextSibling) specsEl2.parentNode.insertBefore(cartSection, specsEl2.nextSibling);
386    else document.getElementById('vaistudio-detail-modal').appendChild(cartSection);
387  }
388
389  // Wire handlers
390  const shareBtn = document.getElementById('detail-share-btn');
391  if (shareBtn) {
392    shareBtn.onclick = function (e) {
393      e.stopPropagation();
394      try {
395        // Build the share text + self-hosted BIG SALE link ourselves (not the
396        // catalog's getShareLink which would point to the listed-price deep link).
397        var url = bigSaleShareUrl(product);
398        var shareText = '🛒 ' + p.title_clean + '\n💰 ' + (p.priceNum > 0 ? p.priceNum.toLocaleString('vi-VN') + '₫' : 'Liên hệ');
399        if (p.brand) shareText += '\n🏷️ ' + p.brand;
400        shareText += '\n🔥 Giá BIG SALE: ' + (product.bigsale || '') + ' (-' + (product.discount || '') + ')';
401        shareText += '\n\n' + url;
402        if (navigator.share) { navigator.share({ title: p.title_clean, text: shareText, url: url }).catch(function () {}); }
403        else { navigator.clipboard.writeText(shareText).then(function () { (window.showShareToast || function () {})( 'Đã sao chép link chia sẻ: ' + p.title_clean); }).catch(function () { prompt('Sao chép link chia sẻ:', url); }); }
404      } catch (e) { console.warn('[BIG SALE] share:', e); }
405    };
406  }
407
408  const addCartBtn = document.getElementById('detail-addcart-btn');
409  if (addCartBtn) {
410    addCartBtn.onclick = function (e) {
411      e.stopPropagation();
412      const prodObj = { slug: p.slug || '', sku: p.sku || '', name: p.title_clean || p.name || '', brand: p.brand || '', price: p.priceNum, priceNum: p.priceNum, image: p.image || '', noCk: true, promo: 'bigsale' };
413      if (window.addToCart) window.addToCart(prodObj);
414      addCartBtn.innerHTML = '<i class="fas fa-check"></i> Đã thêm!';
415      addCartBtn.style.background = '#166534';
416      const self = this;
417      setTimeout(function () { self.innerHTML = '<i class="fas fa-shopping-cart"></i> Thêm giỏ hàng'; self.style.background = '#25d366'; }, 1500);
418    };
419  }
420
421  const quoteBtn = document.getElementById('detail-quote-btn');
422  if (quoteBtn) { quoteBtn.onclick = function (e) { e.stopPropagation(); if (window.openQuotation) window.openQuotation(); }; }
423
424  // Hỏi AI — ask the avatar about THIS product (mirrors catalog openAskAIForProduct).
425  let askSection = document.getElementById('detail-asksale-section');
426  if (!askSection) {
427    askSection = document.createElement('div');
428    askSection.id = 'detail-asksale-section';
429    askSection.style.cssText = 'padding:2px 16px 10px;display:block';
430    askSection.innerHTML = '<button id="detail-asksale-btn" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;background:linear-gradient(135deg,#7c3aed,#a78bfa);color:#fff;border:none;border-radius:8px;
430cursor:pointer;font-size:0.78rem;font-family:inherit;font-weight:600;transition:all 0.2s"><i class="fas fa-robot"></i> Hỏi AI về sản phẩm</button>';
431    const cartSectionEl = document.getElementById('detail-cart-section');
432    if (cartSectionEl && cartSectionEl.nextSibling) askSection = cartSectionEl.parentNode.insertBefore(askSection, cartSectionEl.nextSibling);
433    else document.getElementById('vaistudio-detail-modal').appendChild(askSection);
434  }
435  const askBtn = document.getElementById('detail-asksale-btn');
436  if (askBtn) {
437    askBtn.onclick = function (e) {
438      e.stopPropagation();
439      if (window.vaix && window.vaix.openAskAIForProduct) {
440        window.vaix.openAskAIForProduct(p);
441      } else {
442        const q = 'Cho em hỏi chi tiết về sản phẩm ' + p.title_clean + '. Cho em biết giá, đặc điểm nổi bật và ưu đãi BIG SALE nhé.';
443        if (window.sendAsUserQuestion) window.sendAsUserQuestion(q);
444        else if (window._triggerSuggestionSend) window._triggerSuggestionSend(q);
445      }
446    };
447  }
448}
449
450// Get emoji for category
451function getCategoryEmoji(category) {
452  const map = {
453    "MÁY HÚT": "🧹",
454    "BẾP GAS": "🔥",
455    "CHẬU ĐÁ": "🧊",
456    "CHẬU RỰA": "🚰",
457    "LỌC NƯỚC": "💧",
458    "VÒI": "🚿",
459    "PHỤ KIỆN": "🧰"
460  };
461  return map[category] || "📦";
462}
463
464// Close BIG SALE panel
465function closeBigSalePanel() {
466  const panel = document.getElementById('big-sale-panel');
467  const floating = document.getElementById('big-sale-floating');
468  if (panel) panel.classList.remove('open');
469  if (floating) floating.classList.remove('open');
470}
471
472// ── Self-hosted BIG SALE deep link (?bigsale=<code>) ──
473// When a shared BIG SALE link is opened, open the BIG SALE detail modal at the
474// discounted price. Waits for the detail modal DOM + this module to be ready,
475// then opens the product. Uses ?bigsale= (not ?product=) so it never collides
476// with the catalog deep-link handler in greeting-news.js/vaix-rag.js.
477function dismissWelcomeForBigSale() {
478  try {
479    var wm = document.getElementById('welcome-modal');
480    if (wm) { wm.classList.remove('show'); wm.style.display = 'none'; }
481    var ids = ['topbar','controls','subtitles','vaistudio-toggle','text-chat'];
482    for (var i = 0; i < ids.length; i++) {
483      var el = document.getElementById(ids[i]);
484      if (el) { el.style.display = ''; el.hidden = false; }
485    }
486    var ld = document.getElementById('loading');
487    if (ld) ld.classList.add('done');
488    var av = document.getElementById('avatar-picker-modal');
489    if (av) { av.classList.remove('show'); av.style.display = 'none'; }
490    try {
491      var panel = document.getElementById('vaistudio-panel');
492      var toggle = document.getElementById('vaistudio-toggle');
493      if (panel) { panel.classList.add('open'); panel.style.display = 'flex'; }
494      if (toggle) toggle.classList.add('active');
495    } catch (_e) {}
496  } catch (e) { /* never block */ }
497}
498
499function handleBigSaleUrlParam() {
500  try {
501    var params = new URLSearchParams(location.search);
502    var code = params.get('bigsale') || '';
503    if (!code) return;
504    // ?bigsale=all → open the FULL list (not a single product).
505    var codeNorm = String(code).trim().toUpperCase().replace(/\s+/g, ' ');
506    if (codeNorm === 'ALL' || codeNorm === 'DANHSACH' || codeNorm === 'LIST') {
507      var triesAll = 0;
508      var ivAll = setInterval(function () {
509        triesAll++;
510        try {
511          if (document.getElementById('big-sale-floating')) {
512            if (!document.getElementById('big-sale-panel')) initBigSale();
513            dismissWelcomeForBigSale();
514            openBigSalePanel();
515            clearInterval(ivAll);
516            return;
517          }
518        } catch (e) {}
519        if (triesAll > 40) clearInterval(ivAll);
520      }, 500);
521      return;
522    }
523    var tries = 0;
524    var iv = setInterval(function () {
525      tries++;
526      try {
527        var modal = document.getElementById('vaistudio-detail-modal');
528        // Normalize the code (uppercase, collapse spaces) for matching.
529        var norm = function (s) { return String(s || '').trim().toUpperCase().replace(/\s+/g, ' '); };
530        var target = norm(code);
531        var product = BIG_SALE_PRODUCTS.find(function (p) { return norm(p.code) === target; });
532        if (!product && window.BIG_SALE_PRODUCTS) {
533          product = window.BIG_SALE_PRODUCTS.find(function (p) { return norm(p.code) === target; });
534        }
535        if (modal && product && typeof openBigSaleDetail === 'function') {
536          // Make sure the floating panel/module is initialized (creates panel).
537          if (!document.getElementById('big-sale-floating')) initBigSale();
538          dismissWelcomeForBigSale();
539          openBigSaleDetail(product);
540          try {
541            if (window.vaix && typeof window.vaix.openAskAIForProduct === 'function') {
542              const matchedBS = (window.vaix && typeof window.vaix.findProduct === 'function') ? window.vaix.findProduct(String(code)) : null;
543              const p = (typeof bigSaleAsProduct === 'function') ? bigSaleAsProduct(product, matchedBS) : null;
544              if (p) window.vaix.openAskAIForProduct(p);
545            }
546          } catch(e) {}
547          clearInterval(iv);
548          return;
549        }
550      } catch (e) {
551        if (tries > 40) clearInterval(iv);
552      }
553      if (tries > 60) clearInterval(iv); // give up after ~30s (catalog may be slow)
554    }, 500);
555  } catch (e) { /* never block */ }
556}
557
558// Ensure the BIGSALE floating button shows even on discount-link pages (?kh=...)
559function ensureBigSaleButton() {
560  var existing = document.getElementById('big-sale-floating');
561  if (existing) {
562    existing.style.display = '';
563    var panel = document.getElementById('big-sale-panel');
564    if (panel && panel.classList.contains('open')) { panel.style.display = ''; }
565    return;
566  }
567  if (typeof initBigSale === 'function') {
568    try { initBigSale(); } catch(e) {}
569    return;
570  }
571  var _retry = 0;
572  var _iv = setInterval(function() {
573    _retry++;
574    if (typeof initBigSale === 'function') {
575      clearInterval(_iv);
576      try { initBigSale(); } catch(e) {}
577    } else if (_retry > 30) {
578      clearInterval(_iv);
579    }
580  }, 300);
581}
582window.ensureBigSaleButton = ensureBigSaleButton;
583
584// Initialize when DOM is ready
585// ── Dynamic BIG SALE data (from Promo CMS /api/promos) ──
586// Replaces the runtime list with admin-managed BIGSALE products. Falls back to
587// DEFAULT_BIG_SALE_PRODUCTS when the API is unavailable. Emits an event so the
588// panel can re-render without a page reload.
589function loadBigSaleProducts(force) {
590  if (force !== true && _bigsaleLoaded) return Promise.resolve(BIG_SALE_PRODUCTS);
591  return fetch('/api/promos', { method: 'GET', cache: 'no-store' })
592    .then(function (r) {
593      if (!r.ok) throw new Error('HTTP ' + r.status);
594      return r.json();
595    })
596    .then(function (d) {
597      var list = (d && Array.isArray(d.bigsale)) ? d.bigsale : null;
598      if (list && list.length) {
599        BIG_SALE_PRODUCTS = list.slice().map(function (p, i) { return Object.assign({}, p, { stt: i + 1 }); });
600      } else if (list && list.length === 0 && force === true) {
601        // Admin explicitly emptied the BIGSALE list — respect it.
602        BIG_SALE_PRODUCTS = [];
603      }
604      // Auto-merge catalog products whose discount is >= 40% (BIGSALE tier).
605      // Tagged _auto:true so they are NOT deletable via the promo API.
606      try {
607        var cat = (window.vaix && typeof window.vaix.allProducts === 'function') ? window.vaix.allProducts() : [];
608        var normCode = function (s) { return String(s || '').trim().toUpperCase().replace(/\s+/g, ' '); };
609        var have = {};
610        for (var h = 0; h < BIG_SALE_PRODUCTS.length; h++) { var hk = normCode(BIG_SALE_PRODUCTS[h].code) || normCode(BIG_SALE_PRODUCTS[h].name); if (hk) have[hk] = 1; }
611        var added = 0;
612        for (var ci = 0; ci < cat.length && added < 30; ci++) {
613          var p2 = cat[ci];
614          if (p2._deleted || p2.deleted) continue;
615          var ln = Number(p2.listPn) || (p2.listPrice ? parseInt(String(p2.listPrice).replace(/[^\d]/g, ''), 10) : 0) || 0;
616          var sn = Number(p2.salePn) || (p2.salePrice ? parseInt(String(p2.salePrice).replace(/[^\d]/g, ''), 10) : 0) || Number(p2.priceNum) || 0;
617          if (!ln || !sn || ln <= sn) continue;
618          var disc = Math.round((ln - sn) / ln * 100);
619          if (disc < 40) continue;
620          var ck = normCode(p2.sku || p2.model || p2.code || '');
621          if (!ck) ck = normCode(p2.title_clean || p2.name || '');
622          if (!ck || have[ck]) continue;
623          have[ck] = 1;
624          BIG_SALE_PRODUCTS.push({
625            stt: BIG_SALE_PRODUCTS.length + 1, code: p2.sku || p2.model || p2.code || ck,
626            name: (p2.title_clean || p2.name || '').toUpperCase(), category: p2.category || p2.category_slug || '',
627            price: fmtNum(ln), bigsale: fmtNum(sn), discount: disc + '%', status: 'AUTO', image: p2.image || (p2.images && p2.images[0]) || '',
628            _auto: true
629          });
630          added++;
631        }
632      } catch (e) {}
633      window.BIG_SALE_PRODUCTS = BIG_SALE_PRODUCTS;
634      _bigsaleLoaded = true;
635      refreshBigSalePanel();
636      return BIG_SALE_PRODUCTS;
637    })
638    .catch(function () {
639      return BIG_SALE_PRODUCTS;
640    });
641}
642let _bigsaleLoaded = false;
643
644// Re-render the floating panel in place (preserve open state).
645function refreshBigSalePanel() {
646  try {
647    var panel = document.getElementById('big-sale-panel');
648    var floating = document.getElementById('big-sale-floating');
649    if (!panel || !floating) return;
650    var wasOpen = panel.classList.contains('open');
651    var listEl = panel.querySelector('.big-sale-product-list');
652    var countEl = panel.querySelector('.big-sale-product-count');
653    var countBadge = floating.querySelector('.big-sale-count');
654    if (listEl) listEl.innerHTML = BIG_SALE_PRODUCTS.map(function (p) { return buildProductCard(p); }).join('');
655    if (countEl) countEl.textContent = BIG_SALE_PRODUCTS.length + ' sản phẩm';
656    if (countBadge) countBadge.textContent = BIG_SALE_PRODUCTS.length + ' SP';
657    if (wasOpen) panel.classList.add('open'); else panel.classList.remove('open');
658    injectBigSaleAdminActions();
659  } catch (e) {}
660}
661
662// ── List-level share (toàn bộ danh sách BIG SALE) ──
663// Copies a self-hosted deep link (?bigsale=all) that opens the FULL BIG SALE
664// panel — NOT a single product. Replaces the old per-product share.
665window.shareBigSaleList = function () {
666  var base = window.location.origin + window.location.pathname;
667  var url = base + '?bigsale=all';
668  var done = function () {
669    if (typeof window.toast === 'function') window.toast('✓ Đã copy link chia sẻ danh sách BIG SALE');
670    else alert('Link chia sẻ danh sách BIG SALE:\n' + url);
671  };
672  if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
673    navigator.clipboard.writeText(url).then(done).catch(function () { window.prompt('Link chia sẻ danh sách BIG SALE:', url); });
674  } else { window.prompt('Link chia sẻ danh sách BIG SALE:', url); }
675};
676
677// Open the full BIG SALE panel (used by ?bigsale=all deep link).
678function openBigSalePanel() {
679  try {
680    var panel = document.getElementById('big-sale-panel');
681    var floating = document.getElementById('big-sale-floating');
682    if (panel) panel.classList.add('open');
683    if (floating) floating.classList.add('open');
684  } catch (e) {}
685}
686window.openBigSalePanel = openBigSalePanel;
687
688// Admin-only delete for a BIG SALE item (permanent, via /api/delete-promo).
689async function deleteBigSaleProduct(code) {
690  try {
691    var isAdmin = (typeof window.isVaAdmin === 'function') ? window.isVaAdmin() : false;
692    if (!isAdmin) { alert('🔒 Cần nhập mã V.AISTUDIO trên trang chủ.'); return; }
693    if (!window.confirm('Xóa sản phẩm ' + code + ' khỏi danh sách BIG SALE?')) return;
694    var r = await fetch('/api/delete-promo', {
695      method: 'POST',
696      headers: { 'Content-Type': 'application/json', 'x-vai-admin': '1' },
697      body: JSON.stringify({ type: 'bigsale', key: code })
698    });
699    var d = await r.json().catch(function () { return {}; });
700    if (!r.ok) { alert('❌ ' + (d.error || 'Lỗi xóa')); return; }
701    if (window.toast) window.toast('✓ Đã xóa khỏi BIG SALE');
702    loadBigSaleProducts(true);
703  } catch (e) { alert('❌ Lỗi xóa: ' + (e && e.message || e)); }
704}
705window.deleteBigSaleProduct = deleteBigSaleProduct;
706
707// Inject admin delete buttons into every BIG SALE card (after each render).
708function injectBigSaleAdminActions() {
709  try {
710    var isAdmin = (typeof window.isVaAdmin === 'function') ? window.isVaAdmin() : false;
711    if (!isAdmin) return;
712    var panel = document.getElementById('big-sale-panel');
713    if (!panel) return;
714    var norm = function (s) { return String(s || '').trim().toUpperCase().replace(/\s+/g, ' '); };
715    var cards = panel.querySelectorAll('.big-sale-product');
716    for (var i = 0; i < cards.length; i++) {
717      var card = cards[i];
718      var code = card.getAttribute('data-code') || '';
719      if (!code) continue;
720      // Auto-merged catalog items are NOT admin-managed → no delete button.
721      var isAuto = false;
722      for (var k = 0; k < BIG_SALE_PRODUCTS.length; k++) {
723        if (norm(BIG_SALE_PRODUCTS[k].code) === norm(code)) { isAuto = !!BIG_SALE_PRODUCTS[k]._auto; break; }
724      }
725      if (isAuto) continue;
726      var actions = card.querySelector('.big-sale-card-actions');
727      if (!actions || actions.querySelector('.bs-del-btn')) continue;
728      var del = document.createElement('button');
729      del.type = 'button';
730      del.className = 'bs-del-btn';
731      del.innerHTML = '<i class="fas fa-trash"></i> Xóa';
732      del.style.cssText = "flex:1;padding:6px 8px;border:none;border-radius:8px;background:#dc2626;color:#fff;font-size:0.7rem;font-weight:700;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px";
733      del.addEventListener('click', function (e) {
734        e.stopPropagation();
735        window.deleteBigSaleProduct(code);
736      });
737      actions.appendChild(del);
738    }
739  } catch (e) {}
740}
741
742function initBigSale() {
743  createBigSaleElements();
744  console.log('[BIG SALE] Floating promotion initialized with ' + BIG_SALE_PRODUCTS.length + ' products');
745  // Load admin-managed BIGSALE products from the Promo CMS backend.
746  loadBigSaleProducts().then(function (list) {
747    console.log('[BIG SALE] Dynamic list loaded: ' + list.length + ' products');
748    injectBigSaleAdminActions();
749    handleBigSaleUrlParam();
750  }).catch(function () {
751    injectBigSaleAdminActions();
752    handleBigSaleUrlParam();
753  });
754  // Listen for CMS saves so the panel stays in sync.
755  try {
756    window.addEventListener('vai-promo-changed', function () { loadBigSaleProducts(true).then(function () { injectBigSaleAdminActions(); }); });
757  } catch (e) {}
758}
759
760// Handle dynamic DOM changes — HEAVY module: deferred until a mode is picked.
761function tryInitBigSale(retries = 20) {
762  if (window.VAI_deferModule) {
763    // bigsale deep-links (?bigsale=...) still load immediately via the URL param
764    var deep = /[?&]bigsale=/.test(location.search);
765    if (deep) { window.VAI_requireNow(initBigSale); return; }
766    window.VAI_deferModule(initBigSale);
767    return;
768  }
769  if (document.readyState === 'loading') {
770    document.addEventListener('DOMContentLoaded', () => initBigSale());
771  } else {
772    initBigSale();
773  }
774}
775
776tryInitBigSale();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.