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.