0
Đang tải...
Đang tải...
Mã SKU: Đang tải...
|
Đã bán 0
|
|
Truy xuất nguồn gốc
0
Đang tải...
Thêm vào giỏ hàng
' +
'' + 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 += '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 += '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 += '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 += '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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
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.
Đả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...
Không thể tải thông số kỹ thuật
Đang tải...
Voucher của cửa hàng
Chọn voucher và nhấn Lưu để dùng khi thanh toán
Chưa có voucher nào cho
cửa hàng này.
0
Đang tải...
Đang tải...
Mã SKU: Đang tải...
0
Đang tải...
';
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 += '
';
html += '' +
'' + escapedLabel + ':' +
'' + escapedValue + '' +
'
';
});
html += 'Đang tải thông tin...
Không thể tải thông tin sản phẩm
' + '' + escapeHtml(errorMsg) + '
' + '';
html += '
';
// Thông tin nhà sản xuất/cửa hàng
if (account) {
html += ''; html += ''; html += 'Thông tin sản phẩm'; html += '
'; 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 catalog
if (catalog) {
html += ''; html += ''; html += 'Thông tin nhà sản xuất'; html += '
'; 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 truy xuất
html += ''; html += ''; html += 'Thông tin danh mục'; html += '
'; html += '';
html += '
';
html += 'Tên danh mục:' + escapeHtml(catalog.name?.vi_VN || catalog.name || 'N/A') + '
';
html += '';
html += '
';
html += ''; html += ''; html += 'Thông tin truy xuất'; html += '
'; html += '';
const traceDate = new Date().toLocaleString('vi-VN');
html += '
';
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 += '';
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.
Đả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...
Không thể tải thông số kỹ thuật
Đang tải...
Voucher của cửa hàng
Chọn voucher và nhấn Lưu để dùng khi thanh toán
Chưa có voucher nào cho
cửa hàng này.
Đánh giá sản phẩm
0
Dựa trên 0 đánh giá
Đánh giá sản phẩm
0
Dựa trên 0 đánh giá