bep40 commited on
Commit
f350419
·
1 Parent(s): 7c94cd2

Thêm SP: hiện đủ sp đã thêm vào đầu panel (URL+OCR); nút Xóa vĩnh viễn (/api/delete-product) cho sp đã thêm; nút chia sẻ link SEO trên card, BIGSALE & COMBO KHUYẾN MÃI

Browse files
Files changed (5) hide show
  1. big_sale.js +23 -0
  2. index.html +11 -11
  3. index.ts +71 -0
  4. src/vaix-combo.js +31 -2
  5. src/vaix-rag.js +117 -2
big_sale.js CHANGED
@@ -84,6 +84,9 @@ function buildProductCard(p) {
84
  <span class="big-sale-price-original">${formatVND(p.price)}</span>
85
  <span class="big-sale-price-new">${formatVND(p.bigsale)}</span>
86
  </div>
 
 
 
87
  </div>`;
88
  }
89
 
@@ -323,6 +326,26 @@ function bigSaleShareUrl(product) {
323
  return base + '?bigsale=' + encodeURIComponent((product && product.code) || '');
324
  }
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  // Create/re-ensure the share + add-to-cart + quote buttons in the BIG SALE
327
  // detail modal. These mirror the catalog modal's detail-cart-section /
328
  // detail-share-section so behaviour (addToCart / openQuotation / shareProduct /
 
84
  <span class="big-sale-price-original">${formatVND(p.price)}</span>
85
  <span class="big-sale-price-new">${formatVND(p.bigsale)}</span>
86
  </div>
87
+ <button type="button" class="big-sale-share-btn" onclick="window.shareBigSale('${p.code}')">
88
+ <i class="fas fa-share-alt"></i> Chia sẻ link
89
+ </button>
90
  </div>`;
91
  }
92
 
 
326
  return base + '?bigsale=' + encodeURIComponent((product && product.code) || '');
327
  }
328
 
329
+ // Per-card "Chia sẻ link" (SEO share). Copies the self-hosted SEO link to the
330
+ // clipboard and shows a toast. Exposed on window because the list cards are
331
+ // built as HTML strings with inline onclick.
332
+ window.shareBigSale = function (code) {
333
+ try {
334
+ var url = bigSaleShareUrl({ code: code });
335
+ var copy = function (u) {
336
+ if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
337
+ navigator.clipboard.writeText(u).then(function(){
338
+ if (typeof window.toast === 'function') window.toast('✓ Đã copy link chia sẻ BIG SALE');
339
+ else alert('Link chia sẻ BIG SALE (SEO):\n' + u);
340
+ }).catch(function(){ window.prompt('Link chia sẻ BIG SALE:', u); });
341
+ } else { window.prompt('Link chia sẻ BIG SALE:', u); }
342
+ };
343
+ copy(url);
344
+ } catch (err) {
345
+ alert('Lỗi chia sẻ: ' + (err && err.message || err));
346
+ }
347
+ };
348
+
349
  // Create/re-ensure the share + add-to-cart + quote buttons in the BIG SALE
350
  // detail modal. These mirror the catalog modal's detail-cart-section /
351
  // detail-share-section so behaviour (addToCart / openQuotation / shareProduct /
index.html CHANGED
@@ -758,17 +758,17 @@
758
  first paint — the welcome/avatar-picker modal renders immediately. -->
759
  <script defer src="./src/cart-quote.js?gc=33"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
760
  <script defer src="./src/customers.js?gc=17"></script> <!-- Customer list (Danh sách khách hàng) -->
761
- <script defer src="./src/vaix-rag.js?gc=55"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
762
- <script defer src="./src/greeting-news.js?gc=40"></script> <!-- Greeting HOT news cards + source links -->
763
- <script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
764
- <script type="module" src="./src/app.js?gc=58"></script>
765
- <script defer src="./src/greeting-source-cards.js?gc=15"></script> <!-- FIX: Nguồn tin cards under greeting -->
766
- <script defer src="./src/avatar-picker.js?gc=13"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
767
- <!-- BIG SALE Floating Promotion Module -->
768
- <script defer src="./big_sale.js?v=12"></script>
769
- <!-- V.AI STUDIO Promo CMS (admin editor + BIGSALE/COMBO manager) -->
770
- <script defer src="./src/vaix-promo-cms.js?v=3"></script>
771
- <script defer src="./src/vaix-combo.js?v=2"></script>
772
 
