Sản phẩm
Đang tải...

Đang tải...

Mã SKU: Đang tải...

Đã bán 0 | Chia sẻ | Truy xuất nguồn gốc
' + '' + escapedValue + '' + '
'; }); html += '
'; // Cột phải html += '
'; rightColumn.forEach(function (spec) { const label = spec.label?.vi_VN || spec.label || 'N/A'; const value = spec.value?.vi_VN || spec.value || 'N/A'; const escapedLabel = escapeHtml(String(label || '').trim()); const escapedValue = escapeHtml(String(value || '').trim()); html += '
' + '' + escapedLabel + ':' + '' + escapedValue + '' + '
'; }); html += '
'; html += '
'; container.innerHTML = html; if (container.classList) { container.classList.remove('hidden'); } } // Xử lý lỗi function showSpecificationsError() { // Ẩn toàn bộ section khi có lỗi const sectionEl = document.getElementById('product-specifications-section'); if (sectionEl && sectionEl.classList) { sectionEl.classList.add('hidden'); } } // Khởi tạo async function initProductSpecifications() { if (!Utils) { console.warn('CommerceUtils not available for specifications'); showSpecificationsError(); return; } // Ensure API hydration (avoid reading DOM data-product-id placeholder) try { if (typeof Utils.getApiProduct === 'function') { await Utils.getApiProduct(); } } catch (e) { } const productInfo = Utils.getProductInfo(); if (!productInfo || !productInfo.productId) { console.warn('Cannot get productId for specifications'); showSpecificationsError(); return; } try { const specifications = await fetchProductSpecifications(productInfo.productId); renderSpecifications(specifications); } catch (error) { console.error('Error loading product specifications:', error); showSpecificationsError(); } } // Chạy khi DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initProductSpecifications); } else { initProductSpecifications(); } // Hỗ trợ Liferay events if (typeof Liferay !== 'undefined' && Liferay.on) { Liferay.on('allPortletsReady', function () { setTimeout(initProductSpecifications, 500); }); } })(); '; }; } // Tạo QR code function generateQRCode(data) { const container = document.getElementById('qrcode-container'); if (!container) return; // Xóa QR code cũ nếu có container.innerHTML = ''; // Hiển thị loading container.innerHTML = '
'; // Đợi QRCode library load xong (giảm thời gian đợi xuống 3 giây) waitForQRCode(function (QRCodeLib) { if (!QRCodeLib) { // Fallback: Sử dụng API online generateQRCodeWithAPI(data); return; } // Xóa loading container.innerHTML = ''; // Tạo canvas element const canvas = document.createElement('canvas'); container.appendChild(canvas); // Tạo QR code mới try { QRCodeLib.toCanvas(canvas, data.traceUrl, { width: 300, margin: 2, color: { dark: '#000000', light: '#FFFFFF' } }, function (error) { if (error) { console.error('Error generating QR code:', error); // Fallback: Sử dụng API online generateQRCodeWithAPI(data); } }); } catch (e) { console.error('Exception generating QR code:', e); // Fallback: Sử dụng API online generateQRCodeWithAPI(data); } }, 30); // Giảm từ 50 xuống 30 (3 giây) } // Hiển thị modal QR code function showQRCodeModal() { const modal = document.getElementById('qr-code-modal'); if (!modal) return; const traceData = getProductTraceData(); if (!traceData) { showToastSPNB('Không thể lấy thông tin sản phẩm để tạo mã QR', true); return; } generateQRCode(traceData); modal.classList.remove('hidden'); } // Đóng modal QR code function closeQRCodeModal() { const modal = document.getElementById('qr-code-modal'); if (modal) { modal.classList.add('hidden'); } } // Hiển thị modal quét QR function showScanQRModal() { const modal = document.getElementById('scan-qr-modal'); if (!modal) return; closeQRCodeModal(); modal.classList.remove('hidden'); // Khởi tạo HTML5 QR Code Scanner const readerContainer = document.getElementById('qr-reader'); if (readerContainer && !html5QrCode) { html5QrCode = new Html5Qrcode("qr-reader"); html5QrCode.start( { facingMode: "environment" }, // Sử dụng camera sau { fps: 10, qrbox: { width: 250, height: 250 } }, function (decodedText, decodedResult) { // Khi quét được QR code handleScannedQR(decodedText); }, function (errorMessage) { // Lỗi khi quét (không hiển thị lỗi thường xuyên) } ).catch(function (err) { console.error('Error starting QR scanner:', err); showToastSPNB('Không thể khởi động camera. Vui lòng kiểm tra quyền truy cập camera.', true); }); } } // Đóng modal quét QR function closeScanQRModal() { const modal = document.getElementById('scan-qr-modal'); if (modal) { modal.classList.add('hidden'); } // Dừng scanner if (html5QrCode) { html5QrCode.stop().then(function () { html5QrCode.clear(); html5QrCode = null; }).catch(function (err) { console.error('Error stopping QR scanner:', err); }); } } // Trích productId từ nội dung QR (URL đủ/không scheme, hoặc chỉ số) function extractProductIdFromQrPayload(qrData) { const raw = (qrData || '').toString().trim(); if (!raw) return null; if (/^\d+$/.test(raw)) return raw; function idFromSearch(str) { try { const u = new URL(str); const p = u.searchParams.get('productId'); if (p) { const digits = String(p).replace(/[^\d]/g, ''); return digits || String(p); } } catch (e) { } return null; } let id = idFromSearch(raw); if (id) return id; // Nhiều app quét QR trả về URL không có scheme → new URL() lỗi if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) { id = idFromSearch('https://' + raw.replace(/^\/+/, '')); if (id) return id; } const m1 = raw.match(/[?&#]productId=(\d+)/i); if (m1) return m1[1]; const m2 = raw.match(/productId[=:]\s*(\d+)/i); if (m2) return m2[1]; return null; } // Xử lý khi quét được QR code function handleScannedQR(qrData) { closeScanQRModal(); const productId = extractProductIdFromQrPayload(qrData); if (!productId) { showToastSPNB('Mã QR không hợp lệ. Vui lòng thử lại.', true); return; } showTraceInfoModal(productId); } // Lấy thông tin sản phẩm: ưu tiên API storefront by-id (cùng nguồn với trang chi tiết), // vì headless-commerce-admin-catalog thường yêu cầu quyền admin — khách quét QR sẽ bị lỗi. async function fetchProductTraceInfo(productId) { const idStr = String(productId || '').trim(); if (!idStr) { throw new Error('Thiếu mã sản phẩm'); } const csrf = (typeof Liferay !== 'undefined' && Liferay.authToken) ? Liferay.authToken : ''; const headers = { 'Accept': 'application/json' }; if (csrf) { headers['x-csrf-token'] = csrf; } const fetchOpts = { method: 'GET', headers: headers, credentials: 'include' }; let productData = null; try { const byIdUrl = '/o/gocgac-commerce/v1.0/products/by-id?id=' + encodeURIComponent(idStr); const byIdRes = await fetch(byIdUrl, fetchOpts); if (byIdRes.ok) { const responseText = await byIdRes.text().catch(function () { return ''; }); const trimmed = (responseText || '').trim(); if (trimmed) { try { productData = JSON.parse(trimmed); } catch (e) { console.warn('fetchProductTraceInfo: by-id JSON parse failed', e); } } } } catch (e) { console.error('fetchProductTraceInfo: by-id request error', e); } if (!productData) { try { const adminUrl = APP_CONFIG.API_BASE_ADMIN + '/products/' + encodeURIComponent(idStr); const response = await fetch(adminUrl, fetchOpts); if (response.ok) { productData = (window.safeResponseJson ? await window.safeResponseJson(response) : null); } } catch (e) { console.error('fetchProductTraceInfo: admin product request error', e); } } if (!productData) { throw new Error('Không thể tải thông tin sản phẩm (kiểm tra mạng hoặc đăng nhập).'); } const product = Object.assign({}, productData, { productId: productData.productId != null ? productData.productId : idStr, id: productData.id != null ? productData.id : (productData.productId || idStr) }); let catalogData = null; let accountData = null; const catalogId = product.catalogId || productData.catalogId; if (catalogId) { try { const catalogUrl = APP_CONFIG.API_BASE_ADMIN + '/catalog/' + catalogId; const catalogRes = await fetch(catalogUrl, fetchOpts); if (catalogRes.ok) { catalogData = (window.safeResponseJson ? await window.safeResponseJson(catalogRes) : null); if (catalogData && catalogData.accountId) { try { const accountUrl = APP_CONFIG.API_BASE_ACCOUNT + '/accounts/' + catalogData.accountId; const accountRes = await fetch(accountUrl, fetchOpts); if (accountRes.ok) { accountData = (window.safeResponseJson ? await window.safeResponseJson(accountRes) : null); } } catch (e) { console.error('Error fetching account info:', e); } } } } catch (e) { console.error('Error fetching catalog/account info:', e); } } return { product: product, catalog: catalogData, account: accountData }; } // Hiển thị modal thông tin truy xuất nguồn gốc async function showTraceInfoModal(productId) { const modal = document.getElementById('trace-info-modal'); const content = document.getElementById('trace-info-content'); if (!modal || !content) return; modal.classList.remove('hidden'); // Hiển thị loading content.innerHTML = `

Đang tải thông tin...