773
  <script>
774
  // Wire the "Đơn hàng" buttons to the order modal
 
758
  first paint — the welcome/avatar-picker modal renders immediately. -->
759
  <script defer src="./src/cart-quote.js?gc=33"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
760
  <script defer src="./src/customers.js?gc=17"></script> <!-- Customer list (Danh sách khách hàng) -->
761
+ <script defer src="./src/vaix-rag.js?gc=56"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
762
+ <script defer src="./src/greeting-news.js?gc=40"></script> <!-- Greeting HOT news cards + source links -->
763
+ <script defer src="./src/order-sync.js?gc=17"></script> <!-- Order sync with V.AISTUDIO backend -->
764
+ <script type="module" src="./src/app.js?gc=59"></script>
765
+ <script defer src="./src/greeting-source-cards.js?gc=15"></script> <!-- FIX: Nguồn tin cards under greeting -->
766
+ <script defer src="./src/avatar-picker.js?gc=13"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
767
+ <!-- BIG SALE Floating Promotion Module -->
768
+ <script defer src="./big_sale.js?v=13"></script>
769
+ <!-- V.AI STUDIO Promo CMS (admin editor + BIGSALE/COMBO manager) -->
770
+ <script defer src="./src/vaix-promo-cms.js?v=3"></script>
771
+ <script defer src="./src/vaix-combo.js?v=3"></script>
772
 
773
  <script>
774
  // Wire the "Đơn hàng" buttons to the order modal
index.ts CHANGED
@@ -1604,6 +1604,77 @@ const server = Bun.serve({
1604
  }
1605
  },
1606
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1607
  "/api/promos": {
1608
  GET: async () => {
1609
  try {
 
1604
  }
1605
  },
1606
  },
1607
+ "/api/delete-product": {
1608
+ POST: async (req: Request) => {
1609
+ // Admin-only: permanently remove a product the admin added (via URL/OCR).
1610
+ try {
1611
+ const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
1612
+ if (!token) return Response.json({ error: "No write token on server" }, { status: 500 });
1613
+ const isAdmin = (req.headers.get("x-vai-admin") || "") === "1";
1614
+ if (!isAdmin) return Response.json({ error: "Admin unlock required" }, { status: 403 });
1615
+ const body: any = await req.json().catch(() => ({}));
1616
+ const sku = String(body?.sku || "").trim();
1617
+ const slug = String(body?.slug || "").trim();
1618
+ if (!sku && !slug) return Response.json({ error: "Missing sku/slug" }, { status: 400 });
1619
+ const CAT_DATASET = "bep40/grob-products-updated";
1620
+ const CAT_FILE = "products_url_added.json";
1621
+
1622
+ // 1) Remove from products_url_added.json (the permanent shared file).
1623
+ let list: any[] = [];
1624
+ let hadAdded = false;
1625
+ try {
1626
+ const ra = await fetch("https://huggingface.co/datasets/" + CAT_DATASET + "/resolve/main/" + CAT_FILE, { signal: AbortSignal.timeout(10000) });
1627
+ if (ra.ok) { const d = await ra.json(); if (Array.isArray(d)) list = d; }
1628
+ } catch (_e) {}
1629
+ const SKU = sku.toUpperCase();
1630
+ const prevCount = list.length;
1631
+ list = list.filter((x) => {
1632
+ if (!x) return false;
1633
+ const mSku = String(x.sku || "").toUpperCase();
1634
+ const mSlug = String(x.slug || "").toLowerCase();
1635
+ return !((SKU && mSku === SKU) || (slug && mSlug === slug.toLowerCase()));
1636
+ });
1637
+ hadAdded = list.length !== prevCount;
1638
+ if (hadAdded) {
1639
+ const content = JSON.stringify(list);
1640
+ const payload = [
1641
+ { key: "header", value: { summary: "Delete added product from avatar2 admin", repo: { type: "dataset", id: CAT_DATASET } } },
1642
+ { key: "file", value: { path: CAT_FILE, content } },
1643
+ ].map((s) => JSON.stringify(s)).join("\n");
1644
+ const commit = await fetch("https://huggingface.co/api/datasets/" + CAT_DATASET + "/commit/main", {
1645
+ method: "POST",
1646
+ headers: { Authorization: "Bearer " + token, "Content-Type": "application/x-ndjson" },
1647
+ body: payload, signal: AbortSignal.timeout(20000),
1648
+ });
1649
+ if (!commit.ok) return Response.json({ error: "Dataset commit failed (" + commit.status + ")" }, { status: 502 });
1650
+ }
1651
+
1652
+ // 2) Mirror-remove from promos.json productEdits so the panel updates fast.
1653
+ let removedFromPromos = false;
1654
+ try {
1655
+ const cur = await readPromos(token);
1656
+ if (cur.productEdits && typeof cur.productEdits === "object") {
1657
+ let changed = false;
1658
+ if (sku && cur.productEdits[sku] != null) { delete cur.productEdits[sku]; changed = true; }
1659
+ if (slug) {
1660
+ for (const k of Object.keys(cur.productEdits)) {
1661
+ const ed = cur.productEdits[k];
1662
+ if (ed && (ed.slug === slug || String(ed.model || ed.sku || k).toUpperCase() === SKU)) {
1663
+ delete cur.productEdits[k]; changed = true;
1664
+ }
1665
+ }
1666
+ }
1667
+ if (changed) { await writePromos(token, cur); removedFromPromos = true; }
1668
+ }
1669
+ } catch (e2: any) { console.error("[delete-product] promos mirror failed", e2?.message); }
1670
+
1671
+ return Response.json({ ok: true, removed: hadAdded, removedFromPromos, file: CAT_DATASET + "/" + CAT_FILE });
1672
+ } catch (e: any) {
1673
+ console.error("[delete-product]", e?.message);
1674
+ return Response.json({ error: e.message || String(e) }, { status: 502 });
1675
+ }
1676
+ },
1677
+ },
1678
  "/api/promos": {
1679
  GET: async () => {
1680
  try {
src/vaix-combo.js CHANGED
@@ -173,12 +173,41 @@
173
  '<div style="font-weight:700;font-size:0.8rem;color:#1e293b;margin-bottom:3px">' + (c.name || "COMBO") + '</div>' +
174
  '<div style="display:flex;gap:4px;margin-bottom:4px">' + thumbs + '</div>' +
175
  '<div style="font-size:0.7rem;color:#64748b;margin-bottom:4px">' + itemsHtml + '</div>' +
176
- '<div style="font-weight:700;color:#1d4ed8;font-size:0.8rem">' + fmt(priceNum) + '</div>' +
 
 
 
177
  '</div>';
178
  }).join("");
179
  } catch(e) { console.warn("[COMBO] refresh:", e); }
180
  }
181
- function boot() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  // Retry createComboElements until vaixPromo is ready (can load after CMS module)
183
  var _retry = 0;
184
  (function tryCreate() {
 
173
  '<div style="font-weight:700;font-size:0.8rem;color:#1e293b;margin-bottom:3px">' + (c.name || "COMBO") + '</div>' +
174
  '<div style="display:flex;gap:4px;margin-bottom:4px">' + thumbs + '</div>' +
175
  '<div style="font-size:0.7rem;color:#64748b;margin-bottom:4px">' + itemsHtml + '</div>' +
176
+ '<div style="display:flex;align-items:center;justify-content:space-between;gap:6px">' +
177
+ '<span style="font-weight:700;color:#1d4ed8;font-size:0.8rem">' + fmt(priceNum) + '</span>' +
178
+ '<button type="button" class="combo-share-btn" onclick="window.shareCombo(' + idx + ', event, this)" style="padding:5px 10px;border:none;border-radius:7px;background:#1e40af;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ẻ</button>' +
179
+ '</div>' +
180
  '</div>';
181
  }).join("");
182
  } catch(e) { console.warn("[COMBO] refresh:", e); }
183
  }
184
+ // Per-combo "Chia sẻ link" (SEO). Copy a self-hosted deep link that opens the
185
+ // COMBO KHUYẾN MÃI panel. Exposed globally because combo cards are HTML strings.
186
+ window.shareCombo = function (idx, ev, btn) {
187
+ try {
188
+ if (ev) { ev.stopPropagation(); }
189
+ var combos = loadCombos() || [];
190
+ var c = combos[idx];
191
+ if (!c) return;
192
+ var slug = String(c.slug || c.name || "combo-" + (idx + 1))
193
+ .toLowerCase()
194
+ .replace(/[^a-z0-9]+/g, "-")
195
+ .replace(/^-+|-+$/g, "");
196
+ var base = window.location.origin + window.location.pathname;
197
+ var url = base + "?combo=" + encodeURIComponent(slug);
198
+ var done = function () {
199
+ if (typeof window.toast === "function") window.toast("\u2713 \u0110\u00e3 copy link chia s\u1ebb COMBO (SEO)");
200
+ else alert("Link chia s\u1ebb COMBO:\n" + url);
201
+ };
202
+ if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
203
+ navigator.clipboard.writeText(url).then(done).catch(function () { window.prompt("Link chia s\u1ebb COMBO:", url); });
204
+ } else { window.prompt("Link chia s\u1ebb COMBO:", url); }
205
+ } catch (err) {
206
+ alert("L\u1ed7i chia s\u1ebb: " + (err && err.message || err));
207
+ }
208
+ };
209
+
210
+ function boot() {
211
  // Retry createComboElements until vaixPromo is ready (can load after CMS module)
212
  var _retry = 0;
213
  (function tryCreate() {
src/vaix-rag.js CHANGED
@@ -1645,6 +1645,72 @@ function openAskAIForProduct(product) {
1645
  // UI RENDERING
1646
  // ─────────────────────────────────────────────
1647
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1648
  function renderPanelResults(products) {
1649
  const panel = document.getElementById("vaistudio-panel");
1650
  const toggle = document.getElementById("vaistudio-toggle");
@@ -1699,6 +1765,43 @@ function renderPanelResults(products) {
1699
  });
1700
  card.appendChild(cartBtn);
1701
  card.appendChild(info);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1702
  card.addEventListener("click", function(e){ e.stopPropagation(); renderDetail(p); renderSimilarInPanel(p); });
1703
  productsEl.appendChild(card);
1704
  }
@@ -1718,9 +1821,18 @@ function renderDiversePanel(count) {
1718
  if (!allProducts.length) { renderPanelResults([]); return; }
1719
  // Durable "Thêm SP" products (productEdits overlay) always surface first.
1720
  const added = (window.vaixPromo && typeof window.vaixPromo.getAddedProducts === 'function') ? window.vaixPromo.getAddedProducts() : [];
 
 
 
1721
  const base = allProducts;
1722
- if (added.length) {
1723
- renderPanelResults(added.concat(base.slice(0, Math.max(0, n - added.length))));
 
 
 
 
 
 
1724
  return;
1725
  }
1726
  const picks = getDiverseProducts(n);
@@ -2526,6 +2638,9 @@ window.vaix = {
2526
  showComboResultsInPanel: showComboResultsInPanel,
2527
  // Guard: returns true if ?product=xxx URL param has already rendered the panel
2528
  hasProductUrlHandled: function(){ return productUrlHandled; },
 
 
 
2529
  // Promo CMS hooks
2530
  renderDetail: renderDetail,
2531
  applyPromoEdits: applyPromoEdits
 
1645
  // UI RENDERING
1646
  // ─────────────────────────────────────────────
1647
 
1648
+ function toast(msg) {
1649
+ try {
1650
+ var t = document.getElementById('vaix-toast');
1651
+ if (!t) { t = document.createElement('div'); t.id = 'vaix-toast'; t.style.cssText = "position:fixed;bottom:120px;left:50%;transform:translateX(-50%);background:rgba(30,41,59,0.95);color:#fff;padding:11px 20px;border-radius:10px;font-size:.82rem;z-index:110200;box-shadow:0 6px 20px rgba(0,0,0,.3);font-family:inherit;max-width:92vw;text-align:center;transition:opacity .3s"; document.body.appendChild(t); }
1652
+ t.textContent = msg;
1653
+ t.style.opacity = '1';
1654
+ clearTimeout(t._ht);
1655
+ t._ht = setTimeout(function(){ t.style.opacity = '0'; }, 2600);
1656
+ } catch (e) {}
1657
+ }
1658
+
1659
+ // SEO-friendly share link: canonical product URL (slug-based).
1660
+ function shareableProductUrl(p) {
1661
+ const base = (window.location.origin && window.location.origin.indexOf('hf.space') >= 0) ? window.location.origin : 'https://bep40-vai-avatar2.hf.space';
1662
+ const slug = p.slug || (p.sku ? String(p.sku).toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'') : '') || 'san-pham';
1663
+ // Keep the map share link friendlier: encode the slug into a vanity query.
1664
+ return base + '/?product=' + encodeURIComponent(slug);
1665
+ }
1666
+
1667
+ // Admin delete: permanently removes an added product from the shared dataset,
1668
+ // localStorage list, productEdits, and the live catalog/allProducts.
1669
+ function deleteProductAction(p, onDone) {
1670
+ try {
1671
+ if (!(typeof window.isVaAdmin === 'function') || !window.isVaAdmin()) { toast('🔒 Cần nhập mã V.AISTUDIO trên trang chủ.'); return; }
1672
+ const sku = p.sku || p.model || '';
1673
+ const slug = p.slug || '';
1674
+ const name = p.title_clean || p.name || 'sản phẩm này';
1675
+ if (!window.confirm('⚠️ Xóa vĩnh viễn "' + name + '" khỏi danh sách sản phẩm (không thể khôi phục)?')) return;
1676
+ // 1) Remove from the permanent shared dataset (products_url_added.json).
1677
+ return fetch('/api/delete-product', {
1678
+ method: 'POST',
1679
+ headers: { 'Content-Type': 'application/json', 'x-vai-admin': '1' },
1680
+ body: JSON.stringify({ sku: sku, slug: slug })
1681
+ }).then(function(r){ return r.json().catch(function(){ return {}; }); }).then(function(res){
1682
+ if (res && res.ok) {
1683
+ // 2) Remove from local durable list.
1684
+ try { if (window.vaixPromo && typeof window.vaixPromo.removeAddedProduct === 'function') window.vaixPromo.removeAddedProduct(sku); } catch (e1) {}
1685
+ if (window.localStorage) {
1686
+ try {
1687
+ const list = JSON.parse(window.localStorage.getItem('vai_padded_products') || '[]');
1688
+ const n2 = Array.isArray(list) ? list.filter(function(x){ return !(x && sku && String(x.sku).toUpperCase() === String(sku).toUpperCase()); }) : [];
1689
+ window.localStorage.setItem('vai_padded_products', JSON.stringify(n2));
1690
+ } catch (e2) {}
1691
+ }
1692
+ // 3) Remove from the live in-memory catalog so it disappears immediately.
1693
+ try {
1694
+ const arr = (window.vaix && window.vaix.allProducts) ? window.vaix.allProducts() : null;
1695
+ if (Array.isArray(arr) && arr.length) {
1696
+ const idx = arr.findIndex(function(x){ return x && x.sku && sku && String(x.sku).toUpperCase() === String(sku).toUpperCase(); });
1697
+ if (idx >= 0) { arr.splice(idx, 1); }
1698
+ if (window.vaix.resetDiversePool) { try { window.vaix.resetDiversePool(); } catch (e3) {} }
1699
+ }
1700
+ } catch (e4) {}
1701
+ toast('🗑️ Đã xóa vĩnh viễn "' + name + '" khỏi dữ liệu.');
1702
+ if (onDone) onDone();
1703
+ } else {
1704
+ toast('❌ Xóa lỗi: ' + ((res && res.error) || 'không thể xóa khỏi dataset'));
1705
+ }
1706
+ }).catch(function(e){
1707
+ toast('❌ Lỗi khi xóa: ' + (e && e.message || e));
1708
+ });
1709
+ } catch (err) {
1710
+ toast('❌ Lỗi xóa: ' + (err && err.message || err));
1711
+ }
1712
+ }
1713
+
1714
  function renderPanelResults(products) {
1715
  const panel = document.getElementById("vaistudio-panel");
1716
  const toggle = document.getElementById("vaistudio-toggle");
 
1765
  });
1766
  card.appendChild(cartBtn);
1767
  card.appendChild(info);
1768
+ // ---- Admin actions (only after V.AISTUDIO homepage code) ----
1769
+ const isAdmin = (typeof window.isVaAdmin === 'function') ? window.isVaAdmin() : false;
1770
+ const isAddedProduct = !!(p._addedBy === 'products_url_added' || p._addedBy === 'vai-add-product' || (window.vaixPromo && window.vaixPromo.isAddedProduct ? window.vaixPromo.isAddedProduct(p) : false));
1771
+ if (isAdmin) {
1772
+ const adminBtns = document.createElement("div");
1773
+ adminBtns.className = "product-card-admin";
1774
+ adminBtns.style.cssText = "display:flex;gap:6px;margin-top:8px;flex-wrap:wrap";
1775
+ // Share link button (SEO-friendly ?product=<slug> URL, copied to clipboard).
1776
+ const shareBtn = document.createElement("button");
1777
+ shareBtn.type = "button";
1778
+ shareBtn.className = "product-card-btn share-btn";
1779
+ shareBtn.style.cssText = "flex:1;padding:7px 8px;border:none;border-radius:8px;background:#1e40af;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:5px;font-family:inherit";
1780
+ shareBtn.innerHTML = '<i class="fas fa-bullhorn"></i> Chia sẻ';
1781
+ shareBtn.addEventListener("click", function(e) {
1782
+ e.stopPropagation();
1783
+ const url = shareableProductUrl(p);
1784
+ if (window.navigator && navigator.clipboard && navigator.clipboard.writeText) {
1785
+ navigator.clipboard.writeText(url).then(function(){ toast('✓ Đã copy link chia sẻ (SEO)'); }).catch(function(){ window.prompt('Link chia sẻ:', url); });
1786
+ } else { window.prompt('Link chia sẻ:', url); }
1787
+ });
1788
+ adminBtns.appendChild(shareBtn);
1789
+ // Delete button (admin only) — appears on admin-added products and
1790
+ // permanently removes them from the shared dataset.
1791
+ if (isAddedProduct) {
1792
+ const delBtn = document.createElement("button");
1793
+ delBtn.type = "button";
1794
+ delBtn.className = "product-card-btn del-btn";
1795
+ delBtn.style.cssText = "padding:7px 10px;border:none;border-radius:8px;background:#dc2626;color:#fff;font-size:.72rem;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:5px;font-family:inherit";
1796
+ delBtn.innerHTML = '<i class="fas fa-trash"></i> Xóa';
1797
+ delBtn.addEventListener("click", function(e) {
1798
+ e.stopPropagation();
1799
+ deleteProductAction(p, function(){ productsEl.removeChild(card); if (countEl) countEl.textContent = (productsEl.children.length) + ' kết quả'; });
1800
+ });
1801
+ adminBtns.appendChild(delBtn);
1802
+ }
1803
+ card.appendChild(adminBtns);
1804
+ }
1805
  card.addEventListener("click", function(e){ e.stopPropagation(); renderDetail(p); renderSimilarInPanel(p); });
1806
  productsEl.appendChild(card);
1807
  }
 
1821
  if (!allProducts.length) { renderPanelResults([]); return; }
1822
  // Durable "Thêm SP" products (productEdits overlay) always surface first.
1823
  const added = (window.vaixPromo && typeof window.vaixPromo.getAddedProducts === 'function') ? window.vaixPromo.getAddedProducts() : [];
1824
+ // Also surface products added via URL/OCR (kept in products_url_added.json, already
1825
+ // merged into allProducts by load()). Their home is _addedBy === 'products_url_added'.
1826
+ const addedByUrl = allProducts.filter(function (p) { return p && p._addedBy === 'products_url_added'; });
1827
  const base = allProducts;
1828
+ // Merge + dedup by sku so a product only appears once at the top.
1829
+ const topAdded = [];
1830
+ const seenTop = new Set();
1831
+ const pushTop = function (p) { if (!p) return; const k = String((p.sku || p.model || '')).toUpperCase(); if (!seenTop.has(k)) { seenTop.add(k); topAdded.push(p); } };
1832
+ added.forEach(pushTop);
1833
+ addedByUrl.forEach(pushTop);
1834
+ if (topAdded.length) {
1835
+ renderPanelResults(topAdded.concat(base.slice(0, Math.max(0, n - topAdded.length))));
1836
  return;
1837
  }
1838
  const picks = getDiverseProducts(n);
 
2638
  showComboResultsInPanel: showComboResultsInPanel,
2639
  // Guard: returns true if ?product=xxx URL param has already rendered the panel
2640
  hasProductUrlHandled: function(){ return productUrlHandled; },
2641
+ // Share / admin-delete helpers (also used by renderPanelResults cards)
2642
+ shareableProductUrl: shareableProductUrl,
2643
+ deleteProductAction: deleteProductAction,
2644
  // Promo CMS hooks
2645
  renderDetail: renderDetail,
2646
  applyPromoEdits: applyPromoEdits