`; try { const traceInfo = await fetchProductTraceInfo(productId); renderTraceInfo(traceInfo); } catch (error) { const errorMsg = error.message || 'Vui lòng thử lại sau'; content.innerHTML = '
' + '' + '' + '' + '' + '' + '

Không thể tải thông tin sản phẩm

' + '

' + escapeHtml(errorMsg) + '

' + '
'; } } // Render thông tin truy xuất nguồn gốc function renderTraceInfo(traceInfo) { const content = document.getElementById('trace-info-content'); if (!content) return; const { product, catalog, account } = traceInfo; // Xác định nguồn gốc sản phẩm từ một số field phổ biến (nếu có) let originText = ''; try { const productCf = product?.customFields || {}; const accountCf = account?.customFields || {}; originText = productCf.origin || productCf.originName || productCf.province || productCf.xuatXu || // trường hay dùng cho "xuất xứ" product?.address || accountCf.origin || accountCf.originName || accountCf.address || ''; } catch (e) { originText = ''; } let html = '
'; // Thông tin sản phẩm html += '
'; html += '

'; html += ''; html += ''; html += ''; html += ''; html += ''; html += 'Thông tin sản phẩm'; html += '

'; html += '
'; html += '
Tên sản phẩm:' + escapeHtml(product.name?.vi_VN || product.name || 'N/A') + '
'; html += '
Mã sản phẩm:' + escapeHtml(product.productId || product.id || 'N/A') + '
'; if (product.sku) { html += '
SKU:' + escapeHtml(product.sku) + '
'; } html += '
'; html += '
'; // Thông tin nhà sản xuất/cửa hàng if (account) { html += '
'; html += '

'; html += ''; html += ''; html += ''; html += ''; html += 'Thông tin nhà sản xuất'; html += '

'; html += '
'; html += '
Tên tổ chức:' + escapeHtml(account.name || 'N/A') + '
'; if (account.customFields?.address) { html += '
Địa chỉ:' + escapeHtml(account.customFields.address) + '
'; } if (account.customFields?.phone) { html += '
Điện thoại:' + escapeHtml(account.customFields.phone) + '
'; } html += '
'; html += '
'; } // Thông tin catalog if (catalog) { html += '
'; html += '

'; html += ''; html += ''; html += ''; html += ''; html += ''; html += 'Thông tin danh mục'; html += '

'; html += '
'; html += '
Tên danh mục:' + escapeHtml(catalog.name?.vi_VN || catalog.name || 'N/A') + '
'; html += '
'; html += '
'; } // Thông tin truy xuất html += '
'; html += '

'; html += ''; html += ''; html += ''; html += ''; html += 'Thông tin truy xuất'; html += '

'; html += '
'; const traceDate = new Date().toLocaleString('vi-VN'); html += '
Thời gian truy xuất:' + escapeHtml(traceDate) + '
'; // Hiển thị nguồn gốc (nếu không có dữ liệu thì ghi "Đang cập nhật") const originDisplay = originText ? escapeHtml(String(originText)) : 'Đang cập nhật'; html += '
Nguồn gốc:' + originDisplay + '
'; html += '
Trạng thái:✓ Đã xác thực
'; html += '
'; html += '
'; html += '
'; content.innerHTML = html; } // Đóng modal thông tin truy xuất function closeTraceInfoModal() { const modal = document.getElementById('trace-info-modal'); if (modal) { modal.classList.add('hidden'); } } // Khởi tạo event listeners (chỉ gắn một lần — tránh double fire khi Liferay allPortletsReady) function initTraceOrigin() { if (!window.__newDetailTraceOriginUiBound) { window.__newDetailTraceOriginUiBound = true; const traceLink = document.getElementById('trace-origin-link'); if (traceLink) { traceLink.addEventListener('click', function (e) { e.preventDefault(); showQRCodeModal(); }); } const closeQRBtns = [ document.getElementById('close-qr-modal'), document.getElementById('close-qr-modal-btn') ]; closeQRBtns.forEach(function (btn) { if (btn) { btn.addEventListener('click', closeQRCodeModal); } }); const scanQRBtn = document.getElementById('scan-qr-btn'); if (scanQRBtn) { scanQRBtn.addEventListener('click', showScanQRModal); } const closeScanBtns = [ document.getElementById('close-scan-modal'), document.getElementById('close-scan-modal-btn') ]; closeScanBtns.forEach(function (btn) { if (btn) { btn.addEventListener('click', closeScanQRModal); } }); const closeTraceInfoBtns = [ document.getElementById('close-trace-info-modal'), document.getElementById('close-trace-info-modal-btn') ]; closeTraceInfoBtns.forEach(function (btn) { if (btn) { btn.addEventListener('click', closeTraceInfoModal); } }); const modals = [ document.getElementById('qr-code-modal'), document.getElementById('scan-qr-modal'), document.getElementById('trace-info-modal') ]; modals.forEach(function (modal) { if (modal) { modal.addEventListener('click', function (e) { if (e.target === modal) { if (modal.id === 'qr-code-modal') { closeQRCodeModal(); } else if (modal.id === 'scan-qr-modal') { closeScanQRModal(); } else if (modal.id === 'trace-info-modal') { closeTraceInfoModal(); } } }); } }); } try { const params = new URLSearchParams(window.location.search || ''); if (params.get('traceOrigin') === '1' && !window.__newDetailTraceOriginAutoShown) { let pid = params.get('productId'); if (pid) { pid = String(pid).replace(/[^\d]/g, '') || pid; } if (!pid && Utils && typeof Utils.getProductInfo === 'function') { if (Utils.apiProduct && Utils.apiProduct.productId) { pid = String(Utils.apiProduct.productId); } else { const info = Utils.getProductInfo(); if (info && info.productId) { pid = String(info.productId); } } } if (pid) { window.__newDetailTraceOriginAutoShown = true; showTraceInfoModal(pid); } } } catch (e) { } } // Khởi tạo khi DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { setTimeout(initTraceOrigin, 100); }); } else { setTimeout(initTraceOrigin, 100); } // Hỗ trợ Liferay events if (typeof Liferay !== 'undefined' && Liferay.on) { Liferay.on('allPortletsReady', function () { setTimeout(initTraceOrigin, 500); }); } })(); sản phẩm' : 'Hết hàng'; } else { stockInfo.removeAttribute('data-max-quantity'); stockInfo.innerHTML = 'Đang cập nhật tồn kho theo phân loại'; } } btnIds.forEach(function (id) { var btn = document.getElementById(id); if (btn) btn.disabled = !canPurchase; }); if (window.CommerceUtils && window.CommerceUtils.apiProduct) { var rawUomList = sku.skuUnitOfMeasures || []; var primaryUom = rawUomList.find(function (u) { return u.primary === true; }) || rawUomList[0]; window.CommerceUtils.apiProduct.skuId = sku.id || null; window.CommerceUtils.apiProduct.skuUnitOfMeasureKey = sku.skuUnitOfMeasureKey || (primaryUom ? primaryUom.key : ''); window.CommerceUtils.apiProduct.skuUnitOfMeasure = normalizeUOM(primaryUom); window.CommerceUtils.apiProduct.stockLevel = stock; window.CommerceUtils.apiProduct.price = listPrice; window.CommerceUtils.apiProduct.promoPrice = promoPrice; window.CommerceUtils.apiProduct.finalPrice = finalPrice; } var quantityInput = document.getElementById('quantity-input'); var quantityInputMobile = document.getElementById('quantity-input-mobile'); if (quantityInput) { // Khi đổi SKU thì reset về 1 để tính giá/tồn theo biến thể mới quantityInput.value = '1'; if (quantityInputMobile) quantityInputMobile.value = '1'; quantityInput.dispatchEvent(new Event('change', { bubbles: true })); } } function renderSkuOptions(skus) { var section = document.getElementById('sku-variant-section'); var container = document.getElementById('sku-variant-options'); if (!section || !container) return; if (!Array.isArray(skus) || skus.length === 0) { syncNoVariantBadge(false); section.classList.remove('hidden'); container.innerHTML = 'Chưa có dữ liệu phân loại. // ==================== SHARED COMMERCE UTILITIES ==================== (function () { 'use strict'; /** * Chuẩn hóa skuUnitOfMeasure cho Headless Cart. * Không tự gán UOM mặc định (vd. C62) — sai UOM khiến POST /carts/.../items trả 400. * Khi không có key hợp lệ, trả null để caller bỏ qua field (Liferay lấy UOM mặc định của SKU). */ function cleanSkuUnitOfMeasure(skuUnitOfMeasure) { if (!skuUnitOfMeasure || typeof skuUnitOfMeasure !== 'object') { return null; } var key = skuUnitOfMeasure.key; if (key == null || String(key).trim() === '') { return null; } return { key: String(key).trim(), precision: skuUnitOfMeasure.precision !== undefined ? skuUnitOfMeasure.precision : 0, incrementalOrderQuantity: skuUnitOfMeasure.incrementalOrderQuantity !== undefined ? skuUnitOfMeasure.incrementalOrderQuantity : 1 }; } async function safeResponseJson(response) { try { const text = await response.text(); if (!text || !text.trim()) return null; return JSON.parse(text); } catch (e) { return null; } } // Expose để các <script> khác trong file dùng chung window.safeResponseJson = safeResponseJson; /** * Chuẩn hóa mảng skuStockLevels từ API by-id (tồn + giá từng SKU). */ function normalizeSkuStockLevelsFromApi(api) { if (!api || !Array.isArray(api.skuStockLevels)) { return []; } return api.skuStockLevels.map(function (row) { var rawId = row.skuId != null ? row.skuId : row.id; var id = typeof rawId === 'number' ? rawId : parseInt(String(rawId || '').replace(/[^\d]/g, ''), 10); var slRaw = row.stockLevel; var sl = slRaw != null && slRaw !== '' ? parseInt(String(slRaw), 10) : NaN; var p = Number(row.price); var pp = Number(row.promoPrice); return { skuId: !id || isNaN(id) ? null : id, sku: (row.sku || '').toString(), stockLevel: isNaN(sl) ? null : sl, price: isNaN(p) ? 0 : p, promoPrice: isNaN(pp) ? 0 : pp, skuUnitOfMeasureKey: (row.skuUnitOfMeasureKey || '').toString(), unitOfMeasure: (row.unitOfMeasure || '').toString() }; }).filter(function (x) { return x.skuId != null && x.skuId > 0; }); } /** * Lấy stockLevel cho một skuId từ mảng đã chuẩn hóa (API gocgac-commerce). */ function getStockFromSkuStockLevels(skuStockLevels, skuId) { if (!skuStockLevels || !skuStockLevels.length || skuId == null || skuId === '') { return null; } var sid = typeof skuId === 'number' ? skuId : parseInt(String(skuId).replace(/[^\d]/g, ''), 10); if (!sid || isNaN(sid)) { return null; } for (var i = 0; i < skuStockLevels.length; i++) { var row = skuStockLevels[i]; if (row.skuId === sid && row.stockLevel != null && row.stockLevel !== '') { var v = parseInt(String(row.stockLevel), 10); if (!isNaN(v)) { return v; } } } return null; } function getSkuDataFromSkuStockLevels(skuStockLevels, skuId) { if (!Array.isArray(skuStockLevels) || skuStockLevels.length === 0) { return null; } if (skuId == null || skuId === '') { return skuStockLevels[0] || null; } var sid = typeof skuId === 'number' ? skuId : parseInt(String(skuId).replace(/[^\d]/g, ''), 10); if (!sid || isNaN(sid)) { return skuStockLevels[0] || null; } for (var i = 0; i < skuStockLevels.length; i++) { if (skuStockLevels[i].skuId === sid) { return skuStockLevels[i]; } } return skuStockLevels[0] || null; } window.getStockFromSkuStockLevels = getStockFromSkuStockLevels; window.getSkuDataFromSkuStockLevels = getSkuDataFromSkuStockLevels; window.CommerceUtils = { accountId: null, // Best-effort UI hydration from API (by-id?id=productId) apiProduct: null, apiProductPromise: null, // Caching promises to prevent redundant network calls accountContextPromise: null, /** * Tồn kho hiệu dụng: ưu tiên skuStockLevels theo skuId, sau đó stockLevel tổng (SKU primary). */ getEffectiveStockLevel(skuIdOverride) { var ap = this.apiProduct; if (!ap) { return null; } var sid = skuIdOverride; if (sid == null || sid === '') { sid = ap.skuId; } if (sid != null && typeof sid !== 'number') { sid = parseInt(String(sid).replace(/[^\d]/g, ''), 10); } if (sid && ap.skuStockLevels && ap.skuStockLevels.length) { var fromArr = getStockFromSkuStockLevels(ap.skuStockLevels, sid); if (fromArr != null) { return fromArr; } } if (ap.stockLevel != null && ap.stockLevel !== '') { var sl = parseInt(String(ap.stockLevel), 10); return isNaN(sl) ? null : sl; } return null; }, /** * Giá hiển thị dùng để quyết định cho phép đặt hàng: ưu tiên apiProduct.finalPrice, * fallback data-list-price / data-promo-price trên khối giá (sau khi render/đổi biến thể). */ getEffectiveFinalPrice() { var ap = this.apiProduct; var n = ap != null ? Number(ap.finalPrice) : NaN; if (!isNaN(n) && n > 0) { return n; } var el = document.getElementById('product-total-price') || document.getElementById('product-total-price-mobile'); if (!el) { return 0; } var lp = parseFloat(String(el.getAttribute('data-list-price') || '0'), 10) || 0; var pp = parseFloat(String(el.getAttribute('data-promo-price') || '0'), 10) || 0; var v = (pp > 0 ? pp : lp) || 0; return v > 0 ? v : 0; }, async fetchAccountId() { if (this.accountId) return this.accountId; try { const userId = Liferay.ThemeDisplay.isSignedIn() ? Liferay.ThemeDisplay.getUserId() : null; if (!userId) return null; const response = await fetch(APP_CONFIG.API_BASE_USER + '/my-user-account', { headers: { 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include' }); if (!response.ok) return null; const userData = await safeResponseJson(response); // Phiên REST phải đúng user đang xem trang; nếu không (phiên hết hạn) thì yêu cầu đăng nhập lại if (!userData || String(userData.id) !== String(userId)) return null; this.accountId = userData?.accountBriefs?.[0]?.id || null; return this.accountId; } catch (error) { console.error('Error fetching accountId:', error); return null; } }, async fetchAccountContext() { const accountId = await this.fetchAccountId(); if (!accountId) { return { accountId: null, channelId: null }; } // Luôn ưu tiên kênh theo sản phẩm (GET by-id) — không dùng DOM/kênh cố định const fromProduct = safeParseInt(this.apiProduct && this.apiProduct.channelId); if (fromProduct > 0) { return { accountId: accountId, channelId: fromProduct }; } if (this.accountContextPromise) { return this.accountContextPromise; } this.accountContextPromise = (async () => { try { const url = APP_CONFIG.API_BASE_ACCOUNT + '/accounts/' + accountId; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include' }); if (response.ok) { const data = await safeResponseJson(response); const channelIdField = data?.customFields?.ChannelId || data?.customFields?.channelId; if (channelIdField) { const channel = safeParseInt(channelIdField); if (channel > 0) { return { accountId: accountId, channelId: channel }; } } } } catch (error) { console.error('Error fetching account detail for channelId:', error); } return { accountId: accountId, channelId: null }; })(); return this.accountContextPromise; }, async fetchChannelIdByProduct(productId) { try { if (!productId) return null; if (this.apiProduct && String(this.apiProduct.productId) === String(productId)) { const ch = safeParseInt(this.apiProduct.channelId); if (ch > 0) { return ch; } } if (typeof this.getApiProduct === 'function') { await this.getApiProduct(); } if (this.apiProduct && String(this.apiProduct.productId) === String(productId)) { const ch = safeParseInt(this.apiProduct.channelId); if (ch > 0) { return ch; } } const productUrl = APP_CONFIG.API_BASE_ADMIN + '/products/' + productId; const productRes = await fetch(productUrl, { method: 'GET', headers: { 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include' }); if (!productRes.ok) return null; const productData = await safeResponseJson(productRes); // Ưu tiên channelId trực tiếp từ product (admin catalog) nếu API có trả const directChannel = safeParseInt(productData?.channelId); if (directChannel > 0) { return directChannel; } return null; } catch (error) { console.error('Error fetching channelId by product:', error); return null; } }, getProductInfo() { // Prefer cached API result if already loaded (productId đủ để add-to-cart; catalogEntryId có thể trùng id). if (this.apiProduct && this.apiProduct.productId) { return { catalogEntryId: this.apiProduct.catalogEntryId || safeParseInt(this.apiProduct.id) || null, productId: this.apiProduct.productId, skuId: this.apiProduct.skuId }; } const totalPriceElement = document.getElementById('product-total-price'); const tagsContainer = document.querySelector('[data-product-id]'); if (!totalPriceElement) return null; let catalogEntryId = null; if (tagsContainer) { catalogEntryId = tagsContainer.getAttribute('data-product-id'); catalogEntryId = (catalogEntryId || '').toString().replace(/[^\d]/g, ''); } // productId mới lấy từ query param (productId=...) nên không cộng +1 nữa let productId = null; try { const qs = new URLSearchParams(window.location.search || ''); const qProductId = (qs.get('productId') || '').toString().replace(/[^\d]/g, ''); if (qProductId) { productId = safeParseInt(qProductId); } } catch (e) { } // Fallback cũ: nếu không có query param thì tạm dùng data-product-id (KHÔNG cộng +1) if (!productId && catalogEntryId && catalogEntryId !== '0') { productId = safeParseInt(catalogEntryId); } return { catalogEntryId: catalogEntryId ? safeParseInt(catalogEntryId) : null, productId: productId, skuId: null }; }, // Extract friendUrl from current pathname (e.g. /en/p/test => /p/test). (legacy fallback) getFriendUrlFromPage() { try { const pathname = window.location && window.location.pathname ? window.location.pathname : ''; if (!pathname) return ''; const idx = pathname.indexOf('/p/'); let friendUrl = idx >= 0 ? pathname.substring(idx) : pathname; if (!friendUrl.startsWith('/')) friendUrl = '/' + friendUrl; friendUrl = friendUrl.replace(/\/+$/g, ''); return friendUrl; } catch (e) { return ''; } }, // New: productId is from URL query string (?productId=111290) getProductIdFromUrl() { try { const qs = new URLSearchParams(window.location.search || ''); const raw = (qs.get('productId') || '').toString().replace(/[^\d]/g, ''); return raw ? safeParseInt(raw) : null; } catch (e) { return null; } }, async getApiProduct() { const productIdParam = this.getProductIdFromUrl(); if (!productIdParam) return null; // Đổi ?productId= hoặc điều hướng SP khác: xóa cache để không hiện giá/tồn của SP trước if (this.apiProduct && String(this.apiProduct.productId) !== String(productIdParam)) { this.apiProduct = null; this.apiProductPromise = null; this.accountContextPromise = null; } if (this.apiProduct) return this.apiProduct; if (this.apiProductPromise) return this.apiProductPromise; const endpoint = '/o/gocgac-commerce/v1.0/products/by-id?id=' + encodeURIComponent(String(productIdParam)); this.apiProductPromise = (async () => { const csrf = (typeof Liferay !== 'undefined' && Liferay.authToken) ? Liferay.authToken : ''; const headers = { 'Accept': 'application/json' }; if (csrf) { headers['x-csrf-token'] = csrf; } const res = await fetch(endpoint, { method: 'GET', headers: headers, credentials: 'include' }); if (!res.ok) { const text = await res.text().catch(function () { return ''; }); throw new Error('GET by-id failed: ' + res.status + ' ' + text); } // API đôi khi trả về body rỗng/không phải JSON => tránh lỗi "Unexpected end of JSON input" const responseText = await res.text().catch(function () { return ''; }); const trimmed = (responseText || '').trim(); if (!trimmed) return null; let api = null; try { api = JSON.parse(trimmed); } catch (e) { console.warn('getApiProduct: Invalid JSON response', e); api = null; } if (!api) return null; // Với API by-id mới: id chính là productId (theo query param) const catalogEntryId = safeParseInt(api.id) || null; const productId = safeParseInt(productIdParam) || safeParseInt(api.id) || null; const skuStockLevels = normalizeSkuStockLevelsFromApi(api); const fallbackSkuId = (skuStockLevels && skuStockLevels.length > 0) ? safeParseInt(skuStockLevels[0].skuId) : null; const skuId = safeParseInt(api.skuId) || fallbackSkuId || null; const selectedSku = getSkuDataFromSkuStockLevels(skuStockLevels, skuId); const listPrice = (selectedSku && Number(selectedSku.price) > 0) ? Number(selectedSku.price) : (Number(api.price) || 0); const promoPrice = (selectedSku && Number(selectedSku.promoPrice) > 0) ? Number(selectedSku.promoPrice) : (Number(api.promoPrice) || 0); const finalPrice = promoPrice > 0 ? promoPrice : listPrice; const skuUnitOfMeasureKey = ( (selectedSku && selectedSku.skuUnitOfMeasureKey) || api.skuUnitOfMeasureKey || '' ).toString(); const unitOfMeasure = ( (selectedSku && selectedSku.unitOfMeasure) || api.unitOfMeasure || skuUnitOfMeasureKey || '' ).toString(); const skuUnitOfMeasure = skuUnitOfMeasureKey ? { key: skuUnitOfMeasureKey, name: unitOfMeasure || skuUnitOfMeasureKey, precision: 0, primary: true, priority: 0, rate: 1, incrementalOrderQuantity: 1 } : null; const hydrated = Object.assign({}, api, { catalogEntryId: catalogEntryId, productId: productId, skuId: skuId, listPrice: listPrice, promoPrice: promoPrice, finalPrice: finalPrice, unitOfMeasure: unitOfMeasure, skuUnitOfMeasure: skuUnitOfMeasure, skuStockLevels: skuStockLevels }); this.apiProduct = hydrated; try { this.renderProductDetailsFromApi(hydrated); } catch (e) { console.error('renderProductDetailsFromApi error:', e); } return hydrated; })(); return this.apiProductPromise; }, renderProductDetailsFromApi(api) { if (!api) return; const formatInt = function (n) { const num = Number(n) || 0; return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); }; const formatVnd = function (n) { const num = Number(n) || 0; return Math.round(num).toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.'); }; const escapeHtml = function (text) { if (text === null || text === undefined) return ''; return String(text) .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#039;'); }; const normalizeImageUrl = function (url) { if (!url) return ''; if (url.startsWith('http')) return url; if (url.startsWith('//')) return 'https:' + url; if (url.startsWith('/')) return (window.location.origin || '') + url; return (window.location.origin || '') + '/' + url; }; /** Danh sách URL ảnh: ưu tiên imageUrls (mảng), fallback imageUrl; loại trùng giữ thứ tự. */ const collectProductImageUrls = function (apiData) { var seen = {}; var out = []; var push = function (raw) { var u = normalizeImageUrl(raw); if (u && !seen[u]) { seen[u] = true; out.push(u); } }; if (apiData && Array.isArray(apiData.imageUrls)) { apiData.imageUrls.forEach(function (u) { if (u != null && String(u).trim() !== '') { push(String(u).trim()); } }); } if (out.length === 0 && apiData && apiData.imageUrl) { push(apiData.imageUrl); } return out; }; /** * Video từ GET /o/gocgac-commerce/v1.0/products/by-id?id=... * Trường: media.attachments[] — ví dụ: * { id, mimeType: "video/mp4", type: 1 (TYPE_OTHER), title, url: "/o/commerce-media/.../attachments/{id}?download=false" } * URL tương đối được gắn origin bằng normalizeImageUrl. */ const collectProductVideos = function (apiData) { var out = []; var seen = {}; if (!apiData || !apiData.media || !Array.isArray(apiData.media.attachments)) { return out; } apiData.media.attachments.forEach(function (att) { if (!att || !att.url) return; var raw = String(att.url).trim(); var u = normalizeImageUrl(raw); if (!u || seen[u]) return; var mime = (att.mimeType || '').toLowerCase(); var isVideoMime = mime.indexOf('video/') === 0; var isVideoExt = /\.(mp4|webm|ogg|mov|m4v)(\?|#|$)/i.test(u); var isYoutube = /youtube\.com\/|youtu\.be\//i.test(u); if (!isVideoMime && !isVideoExt && !isYoutube) return; seen[u] = true; out.push({ url: u, title: (att.title && String(att.title).trim()) || 'Video sản phẩm' }); }); return out; }; const titleEl = document.getElementById('product-title') || document.querySelector('h1'); if (titleEl && api.name) { titleEl.textContent = api.name; } const mainImage = document.getElementById('main-product-image'); var productImages = collectProductImageUrls(api); var primaryImageUrl = productImages.length > 0 ? productImages[0] : ''; if (mainImage && primaryImageUrl) { mainImage.src = primaryImageUrl; mainImage.alt = api.name || mainImage.alt || 'Product'; } // Thumbnails: render theo imageUrls (API by-id), fallback imageUrl. const thumbContainer = document.getElementById('thumbnail-container'); if (thumbContainer) { thumbContainer.innerHTML = ''; productImages.forEach(function (url, idx) { const item = document.createElement('div'); item.className = 'thumbnail-item aspect-square overflow-hidden bg-gray-100 border-2 border-emerald-500 cursor-pointer hover:border-emerald-600 transition-colors'; item.setAttribute('data-image-url', url); const img = document.createElement('img'); img.src = url; img.alt = 'Thumbnail ' + (idx + 1); img.setAttribute('data-fallback-image', url); img.onerror = function () { this.onerror = null; const fb = this.getAttribute('data-fallback-image'); if (fb && this.src !== fb) this.src = fb; }; img.className = 'w-full h-full object-cover'; item.appendChild(img); thumbContainer.appendChild(item); }); var productVideos = collectProductVideos(api); productVideos.forEach(function (vid) { var vItem = document.createElement('div'); vItem.className = 'thumbnail-item thumbnail-item--video aspect-square overflow-hidden bg-gray-900 border-2 border-emerald-500 cursor-pointer hover:border-emerald-600 transition-colors relative'; vItem.setAttribute('data-video-url', vid.url); vItem.setAttribute('data-video-title', escapeHtml(vid.title)); vItem.setAttribute('role', 'button'); vItem.setAttribute('tabindex', '0'); vItem.setAttribute('aria-label', 'Xem video: ' + (vid.title || 'Video')); var inner = document.createElement('div'); inner.className = 'absolute inset-0 flex items-center justify-center bg-gradient-to-br from-gray-800 to-gray-900'; inner.innerHTML = '<span class="text-white/90">' + '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="currentColor" class="drop-shadow-md"><path d="M8 5v14l11-7z"/></svg>' + '</span>'; vItem.appendChild(inner); var cap = document.createElement('span'); cap.className = 'absolute bottom-0 left-0 right-0 px-1 py-0.5 text-[10px] text-white/90 bg-black/50 truncate text-center'; cap.textContent = vid.title || 'Video'; vItem.appendChild(cap); thumbContainer.appendChild(vItem); }); if (typeof window.__initProductImageGallery === 'function') { window.__initProductImageGallery(); } } // Update category tags + data-product-id + data-channel-id (theo API by-id) const tagsContainer = document.querySelector('[data-product-id]'); if (tagsContainer) { const chFromApi = safeParseInt(api.channelId); if (chFromApi > 0) { tagsContainer.setAttribute('data-channel-id', String(chFromApi)); } else { tagsContainer.removeAttribute('data-channel-id'); } } const catalogEntryId = api.catalogEntryId || safeParseInt(api.id) || null; if (tagsContainer && catalogEntryId) { tagsContainer.setAttribute('data-product-id', String(catalogEntryId)); const categories = Array.isArray(api.categories) ? api.categories : []; if (categories.length > 0) { tagsContainer.innerHTML = categories.map(function (c) { var label = ''; if (typeof c === 'string') { label = c; } else if (c && typeof c === 'object') { if (typeof c.name === 'string') { label = c.name; } else if (c.name && typeof c.name === 'object') { label = c.name.vi_VN || c.name.vi || (Object.values(c.name)[0] || ''); } else { label = c.title || c.label || ''; } } return '<span class="bg-emerald-100 text-emerald-800 text-xs font-medium px-3 py-1 rounded-full hover:bg-emerald-200 transition-colors cursor-default">' + escapeHtml(label) + '</span>'; }).join(''); } else { tagsContainer.innerHTML = '<span class="bg-gray-100 text-gray-500 text-xs font-medium px-3 py-1 rounded-full cursor-default">Chưa có phân loại</span>'; } } // Price const finalPrice = Number(api.finalPrice) || 0; const listPrice = Number(api.listPrice) || 0; const promoPrice = Number(api.promoPrice) || 0; const skuId = api.skuId || ((Array.isArray(api.skuStockLevels) && api.skuStockLevels[0]) ? api.skuStockLevels[0].skuId : null) || null; const skuUomKey = (api.skuUnitOfMeasureKey || (api.skuUnitOfMeasure && api.skuUnitOfMeasure.key) || api.skuUnitOfMeasureKey || '').toString(); const unitOfMeasure = (api.unitOfMeasure || (api.skuUnitOfMeasure && api.skuUnitOfMeasure.name) || '').toString(); const totalPriceDesktop = document.getElementById('product-total-price'); const totalPriceMobile = document.getElementById('product-total-price-mobile'); const buildTotalHtmlDesktop = function () { if (finalPrice > 0) { const unitHtml = unitOfMeasure ? '<span class="text-sm lg:text-base font-normal text-emerald-500">/' + escapeHtml(unitOfMeasure) + '</span>' : ''; return formatVnd(finalPrice) + '₫' + unitHtml; } return '<span class="text-gray-500 text-base lg:text-lg">Liên hệ</span>'; }; const buildTotalHtmlMobile = function () { if (finalPrice > 0) { const unitHtml = unitOfMeasure ? '<span class="text-base sm:text-lg font-normal text-emerald-500">/' + escapeHtml(unitOfMeasure) + '</span>' : ''; return formatVnd(finalPrice) + '₫' + unitHtml; } return '<span class="text-gray-500 text-lg sm:text-xl">Liên hệ</span>'; }; if (totalPriceDesktop) { if (skuId) totalPriceDesktop.setAttribute('data-sku-id', String(skuId)); totalPriceDesktop.setAttribute('data-unit-of-measure-key', skuUomKey || ''); totalPriceDesktop.setAttribute('data-list-price', String(listPrice)); totalPriceDesktop.setAttribute('data-promo-price', String(promoPrice)); totalPriceDesktop.innerHTML = buildTotalHtmlDesktop(); } if (totalPriceMobile) { if (skuId) totalPriceMobile.setAttribute('data-sku-id', String(skuId)); totalPriceMobile.setAttribute('data-unit-of-measure-key', skuUomKey || ''); totalPriceMobile.setAttribute('data-list-price', String(listPrice)); totalPriceMobile.setAttribute('data-promo-price', String(promoPrice)); totalPriceMobile.innerHTML = buildTotalHtmlMobile(); } // Hidden unit price spans (some flows read data attributes) const unitPriceDesktop = document.getElementById('product-unit-price'); if (unitPriceDesktop) { unitPriceDesktop.setAttribute('data-unit-price', String(finalPrice)); unitPriceDesktop.textContent = String(finalPrice); } const unitPriceMobile = document.getElementById('product-unit-price-mobile'); if (unitPriceMobile) { unitPriceMobile.setAttribute('data-unit-price', String(finalPrice)); unitPriceMobile.textContent = String(finalPrice); } // List price + discount decorations const hasDiscount = listPrice > 0 && finalPrice > 0 && listPrice > finalPrice; const discountPercent = hasDiscount ? Math.round(((listPrice - finalPrice) / listPrice) * 100) : 0; const listPriceText = formatVnd(listPrice) + '₫'; const discountText = '-' + discountPercent + '%'; // Mobile decorations const totalMobileParent = totalPriceMobile ? totalPriceMobile.parentElement : null; if (totalPriceMobile && totalMobileParent) { let listEl = document.getElementById('product-list-price-mobile'); let percentEl = document.getElementById('product-discount-percent-mobile'); if (hasDiscount) { if (!listEl) { listEl = document.createElement('span'); listEl.id = 'product-list-price-mobile'; listEl.className = 'text-lg text-gray-400 line-through'; totalPriceMobile.insertAdjacentElement('afterend', listEl); } else { listEl.className = 'text-lg text-gray-400 line-through'; listEl.style.display = ''; } listEl.textContent = listPriceText; if (!percentEl) { if (listEl.nextElementSibling) { percentEl = listEl.nextElementSibling; percentEl.id = 'product-discount-percent-mobile'; } else { percentEl = document.createElement('span'); percentEl.id = 'product-discount-percent-mobile'; percentEl.className = 'text-sm px-2 py-1 bg-red-100 text-red-600 font-medium rounded'; listEl.insertAdjacentElement('afterend', percentEl); } } percentEl.className = 'text-sm px-2 py-1 bg-red-100 text-red-600 font-medium rounded'; percentEl.style.display = ''; percentEl.textContent = discountText; } else { if (listEl) listEl.style.display = 'none'; if (percentEl) percentEl.style.display = 'none'; } } // Desktop decorations const totalDesktopParent = totalPriceDesktop ? totalPriceDesktop.parentElement : null; if (totalPriceDesktop && totalDesktopParent) { let listEl = document.getElementById('product-list-price'); let percentEl = document.getElementById('product-discount-percent'); if (hasDiscount) { if (!listEl) { listEl = document.createElement('span'); listEl.id = 'product-list-price'; listEl.className = 'text-sm lg:text-base text-gray-400 line-through'; totalPriceDesktop.insertAdjacentElement('afterend', listEl); } else { listEl.className = 'text-sm lg:text-base text-gray-400 line-through'; listEl.style.display = ''; } listEl.textContent = listPriceText; if (!percentEl) { if (listEl.nextElementSibling) { percentEl = listEl.nextElementSibling; percentEl.id = 'product-discount-percent'; } else { percentEl = document.createElement('span'); percentEl.id = 'product-discount-percent'; percentEl.className = 'text-xs px-1.5 py-0.5 bg-red-100 text-red-600 font-medium rounded'; listEl.insertAdjacentElement('afterend', percentEl); } } percentEl.className = 'text-xs px-1.5 py-0.5 bg-red-100 text-red-600 font-medium rounded'; percentEl.style.display = ''; percentEl.textContent = discountText; } else { if (listEl) listEl.style.display = 'none'; if (percentEl) percentEl.style.display = 'none'; } } // Sold count const soldEl = document.querySelector('#ratingSummary .font-medium'); if (soldEl) { const soldCount = Number(api.soldCount) || 0; soldEl.textContent = formatInt(soldCount); } // Stock — ưu tiên tồn theo SKU đang chọn (skuStockLevels), fallback stockLevel (SKU primary) let stockLevelParsed = NaN; const fromSkuLevels = Array.isArray(api.skuStockLevels) && api.skuStockLevels.length ? getStockFromSkuStockLevels(api.skuStockLevels, api.skuId) : null; if (fromSkuLevels != null) { stockLevelParsed = fromSkuLevels; } else if (api.stockLevel != null && api.stockLevel !== '') { stockLevelParsed = parseInt(String(api.stockLevel), 10); } const stockLevelKnown = !isNaN(stockLevelParsed); const stockEl = document.getElementById('stock-info'); if (stockEl) { if (stockLevelKnown) { stockEl.setAttribute('data-max-quantity', String(Math.max(0, stockLevelParsed))); if (stockLevelParsed > 0) { stockEl.innerHTML = 'Còn <strong>' + formatInt(stockLevelParsed) + '</strong> sản phẩm'; } else { stockEl.innerHTML = '<span class="text-gray-600">Hết hàng</span>'; } } else { stockEl.removeAttribute('data-max-quantity'); stockEl.innerHTML = '<span class="text-gray-600"></span>'; } } // SKU code (best-effort): API hiện chỉ có skuId const skuCodeEl = document.getElementById('product-sku-code'); if (skuCodeEl) { skuCodeEl.style.display = 'none'; } // Description (nếu API có cung cấp) const descContentEl = document.getElementById('product-description-content'); if (descContentEl) { const desc = api.descriptionHtml || api.description || api.longDescription || api.detailHtml || ''; if (desc) { // Nếu string chứa HTML tag => render innerHTML, còn lại render text. if (typeof desc === 'string' && /<\/?[a-z][\s\S]*>/i.test(desc)) { descContentEl.innerHTML = desc; } else { descContentEl.textContent = String(desc); } } else { descContentEl.textContent = ''; } try { if (typeof initProductDescriptionToggle === 'function') { initProductDescriptionToggle(); } } catch (e) { } } // Enable/disable purchase buttons: cần tồn (hoặc chưa biết tồn) và phải có giá niêm yết > 0 (tránh đặt với giá 0) const inStock = !stockLevelKnown || stockLevelParsed > 0; const canPurchase = inStock && finalPrice > 0; const buttonIds = [ 'btn-add-to-cart-desktop', 'btn-add-to-cart-mobile', 'btn-buy-now-desktop', 'btn-buy-now-mobile' ]; buttonIds.forEach(function (id) { const btn = document.getElementById(id); if (!btn) return; btn.disabled = !canPurchase; }); }, /** * Đồng bộ: chỉ đọc từ apiProduct (by-id đã có skuId + skuStockLevels). Không gọi mạng. */ resolveSkuForCart(productId) { if (productId == null || productId === '') { return null; } const ap = this.apiProduct; if (!ap || String(ap.productId) !== String(productId)) { return null; } if (ap.skuId) { let selectedUom = ap.skuUnitOfMeasure || null; if (!selectedUom && ap.skuUnitOfMeasureKey) { selectedUom = { key: String(ap.skuUnitOfMeasureKey), precision: 0, incrementalOrderQuantity: 1 }; } return { id: ap.skuId, skuUnitOfMeasure: selectedUom }; } if (Array.isArray(ap.skuStockLevels) && ap.skuStockLevels.length > 0) { const firstSku = ap.skuStockLevels[0]; const uomKey = (firstSku.skuUnitOfMeasureKey || '').toString().trim(); return { id: firstSku.skuId, skuUnitOfMeasure: uomKey ? { key: uomKey, precision: 0, incrementalOrderQuantity: 1 } : null }; } return null; }, /** Chỉ await getApiProduct khi chưa có cache; SKU lấy từ resolveSkuForCart (không API SKU riêng). */ async getSkuId(productId) { try { if (!this.apiProduct && typeof this.getApiProduct === 'function') { await this.getApiProduct(); } return this.resolveSkuForCart(productId); } catch (error) { console.error('Error getting SKU:', error); throw error; } }, async getAccountChannelCart(accountIdValue, channelIdValue) { try { if (!accountIdValue || !channelIdValue) { return null; } // Tìm cart open đúng account + channel (không cache một promise chung — channelId // lần đầu có thể null/sai, khiến mọi lần sau không tìm được giỏ). const params = { page: 1, pageSize: 1, filter: '(accountId/any(x:(x eq ' + accountIdValue + '))) and (orderStatus/any(x:(x eq 2))) and (channelId eq ' + channelIdValue + ')' }; const queryString = new URLSearchParams(params).toString(); const url = APP_CONFIG.API_BASE_ORDER + '/orders?' + queryString; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include' }); if (!response.ok) { return null; } const data = await safeResponseJson(response); return data?.items && data.items.length > 0 ? data.items[0] : null; } catch (error) { console.error('Error getting account channel cart:', error); return null; } }, async createCartWithItem(accountIdValue, skuId, skuUnitOfMeasure, quantity = 1, channelIdOverride = null) { try { const cleanedUOM = cleanSkuUnitOfMeasure(skuUnitOfMeasure); const validQuantity = quantity && quantity > 0 ? quantity : 1; const channelId = safeParseInt(channelIdOverride); if (!channelId || channelId <= 0) { throw new Error('Thiếu channelId (kênh bán theo sản phẩm).'); } const cartItem = { options: '[]', quantity: validQuantity, replacedSkuId: 0, skuId: skuId }; if (cleanedUOM && cleanedUOM.key) { cartItem.skuUnitOfMeasure = cleanedUOM; } const requestBody = { accountId: accountIdValue, cartItems: [cartItem], currencyCode: 'VND', orderTypeId: null }; const url = APP_CONFIG.API_BASE_CART + '/channels/' + channelId + '/carts?nestedFields=cartItems'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include', body: JSON.stringify(requestBody) }); if (!response.ok) { const errorData = (await safeResponseJson(response)) || {}; const errorMessage = errorData.message || errorData.title || ('HTTP ' + response.status); // Retry 1 lần không kèm skuUnitOfMeasure nếu backend reject payload UOM if (response.status === 400 && cartItem.skuUnitOfMeasure) { const retryBody = { accountId: accountIdValue, cartItems: [{ options: '[]', quantity: validQuantity, replacedSkuId: 0, skuId: skuId }], currencyCode: 'VND', orderTypeId: null }; const retryRes = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include', body: JSON.stringify(retryBody) }); if (retryRes.ok) { return (await safeResponseJson(retryRes)) || {}; } } throw new Error('Không thể tạo giỏ hàng. ' + errorMessage); } return (await safeResponseJson(response)) || {}; } catch (error) { console.error('Error creating cart:', error); throw error; } }, async addItemToCart(cartId, skuId, quantity, productId, skuUnitOfMeasure) { try { const url = APP_CONFIG.API_BASE_CART + '/carts/' + cartId + '/items'; const requestBody = { options: '[]', quantity: quantity || 1, replacedSkuId: 0, skuId: skuId }; // Chỉ gửi skuUnitOfMeasure khi có key hợp lệ để tránh backend reject const cleanedUOM = cleanSkuUnitOfMeasure(skuUnitOfMeasure); if (cleanedUOM && cleanedUOM.key) { requestBody.skuUnitOfMeasure = cleanedUOM; } const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include', body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); let errorData = {}; try { errorData = JSON.parse(errorText); } catch (e) { errorData = { message: errorText || 'Unknown error' }; } // Log chi tiết lỗi để debug console.error('Add item to cart error:', { status: response.status, statusText: response.statusText, errorData: errorData, requestBody: requestBody, url: url }); // Retry không kèm skuUnitOfMeasure nếu backend từ chối UOM (giống createCartWithItem) if (response.status === 400 && requestBody.skuUnitOfMeasure) { const retryBody = { options: '[]', quantity: quantity || 1, replacedSkuId: 0, skuId: skuId }; const retryRes = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include', body: JSON.stringify(retryBody) }); if (retryRes.ok) { return (await safeResponseJson(retryRes)) || {}; } } // Với lỗi 400 (Bad Request), throw error ngay lập tức if (response.status === 400) { const errorMessage = errorData.message || errorData.title || errorData.error || 'Dữ liệu không hợp lệ. Vui lòng kiểm tra lại.'; throw new Error(errorMessage); } // Với lỗi 409 (Conflict), có thể là item đã tồn tại - xử lý riêng if (response.status === 409) { // Item có thể đã tồn tại, nhưng vẫn throw để caller quyết định const errorMessage = errorData.message || 'Sản phẩm có thể đã có trong giỏ hàng.'; throw new Error(errorMessage); } // Các lỗi khác throw new Error(errorData.message || errorData.title || errorData.error || 'Không thể thêm sản phẩm vào giỏ hàng.'); } return (await safeResponseJson(response)) || {}; } catch (error) { console.error('Error adding item to cart:', error); throw error; } }, async getCartWithItems(cartId) { try { if (!cartId) { return null; } const url = APP_CONFIG.API_BASE_CART + '/carts/' + cartId + '?nestedFields=cartItems'; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include' }); if (!response.ok) { console.error('Error fetching cart with items:', response.status, response.statusText); return null; } return (await safeResponseJson(response)) || null; } catch (error) { console.error('Error fetching cart with items:', error); return null; } }, async updateCartItemQuantity(cartId, itemId, newQuantity) { try { if (!cartId || !itemId) { throw new Error('Thiếu thông tin giỏ hàng hoặc sản phẩm trong giỏ.'); } const safeQuantity = newQuantity && newQuantity > 0 ? newQuantity : 1; // Sử dụng endpoint cart-items (đúng theo API thực tế) const url = APP_CONFIG.API_BASE_CART + '/cart-items/' + itemId; const requestBody = { quantity: safeQuantity }; const response = await fetch(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'x-csrf-token': Liferay.authToken }, credentials: 'include', body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); console.error('Update cart item quantity error:', { status: response.status, statusText: response.statusText, errorText: errorText }); throw new Error('Không thể cập nhật số lượng sản phẩm trong giỏ hàng.'); } return (await safeResponseJson(response)) || {}; } catch (error) { console.error('Error updating cart item quantity:', error); throw error; } } }; // Kick off product API hydration for UI (best-effort; does not block user actions). if (window.CommerceUtils && typeof window.CommerceUtils.getApiProduct === 'function') { window.CommerceUtils.getApiProduct().catch(function () { }); } })(); // ==================== VOUCHER SLIDER (discounts theo channel HTX) ==================== (function () { 'use strict'; var API_DISCOUNTS_BASE = '/o/gocgac-voucher/v1/discounts'; function getCsrfToken() { return (typeof Liferay !== 'undefined' && Liferay.authToken) ? Liferay.authToken : ''; } function formatDate(s) { if (!s) return '—'; try { var d = new Date(s); return isNaN(d.getTime()) ? '—' : d.toLocaleDateString('vi-VN'); } catch (e) { return '—'; } } function parseDiscountRules(items) { var lines = []; if (!items || items.length === 0) return lines; for (var i = 0; i < items.length; i++) { var r = items[i]; var type = (r.type || '').toLowerCase(); var settings = r.typeSettings || ''; if (type === 'cart-total') { var m = settings.match(/cart-total\s*=\s*([\d.,]+)/); if (m) { var valStr = m[1].trim(); var num = parseFloat(valStr.replace(/,/g, '')); if (!isNaN(num)) { lines.push('Tổng số tiền tối thiểu: ' + num.toLocaleString('vi-VN')); } } } else if (type === 'added-any') { lines.push('Áp dụng khi đơn hàng bạn có một số sản phẩm nhất định.'); } } return lines; } function openVoucherConditionsDialog(discountId, limitationTimes, limitationTimesPerAccount) { var overlay = document.getElementById('voucher-conditions-dialog-overlay'); var body = document.getElementById('voucher-conditions-dialog-body'); var emptyEl = document.getElementById('voucher-conditions-dialog-empty'); if (!overlay || !body) return; var parts = []; if (limitationTimes != null && limitationTimes !== '') parts.push('<p class="text-gray-700">Số voucher còn lại: ' + escapeHtml(String(limitationTimes)) + '</p>'); if (limitationTimesPerAccount != null && limitationTimesPerAccount !== '') parts.push('<p class="text-gray-700">Số lượt sử dụng voucher mỗi tài khoản: ' + escapeHtml(String(limitationTimesPerAccount)) + '</p>'); body.innerHTML = parts.join(''); body.classList.remove('hidden'); if (emptyEl) emptyEl.classList.add('hidden'); var headers = { 'Accept': 'application/json' }; if (getCsrfToken()) headers['x-csrf-token'] = getCsrfToken(); var url = window.location.origin + '/o/gocgac-voucher/v1/discounts/' + discountId + '/discount-rules'; fetch(url, { method: 'GET', headers: headers, credentials: 'same-origin' }) .then(function (res) { return res.ok ? res.json() : { items: [] }; }) .then(function (data) { var items = data && data.items ? data.items : []; var ruleLines = parseDiscountRules(items); var ruleHtml = ruleLines.length > 0 ? ruleLines.map(function (l) { return '<p class="text-gray-700">' + escapeHtml(l) + '</p>'; }).join('') : ''; if (ruleHtml) body.innerHTML = body.innerHTML + ruleHtml; if (!body.innerHTML.trim()) { body.classList.add('hidden'); if (emptyEl) { emptyEl.textContent = 'Không có điều kiện.'; emptyEl.classList.remove('hidden'); } } }) .catch(function () { if (!body.innerHTML.trim()) { body.classList.add('hidden'); if (emptyEl) { emptyEl.textContent = 'Không có điều kiện.'; emptyEl.classList.remove('hidden'); } } }); overlay.classList.add('show'); overlay.style.display = 'flex'; } function closeVoucherConditionsDialog() { var overlay = document.getElementById('voucher-conditions-dialog-overlay'); if (overlay) { overlay.classList.remove('show'); overlay.style.display = 'none'; } } // Gắn đóng dialog một lần khi script chạy function attachVoucherDialogClose() { var closeBtn = document.getElementById('voucher-conditions-dialog-close'); var okBtn = document.getElementById('voucher-conditions-dialog-ok'); var overlay = document.getElementById('voucher-conditions-dialog-overlay'); if (closeBtn) closeBtn.addEventListener('click', closeVoucherConditionsDialog); if (okBtn) okBtn.addEventListener('click', closeVoucherConditionsDialog); if (overlay) overlay.addEventListener('click', function (e) { if (e.target === overlay) closeVoucherConditionsDialog(); }); } function showToastSPNB(message, isError) { if (typeof window.showToastSPNB === 'function') { window.showToastSPNB(message, isError); } else { alert(message); } } async function fetchDiscountsByChannelId(channelId) { if (!channelId) return { items: [] }; var params = new URLSearchParams({ filter: '(channelId/any(x:(x eq ' + channelId + ')))', sort: 'modifiedDate:desc', page: '1', pageSize: '50', search: 'customFields\Type eq 3' }); var url = window.location.origin + API_DISCOUNTS_BASE + '?' + params.toString(); var res = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'x-csrf-token': getCsrfToken() }, credentials: 'include' }); if (!res.ok) throw new Error('Không tải được danh sách voucher'); return res.json(); } function renderVoucherCard(item, channelId) { var title = (item.title || '').trim() || 'Voucher'; var code = (item.couponCode || '').trim() || ''; var id = item.id || ''; var numberRemain = item.limitationTimesPerAccount != null ? String(item.limitationTimesPerAccount) : '0'; var limitationTimes = item.limitationTimes != null ? String(item.limitationTimes) : ''; var limitationTimesPerAccount = item.limitationTimesPerAccount != null ? String(item.limitationTimesPerAccount) : ''; var displayDate = item.displayDate; var expirationDate = item.expirationDate; var active = item.active !== false; var card = document.createElement('div'); card.className = 'voucher-card rounded-lg border border-emerald-200 bg-gradient-to-br from-emerald-50 to-white p-4 shadow-sm'; card.innerHTML = '<button type="button" class="voucher-conditions-btn btn-conditions" data-discount-id="' + escapeHtml(String(id)) + '" data-limitation-times="' + escapeHtml(limitationTimes) + '" data-limitation-times-per-account="' + escapeHtml(limitationTimesPerAccount) + '">Điều kiện</button>' + '<div class="flex flex-col h-full">' + '<h4 class="font-bold text-gray-900 mt-1 line-clamp-2">' + escapeHtml(title) + '</h4>' + '<p class="text-xs text-gray-500 mt-1">Hiệu lực từ: ' + formatDate(displayDate) + (expirationDate ? ' · Hết hạn: ' + formatDate(expirationDate) : '') + '</p>' + '<div class="mt-auto pt-3">' + '<button type="button" class="btn-voucher-save w-full py-2 px-3 rounded-md border border-emerald-600 text-emerald-600 text-sm font-medium hover:bg-emerald-50 transition-colors" ' + 'data-coupon="' + escapeHtml(code) + '" data-id="' + escapeHtml(String(id)) + '" data-channel-id="' + escapeHtml(String(channelId || '')) + '" ' + 'data-name-voucher="' + escapeHtml(title) + '" data-number-remain="' + escapeHtml(numberRemain) + '" ' + (active ? '' : 'disabled') + '>Lưu</button>' + '</div>' + '</div>'; return card; } function escapeHtml(text) { if (!text) return ''; var div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function showLoading(show) { var el = document.getElementById('voucher-loading'); if (el) el.classList.toggle('hidden', !show); } function showError(msg) { var wrap = document.getElementById('voucher-slider-wrap'); var err = document.getElementById('voucher-error'); var empty = document.getElementById('voucher-empty'); if (wrap) wrap.classList.add('hidden'); if (empty) empty.classList.add('hidden'); if (err) { err.textContent = msg || 'Không tải được voucher'; err.classList.remove('hidden'); } } function showSlider(items) { var wrap = document.getElementById('voucher-slider-wrap'); var slider = document.getElementById('voucher-slider'); var loading = document.getElementById('voucher-loading'); var error = document.getElementById('voucher-error'); var empty = document.getElementById('voucher-empty'); if (loading) loading.classList.add('hidden'); if (error) error.classList.add('hidden'); if (empty) empty.classList.add('hidden'); if (!items || items.length === 0) { if (empty) { empty.classList.remove('hidden'); if (wrap) wrap.classList.add('hidden'); } return; } if (!slider) return; slider.innerHTML = ''; items.forEach(function (item) { slider.appendChild(renderVoucherCard(item)); }); document.querySelectorAll('#voucher-slider .btn-conditions').forEach(function (btn) { btn.addEventListener('click', function () { var did = btn.getAttribute('data-discount-id'); var lim = btn.getAttribute('data-limitation-times'); var limAcc = btn.getAttribute('data-limitation-times-per-account'); if (did) openVoucherConditionsDialog(did, lim, limAcc); }); }); if (wrap) wrap.classList.remove('hidden'); // Nút prev/next (ẩn nếu ít thẻ) var prev = document.getElementById('voucher-slider-prev'); var next = document.getElementById('voucher-slider-next'); if (prev && next) { if (items.length <= 2) { prev.classList.add('hidden'); next.classList.add('hidden'); } else { prev.classList.remove('hidden'); next.classList.remove('hidden'); prev.onclick = function () { slider.scrollBy({ left: -300, behavior: 'smooth' }); }; next.onclick = function () { slider.scrollBy({ left: 300, behavior: 'smooth' }); }; } } } async function loadVouchers() { var Utils = window.CommerceUtils; if (!Utils || !Utils.getProductInfo || !Utils.fetchChannelIdByProduct) { showError('Chưa sẵn sàng lấy channel.'); return; } // Ensure API hydration (avoid reading DOM data-product-id placeholder) try { if (typeof Utils.getApiProduct === 'function') { await Utils.getApiProduct(); } } catch (e) { } var productInfo = Utils.getProductInfo(); if (!productInfo || !productInfo.productId) { showError('Không có thông tin sản phẩm.'); return; } showLoading(true); try { var channelId = await Utils.fetchChannelIdByProduct(productInfo.productId); if (!channelId) { showSlider([]); return; } var data = await fetchDiscountsByChannelId(channelId); showSlider(data.items || []); } catch (e) { console.error('Voucher load error:', e); showError(e.message || 'Không tải được voucher'); } finally { showLoading(false); } } function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { attachVoucherDialogClose(); setTimeout(loadVouchers, 500); }); } else { setTimeout(loadVouchers, 500); } } if (typeof Liferay !== 'undefined' && Liferay.on) { Liferay.on('allPortletsReady', function () { setTimeout(loadVouchers, 800); }); } init(); })(); // ==================== LOAD ORGANIZATION INFO ==================== (function () { 'use strict'; async function loadOrganizationInfo() { try { const Utils = window.CommerceUtils; if (!Utils) { console.warn('CommerceUtils chưa sẵn sàng'); return; } // Lấy trực tiếp theo productId (by-id) const apiProduct = (typeof Utils.getApiProduct === 'function') ? await Utils.getApiProduct() : null; const accountFromProduct = apiProduct && apiProduct.account ? apiProduct.account : null; if (!accountFromProduct) return; const finalAccountId = accountFromProduct.id || null; const baseUrl = window.location.origin || ''; // Cập nhật tên tổ chức const nameElement = document.getElementById('organization-name'); if (nameElement) { const displayName = accountFromProduct.name || apiProduct.supplierName || apiProduct.channelName || '—'; nameElement.textContent = displayName; } // Cập nhật địa chỉ từ API products response // Ưu tiên: account.address > account.customFields.address > firstProduct.address const locationElement = document.getElementById('organization-location'); if (locationElement) { let address = accountFromProduct.address || accountFromProduct.customFields?.address || apiProduct.address || ''; if (address) { locationElement.textContent = address; } } // Cập nhật logo/banner (nếu có trong account response) const logoContainer = document.getElementById('organization-logo-container'); if (logoContainer) { const logoUrlRaw = accountFromProduct.logoURL || accountFromProduct.logoUrl || accountFromProduct.customFields?.logoURL || accountFromProduct.customFields?.logoUrl || accountFromProduct.customFields?.banner || ''; if (logoUrlRaw) { const placeholder = document.getElementById('organization-logo-placeholder'); if (placeholder) placeholder.remove(); let logoImg = document.getElementById('organization-logo-img'); if (!logoImg) { logoImg = document.createElement('img'); logoImg.id = 'organization-logo-img'; logoImg.className = 'w-full h-full rounded-full object-cover'; logoContainer.appendChild(logoImg); } let logoUrl = logoUrlRaw; if (logoUrl && !logoUrl.startsWith('http') && !logoUrl.startsWith('//')) { logoUrl = baseUrl + logoUrl; } logoImg.src = logoUrl; logoImg.alt = accountFromProduct.name || 'Organization Logo'; } } // Cập nhật URL "Xem cửa hàng" với accountId const urlLink = document.getElementById('organization-url-link'); if (urlLink && finalAccountId) { const htxDetailUrl = baseUrl + '/en/htx-detail?id=' + finalAccountId; urlLink.href = htxDetailUrl; } // Cập nhật rating (ưu tiên pointAvg từ API product; fallback: —) const ratingEl = document.getElementById('organization-rating'); if (ratingEl) { const pointAvgRaw = apiProduct?.pointAvg; const pointAvg = (typeof pointAvgRaw === 'number') ? pointAvgRaw : Number(pointAvgRaw); ratingEl.textContent = (pointAvg && !isNaN(pointAvg)) ? pointAvg.toFixed(1) : '—'; } } catch (error) { console.error('Error loading organization info:', error); } } // Khởi tạo khi DOM ready function initOrganizationInfo() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadOrganizationInfo); } else { loadOrganizationInfo(); } } initOrganizationInfo(); })(); // ==================== ADD TO CART FUNCTIONALITY ==================== (function () { 'use strict'; let isAddingToCart = false; // Sử dụng shared utilities thay vì duplicate code const Utils = window.CommerceUtils; /** * Đồng bộ affiliateTrackingCode vào Commerce Order hiện tại sau khi add-to-cart thành công. * - Lấy trackingCode từ sessionStorage (key: AFF_TRACK) * - Lấy commerceOrderId từ Liferay.CommerceContext.commerceOrderId * - Nếu order đã có affiliateTrackingCode thì KHÔNG overwrite * - Sử dụng PATCH /o/headless-commerce-delivery-cart/v1.0/carts/{orderId} * - Có cache theo session để tránh gọi API nhiều lần cho cùng một order */ async function updateAffiliateTrackingCodeForCurrentOrder() { try { // 1) Lấy trackingCode từ sessionStorage let trackingCode = null; try { if (typeof window !== 'undefined' && window.sessionStorage) { trackingCode = window.sessionStorage.getItem('AFF_TRACK'); } } catch (e) { console.warn('[AFF] Không thể đọc sessionStorage:', e); } // 2) Nếu không có trackingCode -> không làm gì if (!trackingCode) { return; } // 3) Lấy commerceOrderId - Thử nhiều cách var orderId = null; // CÁCH 1: Ưu tiên lấy từ sessionStorage (orderId vừa được lưu sau add to cart) try { if (typeof window !== 'undefined' && window.sessionStorage) { const savedOrderId = window.sessionStorage.getItem('LAST_CART_ORDER_ID'); if (savedOrderId) { orderId = parseInt(savedOrderId, 10); if (orderId && orderId > 0) { console.log('[AFF] Lấy orderId từ sessionStorage:', orderId); } else { orderId = null; } } } } catch (e) { console.warn('[AFF] Không thể đọc sessionStorage:', e); } // CÁCH 2: Thử lấy từ Liferay.CommerceContext (nếu chưa có từ sessionStorage) if (!orderId || orderId <= 0) { if (typeof Liferay !== 'undefined' && Liferay.CommerceContext) { orderId = Liferay.CommerceContext.commerceOrderId; if (orderId && orderId > 0) { console.log('[AFF] Lấy orderId từ Liferay.CommerceContext:', orderId); } else { orderId = null; } } } // CÁCH 3: Nếu vẫn không có, lấy từ API Headless (cart hiện tại) if (!orderId || orderId <= 0) { try { const { accountId: accountIdValue, channelId } = await Utils.fetchAccountContext(); if (accountIdValue && channelId) { const cart = await Utils.getAccountChannelCart(accountIdValue, channelId); if (cart && (cart.id || cart.orderId)) { orderId = cart.id || cart.orderId; } } } catch (e) { console.error('[AFF] Lỗi khi lấy cart từ API:', e); } } if (!orderId || orderId <= 0) { return; } // 4) Tránh gọi API nhiều lần cho cùng một order trong một session try { if (typeof window !== 'undefined' && window.sessionStorage) { var cacheKey = 'AFF_ORDER_UPDATED_' + orderId; if (window.sessionStorage.getItem(cacheKey)) { return; } } } catch (e) { console.warn('[AFF] Không thể truy cập sessionStorage:', e); } // 5) Gọi API GET Order để kiểm tra customFields hiện tại (đặc biệt là affiliateTrackingCode) // Lưu ý: Cart API chỉ là view của Order với orderStatus = 2, nên cần cập nhật trực tiếp vào Order API const baseOrderUrl = APP_CONFIG && APP_CONFIG.API_BASE_ORDER ? APP_CONFIG.API_BASE_ORDER : '/o/headless-commerce-admin-order/v1.0'; const orderUrl = baseOrderUrl + '/orders/' + orderId; const getOrderRes = await fetch(orderUrl, { method: 'GET', headers: { 'Accept': 'application/json', 'x-csrf-token': (typeof Liferay !== 'undefined' ? Liferay.authToken : '') }, credentials: 'include' }); if (!getOrderRes.ok) { const errorText = await getOrderRes.text().catch(function () { return ''; }); console.error('[AFF] Lỗi GET order để kiểm tra affiliateTrackingCode:', getOrderRes.status, errorText); return; } const orderData = await getOrderRes.json(); // affiliateTrackingCode là field ở root level, không phải trong customFields const currentAffiliateCode = orderData && orderData.affiliateTrackingCode ? orderData.affiliateTrackingCode : null; // 6) Không overwrite nếu order đã có affiliateTrackingCode if (currentAffiliateCode && currentAffiliateCode.trim() !== '') { try { if (typeof window !== 'undefined' && window.sessionStorage) { window.sessionStorage.setItem('AFF_ORDER_UPDATED_' + orderId, 'true'); } } catch (e) { } return; } // 7) Chuẩn bị body PATCH: affiliateTrackingCode là custom field trong customFields const patchBody = { customFields: { ...(orderData.customFields || {}), affiliateTrackingCode: trackingCode } }; // 8) Gọi PATCH Order API để cập nhật custom field affiliateTrackingCode const patchOrderRes = await fetch(orderUrl, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'x-csrf-token': (typeof Liferay !== 'undefined' ? Liferay.authToken : '') }, credentials: 'include', body: JSON.stringify(patchBody) }); if (!patchOrderRes.ok) { const errorTextPatch = await patchOrderRes.text().catch(function () { return ''; }); console.error('[AFF] Lỗi PATCH order để cập nhật affiliateTrackingCode:', patchOrderRes.status, errorTextPatch); return; } // 9) Sau khi cập nhật thành công, xóa AFF_TRACK khỏi sessionStorage try { if (typeof window !== 'undefined' && window.sessionStorage) { window.sessionStorage.removeItem('AFF_TRACK'); window.sessionStorage.removeItem('AFF_TRACK_USER_ID'); window.sessionStorage.setItem('AFF_ORDER_UPDATED_' + orderId, 'true'); } } catch (e) { console.warn('[AFF] Không thể cập nhật/xóa dữ liệu AFF trong sessionStorage:', e); } } catch (error) { console.error('[AFF] Lỗi khi cập nhật affiliateTrackingCode cho order hiện tại:', error); } } async function addToCart() { if (isAddingToCart) return; if (!Utils) { showToastSPNB('Hệ thống đang khởi tạo. Vui lòng thử lại sau.', true); return; } // Ensure API hydration (avoid reading DOM placeholder data-product-id) try { if (typeof Utils.getApiProduct === 'function') { await Utils.getApiProduct(); } } catch (e) { } // Kiểm tra đăng nhập trước let userId = 0; if (typeof Liferay !== 'undefined' && Liferay.ThemeDisplay) { userId = Liferay.ThemeDisplay.isSignedIn() ? (Liferay.ThemeDisplay.getUserId() || 0) : 0; } // Nếu chưa đăng nhập, redirect đến trang login if (!userId || userId === 0) { window.location.href = '/login'; return; } const productInfo = Utils.getProductInfo(); if (!productInfo || !productInfo.productId) { showToastSPNB('Không thể lấy thông tin sản phẩm. Vui lòng thử lại sau.', true); return; } // Ưu tiên lấy từ mobile input nếu có, nếu không thì lấy từ desktop const quantityInputMobile = document.getElementById('quantity-input-mobile'); const quantityInput = document.getElementById('quantity-input'); const quantityInputToUse = quantityInputMobile || quantityInput; if (!quantityInputToUse) { showToastSPNB('Không tìm thấy input số lượng. Vui lòng thử lại sau.', true); return; } const quantity = parseInt(quantityInputToUse.value) || 1; if (quantity < 1) { showToastSPNB('Số lượng phải lớn hơn 0.', true); return; } const { accountId: accountIdValue } = await Utils.fetchAccountContext(); if (!accountIdValue) { // Nếu không lấy được accountId, redirect đến trang login window.location.href = '/login'; return; } // Kênh bán: chỉ từ product (by-id / Admin catalog), không fallback kênh cố định const channelFromApi = safeParseInt(Utils && Utils.apiProduct ? Utils.apiProduct.channelId : null); const channelFromProduct = await Utils.fetchChannelIdByProduct(productInfo.productId); const channelIdToUse = channelFromProduct || channelFromApi; if (!channelIdToUse) { showToastSPNB('Không xác định được kênh bán hàng (channel).', true); return; } isAddingToCart = true; const btnAddToCartDesktop = document.getElementById('btn-add-to-cart-desktop'); const btnAddToCartMobile = document.getElementById('btn-add-to-cart-mobile'); const addToCartTextDesktop = document.getElementById('add-to-cart-text-desktop'); const addToCartTextMobile = document.getElementById('add-to-cart-text-mobile'); if (btnAddToCartDesktop) btnAddToCartDesktop.disabled = true; if (btnAddToCartMobile) btnAddToCartMobile.disabled = true; if (addToCartTextDesktop) addToCartTextDesktop.textContent = 'Đang thêm...'; if (addToCartTextMobile) addToCartTextMobile.textContent = 'Đang thêm...'; let success = false; try { // Ưu tiên SKU đang chọn trên UI (variant selector đã sync vào DOM) const totalPriceEl = document.getElementById('product-total-price') || document.getElementById('product-total-price-mobile'); let skuId = null; let skuUnitOfMeasure = null; if (totalPriceEl) { const skuIdAttr = (totalPriceEl.getAttribute('data-sku-id') || '').toString().replace(/[^\d]/g, ''); if (skuIdAttr) { skuId = safeParseInt(skuIdAttr); } const skuUomKeyAttr = (totalPriceEl.getAttribute('data-unit-of-measure-key') || '').trim(); if (skuUomKeyAttr) { skuUnitOfMeasure = { key: skuUomKeyAttr, precision: 0, incrementalOrderQuantity: 1 }; } } // Fallback: cùng nguồn by-id (resolveSkuForCart) — không gọi thêm API SKU if (!skuId) { const skuData = typeof Utils.resolveSkuForCart === 'function' ? Utils.resolveSkuForCart(productInfo.productId) : null; if (!skuData || !skuData.id) { throw new Error('Không tìm thấy SKU cho sản phẩm này.'); } skuId = skuData.id; if (!skuUnitOfMeasure) { skuUnitOfMeasure = skuData.skuUnitOfMeasure; } } try { if (typeof Utils.getEffectiveStockLevel === 'function') { const eff = Utils.getEffectiveStockLevel(skuId); if (eff != null && eff <= 0) { showToastSPNB('Sản phẩm đã hết hàng', true); return; } } } catch (e) { } if (typeof Utils.getEffectiveFinalPrice === 'function') { const unitPrice = Utils.getEffectiveFinalPrice(); if (!unitPrice || unitPrice <= 0) { showToastSPNB('Sản phẩm chưa có giá niêm yết. Vui lòng liên hệ.', true); return; } } // Không ép UOM mặc định — để cart API dùng UOM của SKU (tránh 400 do sai key). // Kiểm tra giỏ hàng hiện tại let cart = await Utils.getAccountChannelCart(accountIdValue, channelIdToUse); let currentOrderId = null; if (!cart || !cart.id) { // Tạo giỏ hàng mới kèm sản phẩm try { const newCart = await Utils.createCartWithItem(accountIdValue, skuId, skuUnitOfMeasure, quantity, channelIdToUse); if (newCart && (newCart.id || newCart.orderId)) { currentOrderId = newCart.id || newCart.orderId; success = true; } } catch (createErr) { // Không dùng giỏ mở không lọc channelId — sẽ gộp nhầm sang đơn kênh khác (HTX khác) const recoveredCart = await Utils.getAccountChannelCart(accountIdValue, channelIdToUse); if (recoveredCart && (recoveredCart.id || recoveredCart.orderId)) { cart = recoveredCart; } else { throw createErr; } } } if (cart && (cart.id || cart.orderId)) { const cartId = cart.id || cart.orderId; currentOrderId = cartId; const cartWithItems = await Utils.getCartWithItems(cartId) || cart; const cartItems = cartWithItems.cartItems || []; const existingItem = cartItems.find(item => item && item.skuId === skuId && (!skuUnitOfMeasure || !item.skuUnitOfMeasure || item.skuUnitOfMeasure.key === skuUnitOfMeasure.key) ); if (existingItem) { const newQty = (parseInt(existingItem.quantity) || 0) + quantity; await Utils.updateCartItemQuantity(cartId, existingItem.id || existingItem.itemId, newQty); } else { await Utils.addItemToCart(cartId, skuId, quantity, null, skuUnitOfMeasure); } success = true; } if (currentOrderId && typeof window !== 'undefined' && window.sessionStorage) { window.sessionStorage.setItem('LAST_CART_ORDER_ID', currentOrderId); } } catch (err) { console.error('Add to cart error:', err); success = false; // Kiểm tra xem có phải lỗi do item đã tồn tại không const errorMsg = err.message || ''; const errorMsgLower = errorMsg.toLowerCase(); // Một số trường hợp có thể coi là thành công (item đã có trong cart) if (errorMsgLower.includes('already') || errorMsgLower.includes('đã tồn tại') || errorMsgLower.includes('duplicate') || errorMsgLower.includes('đã có trong giỏ hàng')) { // Item đã tồn tại, coi như thành công success = true; window._cartAlertShown = true; // Đánh dấu đã hiển thị toast showToastSPNB('Sản phẩm đã có trong giỏ hàng!', false); } else { // Hiển thị lỗi thực sự window._cartAlertShown = true; // Đánh dấu đã hiển thị toast showToastSPNB(errorMsg, true); } } finally { isAddingToCart = false; const btnAddToCartDesktop = document.getElementById('btn-add-to-cart-desktop'); const btnAddToCartMobile = document.getElementById('btn-add-to-cart-mobile'); const addToCartTextDesktop = document.getElementById('add-to-cart-text-desktop'); const addToCartTextMobile = document.getElementById('add-to-cart-text-mobile'); const stockInfoEl = document.getElementById('stock-info'); let allowPurchaseNow = true; if (stockInfoEl) { const maxQtyNow = safeParseInt( stockInfoEl.getAttribute('data-max-quantity'), APP_CONFIG.MAX_QUANTITY_DEFAULT ); if (maxQtyNow === 0) { allowPurchaseNow = false; } } if (btnAddToCartDesktop) btnAddToCartDesktop.disabled = !allowPurchaseNow; if (btnAddToCartMobile) btnAddToCartMobile.disabled = !allowPurchaseNow; if (addToCartTextDesktop) addToCartTextDesktop.textContent = 'Thêm vào giỏ hàng'; if (addToCartTextMobile) addToCartTextMobile.textContent = 'Thêm vào giỏ'; // Chỉ hiển thị thông báo thành công nếu đã thành công VÀ chưa hiển thị toast trong catch if (success && !window._cartAlertShown) { showToastSPNB('Đã thêm sản phẩm vào giỏ hàng thành công!', false); } // Sau khi add-to-cart thành công, cố gắng gắn affiliateTrackingCode vào order hiện tại (nếu có) if (success) { window.updateStaticViewProductsAddToCart?.().catch(function (err) { console.error('[STATIC_VIEW_ADD_TO_CART] error:', err); }); // Không await để tránh chặn UI; mọi lỗi được log bên trong hàm updateAffiliateTrackingCodeForCurrentOrder().catch(function (err) { console.error('[AFF] Lỗi ngoài khi gọi updateAffiliateTrackingCodeForCurrentOrder:', err); }); } // Reset flag window._cartAlertShown = false; } } function initAddToCartButton() { // Desktop button const btnAddToCartDesktop = document.getElementById('btn-add-to-cart-desktop'); if (btnAddToCartDesktop) { const newBtnDesktop = btnAddToCartDesktop.cloneNode(true); btnAddToCartDesktop.parentNode.replaceChild(newBtnDesktop, btnAddToCartDesktop); newBtnDesktop.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); addToCart(); }); } // Mobile button const btnAddToCartMobile = document.getElementById('btn-add-to-cart-mobile'); if (btnAddToCartMobile) { const newBtnMobile = btnAddToCartMobile.cloneNode(true); btnAddToCartMobile.parentNode.replaceChild(newBtnMobile, btnAddToCartMobile); newBtnMobile.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); addToCart(); }); } } // Chỉ init một lần để tránh duplicate let addToCartInitialized = false; function tryInitAddToCart() { if (addToCartInitialized) return; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { initAddToCartButton(); addToCartInitialized = true; }); } else { initAddToCartButton(); addToCartInitialized = true; } } tryInitAddToCart(); // Hỗ trợ Liferay events - chỉ init một lần if (typeof Liferay !== 'undefined') { if (Liferay.on && !addToCartInitialized) { Liferay.on('allPortletsReady', function () { if (!addToCartInitialized) { setTimeout(function () { initAddToCartButton(); addToCartInitialized = true; }, 500); } }); } } })();
'; return; } if (skus.length === 1) { syncNoVariantBadge(false); section.classList.add('hidden'); return; } syncNoVariantBadge(true); container.innerHTML = ''; section.classList.remove('hidden'); var currentSkuId = window.CommerceUtils && window.CommerceUtils.apiProduct ? parseInt(window.CommerceUtils.apiProduct.skuId, 10) : null; var selectedIdx = 0; if (currentSkuId) { var found = skus.findIndex(function (s) { return parseInt(s.id, 10) === currentSkuId; }); if (found >= 0) selectedIdx = found; } skus.forEach(function (sku, idx) { var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'px-3 py-1.5 rounded border text-sm transition-colors ' + (idx === selectedIdx ? 'border-emerald-600 text-emerald-700 bg-emerald-50' : 'border-gray-300 text-gray-700 hover:border-emerald-400'); btn.textContent = getSkuLabel(sku, idx); btn.addEventListener('click', function () { Array.prototype.forEach.call(container.children, function (el) { el.className = 'px-3 py-1.5 rounded border text-sm transition-colors border-gray-300 text-gray-700 hover:border-emerald-400'; }); btn.className = 'px-3 py-1.5 rounded border text-sm transition-colors border-emerald-600 text-emerald-700 bg-emerald-50'; applySkuToUi(sku); }); container.appendChild(btn); }); applySkuToUi(skus[selectedIdx]); } /** Đồng bộ: map từ apiProduct (by-id) — không gọi API SKU / Admin. */ function buildVariantSkusFromApiProduct(ap) { if (!ap) { return { skus: [], resolvedProductId: null }; } try { if (ap.skuStockLevels && ap.skuStockLevels.length > 0) { return { skus: ap.skuStockLevels.map(function (row) { // Giá từng SKU độc lập: 0 = chưa niêm yết, không gộp giá cấp sản phẩm (tránh SIZE S hiện giá SIZE M) var rp = row.price; var rpp = row.promoPrice; var rowList = (rp !== undefined && rp !== null && !isNaN(Number(rp))) ? Number(rp) : NaN; var rowPromo = (rpp !== undefined && rpp !== null && !isNaN(Number(rpp))) ? Number(rpp) : NaN; return { id: row.skuId, sku: row.sku || ('SKU ' + row.skuId), price: !isNaN(rowList) ? rowList : (Number(ap.listPrice) || Number(ap.price) || 0), promoPrice: !isNaN(rowPromo) ? rowPromo : (Number(ap.promoPrice) || 0), stockLevel: row.stockLevel, skuUnitOfMeasureKey: row.skuUnitOfMeasureKey || ap.skuUnitOfMeasureKey, skuUnitOfMeasures: (row.skuUnitOfMeasureKey || ap.skuUnitOfMeasureKey) ? [{ key: row.skuUnitOfMeasureKey || ap.skuUnitOfMeasureKey, name: row.unitOfMeasure || row.skuUnitOfMeasureKey || ap.skuUnitOfMeasureKey, primary: true }] : (ap.skuUnitOfMeasure ? [ap.skuUnitOfMeasure] : []) }; }), resolvedProductId: ap.productId || null }; } if (ap.skuId) { return { skus: [{ id: ap.skuId, sku: 'SKU ' + ap.skuId, price: ap.price, promoPrice: ap.promoPrice, stockLevel: ap.stockLevel, skuUnitOfMeasureKey: ap.skuUnitOfMeasureKey, skuUnitOfMeasures: ap.skuUnitOfMeasure ? [ap.skuUnitOfMeasure] : [] }], resolvedProductId: ap.productId || null }; } } catch (e) { } return { skus: [], resolvedProductId: null }; } async function initVariantSelector() { try { if (!window.CommerceUtils || typeof window.CommerceUtils.getApiProduct !== 'function') return; var apiProduct = await window.CommerceUtils.getApiProduct(); if (!apiProduct || !apiProduct.productId) return; var result = buildVariantSkusFromApiProduct(apiProduct); var enrichedSkus = enrichSkuInventoryLevels(result.skus || []); renderSkuOptions(enrichedSkus); } catch (e) { console.warn('initVariantSelector error:', e); var section = document.getElementById('sku-variant-section'); var container = document.getElementById('sku-variant-options'); if (section && container) { section.classList.remove('hidden'); container.innerHTML = 'Không tải được phân loại. '; } } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initVariantSelector); } else { initVariantSelector(); } })();

Đảm bảo chất lượng

Giao hàng nhanh

Đóng gói cẩn thận

Thông số kỹ thuật

Thông tin chi tiết về sản phẩm

Đang tải thông số kỹ thuật...

🏢

Đang tải...

—
Xem cửa hàng
Đang tải...

Voucher của cửa hàng

Chọn voucher và nhấn Lưu để dùng khi thanh toán

Đánh giá sản phẩm

0

Dựa trên 0 đánh giá

Gợi ý sản phẩm

Sản phẩm tương tự