| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| (function(){ |
| 'use strict'; |
|
|
| |
| window.__VAI_EXCEL_FIX = window.__VAI_EXCEL_FIX || { |
| blobRegistry: [], |
| isUnloading: false |
| }; |
|
|
| var g = window.__VAI_EXCEL_FIX; |
|
|
| |
| function cleanupAllBlobs() { |
| g.isUnloading = true; |
| g.blobRegistry.forEach(function(entry) { |
| if (!entry.revoked && entry.url) { |
| try { URL.revokeObjectURL(entry.url); entry.revoked = true; } catch(e) {} |
| } |
| }); |
| g.blobRegistry = []; |
| } |
|
|
| window.addEventListener('pagehide', cleanupAllBlobs); |
| window.addEventListener('beforeunload', cleanupAllBlobs); |
| window.addEventListener('unload', cleanupAllBlobs); |
|
|
| |
| if (!window.__VAI_REVOKE_PATCHED) { |
| var originalRevoke = URL.revokeObjectURL; |
| URL.revokeObjectURL = function(url) { |
| |
| if (g.isUnloading || !url || typeof url !== 'string' || !url.startsWith('blob:')) { |
| return originalRevoke.call(this, url); |
| } |
| |
| var exists = g.blobRegistry.find(function(e) { return e.url === url; }); |
| if (!exists) { |
| g.blobRegistry.push({ url: url, revoked: false, created: Date.now() }); |
| } |
| return undefined; |
| }; |
| window.__VAI_REVOKE_PATCHED = true; |
| } |
|
|
| |
| function fmt(n) { |
| if (!n || isNaN(n)) return '0đ'; |
| return Number(n).toLocaleString('vi-VN') + 'đ'; |
| } |
|
|
| function stripDiacritics(s) { |
| return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[Đđ]/g, 'D'); |
| } |
|
|
| function generateOrderCode(name) { |
| var d = new Date(); |
| var dd = String(d.getDate()).padStart(2, '0'); |
| var mm = String(d.getMonth() + 1).padStart(2, '0'); |
| var yy = String(d.getFullYear()).slice(-2); |
| var ini = ''; |
| if (name && name.trim()) { |
| name.trim().split(/\s+/).forEach(function(w) { |
| if (w) { |
| var ch = stripDiacritics(w.charAt(0)).toUpperCase(); |
| if (/[A-Z]/.test(ch)) { |
| ini += ch; |
| } |
| } |
| }); |
| } |
| return 'VAS' + (ini || 'X') + dd + mm + yy; |
| } |
|
|
| |
| function setBtnState(btn, exporting) { |
| if (!btn) return; |
| if (exporting) { |
| btn.disabled = true; |
| btn.dataset.vaiExporting = '1'; |
| btn.dataset.vaiOrigHTML = btn.innerHTML; |
| btn.innerHTML = '<span style="opacity:0.7">⏳ Đang xuất...</span>'; |
| } else { |
| btn.disabled = false; |
| btn.dataset.vaiExporting = '0'; |
| if (btn.dataset.vaiOrigHTML) { |
| btn.innerHTML = btn.dataset.vaiOrigHTML; |
| } |
| } |
| } |
|
|
| |
| function downloadBlob(blob, filename) { |
| var url = URL.createObjectURL(blob); |
| g.blobRegistry.push({ url: url, revoked: false }); |
| |
| var a = document.createElement('a'); |
| a.href = url; |
| a.download = filename; |
| a.style.cssText = 'position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;'; |
| document.body.appendChild(a); |
| a.click(); |
| |
| setTimeout(function() { |
| try { document.body.removeChild(a); } catch(e) {} |
| }, 100); |
| } |
|
|
| |
| function getQuoteData() { |
| var data = { |
| customer: { name: '', phone: '', email: '', addr: '', date: new Date().toLocaleDateString('vi-VN') }, |
| items: [], |
| grandTotal: 0 |
| }; |
| |
| |
| var cart = window.cart || []; |
| var products = window.D || []; |
| |
| cart.forEach(function(item, i) { |
| var product = products[item.idx] || (products.find ? products.find(function(p) { return p.id === item.id; }) : null); |
| var price = item.priceNum || item.price || (product ? product.price : 0) || 0; |
| data.items.push({ |
| stt: i + 1, |
| image: product ? product.img : '', |
| name: product ? (product.name || product.n || '') : (item.name || ''), |
| model: product ? (product.sku || product.mod || '') : (item.sku || item.model || ''), |
| specs: product ? (product.info || product.desc || '') : '', |
| qty: item.qty || 1, |
| price: price, |
| discPrice: price, |
| total: (item.qty || 1) * price, |
| note: '' |
| }); |
| }); |
| |
| |
| var nameSelectors = ['#quoteName', '#quoteCustomerName', '#checkoutName']; |
| for (var j = 0; j < nameSelectors.length; j++) { |
| var el = document.querySelector(nameSelectors[j]); |
| if (el && el.value && el.value.trim()) { |
| data.customer.name = el.value.trim(); |
| break; |
| } |
| } |
| |
| var phoneEl = document.querySelector('#quotePhone, #checkoutPhone'); |
| if (phoneEl && phoneEl.value) data.customer.phone = phoneEl.value.trim(); |
| |
| var emailEl = document.querySelector('#quoteEmail'); |
| if (emailEl && emailEl.value) data.customer.email = emailEl.value.trim(); |
| |
| var addrEl = document.querySelector('#quoteAddr, #checkoutAddr'); |
| if (addrEl && addrEl.value) data.customer.addr = addrEl.value.trim(); |
| |
| data.grandTotal = data.items.reduce(function(s, i) { return s + (i.total || 0); }, 0); |
| return data; |
| } |
|
|
| |
| async function exportQuoteExcel(quoteData, code) { |
| if (typeof ExcelJS === 'undefined') { |
| if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS'); |
| return false; |
| } |
| |
| try { |
| var wb = new ExcelJS.Workbook(); |
| var ws = wb.addWorksheet('Báo giá'); |
| ws.views = [{ showGridLines: false }]; |
| ws.columns = [ |
| { width: 5 }, { width: 11 }, { width: 28 }, |
| { width: 13 }, { width: 20 }, { width: 6 }, |
| { width: 13 }, { width: 13 }, { width: 15 }, { width: 14 } |
| ]; |
| |
| ws.mergeCells('A3:J3'); |
| ws.getCell('A3').value = 'BẢNG BÁO GIÁ'; |
| ws.getCell('A3').font = { bold: true, size: 18, color: { argb: 'FFDB9815' } }; |
| ws.getCell('A3').alignment = { horizontal: 'center' }; |
| |
| ws.getCell('A5').value = 'Khách hàng:'; |
| ws.mergeCells('B5:E5'); |
| ws.getCell('B5').value = quoteData.customer?.name || ''; |
| |
| ws.getCell('H5').value = 'Mã đơn:'; |
| ws.mergeCells('I5:J5'); |
| ws.getCell('I5').value = code; |
| |
| ws.getCell('A6').value = 'SĐT:'; |
| ws.mergeCells('B6:E6'); |
| ws.getCell('B6').value = quoteData.customer?.phone || ''; |
| |
| ws.getCell('H6').value = 'Ngày:'; |
| ws.mergeCells('I6:J6'); |
| ws.getCell('I6').value = quoteData.customer?.date || ''; |
| |
| ws.getCell('A7').value = 'Email:'; |
| ws.mergeCells('B7:E7'); |
| ws.getCell('B7').value = quoteData.customer?.email || ''; |
| |
| ws.getCell('A8').value = 'Địa chỉ:'; |
| ws.mergeCells('B8:J8'); |
| ws.getCell('B8').value = quoteData.customer?.addr || ''; |
| |
| var hr = ws.getRow(10); |
| ['STT', 'Hình', 'Tên sản phẩm', 'Mã SP', 'Thông tin', 'SL', 'Đơn giá', 'Giá CK', 'Thành tiền', 'Ghi chú'].forEach(function(h, i) { |
| var c = hr.getCell(i + 1); |
| c.value = h; |
| c.font = { bold: true, color: { argb: 'FFFFFFFF' } }; |
| c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; |
| }); |
| |
| var cr = 11, firstRow = cr; |
| (quoteData.items || []).forEach(function(it, i) { |
| var row = ws.getRow(cr); |
| row.height = 50; |
| var bg = i % 2 === 0 ? 'FFF8FAFC' : 'FFFFFFFF'; |
| |
| row.getCell(1).value = it.stt || i + 1; |
| row.getCell(3).value = it.name || ''; |
| row.getCell(3).font = { bold: true, size: 9 }; |
| row.getCell(4).value = it.model || ''; |
| row.getCell(5).value = it.specs || ''; |
| row.getCell(5).font = { size: 8, color: { argb: 'FF64748B' } }; |
| row.getCell(6).value = Number(it.qty || 1); |
| row.getCell(6).numFmt = '#,##0'; |
| row.getCell(7).value = Number(it.price || 0); |
| row.getCell(7).numFmt = '#,##0"đ"'; |
| row.getCell(8).value = Number(it.discPrice || it.price || 0); |
| row.getCell(8).numFmt = '#,##0"đ"'; |
| if ((it.price || 0) > (it.discPrice || 0)) { |
| row.getCell(8).font = { bold: true, color: { argb: 'FFDC3545' } }; |
| } |
| row.getCell(9).value = Number(it.total || 0); |
| row.getCell(9).numFmt = '#,##0"đ"'; |
| row.getCell(9).font = { bold: true }; |
| row.getCell(10).value = it.note || ''; |
| |
| cr++; |
| }); |
| |
| ws.getRow(cr).height = 28; |
| ws.mergeCells(cr, 1, cr, 5); |
| ws.getCell('A' + cr).value = 'TỔNG CỘNG'; |
| ws.getCell('A' + cr).font = { bold: true, size: 13, color: { argb: 'FFFFFFFF' } }; |
| ws.getCell('A' + cr).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; |
| ws.getCell('E' + cr).value = { formula: 'SUM(I' + firstRow + ':I' + (cr - 1) + ')', result: Number(quoteData.grandTotal || 0) }; |
| ws.getCell('E' + cr).numFmt = '#,##0"đ"'; |
| ws.getCell('E' + cr).font = { bold: true, size: 14, color: { argb: 'FFF0B840' } }; |
| |
| wb.calcProperties.fullCalcOnLoad = true; |
| |
| var buf = await wb.xlsx.writeBuffer(); |
| var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); |
| |
| downloadBlob(blob, code + '.xlsx'); |
| return true; |
| |
| } catch (err) { |
| console.error('[VAI Excel Fix] exportQuoteExcel error:', err); |
| alert('❌ Lỗi: ' + err.message); |
| return false; |
| } |
| } |
|
|
| |
| async function exportDeliveryExcel(quoteData, code) { |
| if (typeof ExcelJS === 'undefined') { |
| if (typeof showToast === 'function') showToast('⚠️ Chưa có thư viện ExcelJS'); |
| return false; |
| } |
| |
| try { |
| var wb = new ExcelJS.Workbook(); |
| var ws = wb.addWorksheet('Giao hàng'); |
| ws.views = [{ showGridLines: false }]; |
| ws.columns = [ |
| { width: 5 }, { width: 34 }, { width: 14 }, { width: 8 }, { width: 20 } |
| ]; |
| |
| ws.mergeCells('A1:E1'); |
| ws.getCell('A1').value = 'PHIẾU GIAO HÀNG'; |
| ws.getCell('A1').font = { bold: true, size: 16, color: { argb: 'FF003F62' } }; |
| ws.getCell('A1').alignment = { horizontal: 'center' }; |
| |
| ws.getCell('A2').value = 'KH: ' + (quoteData.customer?.name || ''); |
| ws.getCell('D2').value = 'Mã: ' + code; |
| ws.getCell('A3').value = 'SĐT: ' + (quoteData.customer?.phone || ''); |
| ws.getCell('D3').value = 'Ngày: ' + (quoteData.customer?.date || ''); |
| ws.getCell('A4').value = 'Địa chỉ: ' + (quoteData.customer?.addr || ''); |
| |
| var hr = ws.getRow(6); |
| ['STT', 'Tên sản phẩm', 'Mã SP', 'SL', 'Ghi chú'].forEach(function(h, i) { |
| var c = hr.getCell(i + 1); |
| c.value = h; |
| c.font = { bold: true, color: { argb: 'FFFFFFFF' } }; |
| c.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF003F62' } }; |
| }); |
| |
| var cr = 7, totalQty = 0; |
| (quoteData.items || []).forEach(function(it, i) { |
| var row = ws.getRow(cr); |
| row.height = 20; |
| row.getCell(1).value = it.stt || i + 1; |
| row.getCell(2).value = it.name || ''; |
| row.getCell(3).value = it.model || ''; |
| row.getCell(4).value = it.qty || 1; |
| row.getCell(5).value = it.note || ''; |
| totalQty += Number(it.qty || 1); |
| cr++; |
| }); |
| |
| ws.mergeCells(cr, 1, cr, 3); |
| ws.getCell('A' + cr).value = 'TỔNG CỘNG SỐ LƯỢNG'; |
| ws.getCell('D' + cr).value = totalQty; |
| |
| var buf = await wb.xlsx.writeBuffer(); |
| var blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); |
| |
| downloadBlob(blob, 'GH-' + code + '.xlsx'); |
| return true; |
| |
| } catch (err) { |
| console.error('[VAI Excel Fix] exportDeliveryExcel error:', err); |
| alert('❌ Lỗi: ' + err.message); |
| return false; |
| } |
| } |
|
|
| |
| function handleExportClick(e) { |
| var btn = e.target.closest('button, a'); |
| if (!btn) return; |
| |
| var txt = (btn.textContent || '').toLowerCase(); |
| |
| |
| if (txt.includes('excel') && !txt.includes('giao hàng') && !txt.includes('gh-') && !txt.includes('giao')) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| if (btn.dataset.vaiExporting === '1') return; |
| setBtnState(btn, true); |
| |
| var qd = getQuoteData(); |
| var code = generateOrderCode(qd.customer?.name || ''); |
| |
| exportQuoteExcel(qd, code).then(function(success) { |
| setBtnState(btn, false); |
| if (success && typeof showToast === 'function') { |
| showToast('✅ Excel báo giá tải thành công!'); |
| } |
| }); |
| |
| return false; |
| } |
| |
| |
| if ((txt.includes('gh') || txt.includes('giao hàng')) && txt.includes('excel')) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| |
| if (btn.dataset.vaiExporting === '1') return; |
| setBtnState(btn, true); |
| |
| |
| var order = window.VAI_CURRENT_ORDER; |
| var exportData, orderCode; |
| |
| if (order && order.items) { |
| |
| exportData = { |
| customer: { name: order.customer || '', phone: order.phone || '', addr: order.addr || '', date: order.date || '' }, |
| items: (order.items || []).map(function(it, i) { |
| return { |
| stt: i + 1, |
| name: it.name || '', |
| model: it.model || '', |
| qty: it.qty || 1, |
| note: it.note || '', |
| specs: it.specs || '', |
| price: it.price || 0, |
| discPrice: it.discPrice || it.price || 0, |
| total: it.total || 0 |
| }; |
| }), |
| grandTotal: order.grandTotal || 0 |
| }; |
| orderCode = order.code || generateOrderCode(order.customer || ''); |
| } else { |
| |
| exportData = getQuoteData(); |
| orderCode = generateOrderCode(exportData.customer?.name || ''); |
| } |
| |
| exportDeliveryExcel(exportData, orderCode).then(function(success) { |
| setBtnState(btn, false); |
| if (success && typeof showToast === 'function') { |
| showToast('✅ Excel giao hàng tải thành công!'); |
| } |
| }); |
| |
| return false; |
| } |
| } |
|
|
| |
| function init() { |
| |
| document.removeEventListener('click', handleExportClick, true); |
| |
| document.addEventListener('click', handleExportClick, true); |
| console.log('[VAI Excel Fix v2.0] Loaded - Multi-download enabled'); |
| } |
|
|
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', init); |
| } else { |
| init(); |
| } |
|
|
| |
| setTimeout(init, 2000); |
| setTimeout(init, 5000); |
|
|
| |
| window.VAI_EXCEL_FIX = { |
| init: init, |
| exportQuoteExcel: exportQuoteExcel, |
| exportDeliveryExcel: exportDeliveryExcel, |
| generateOrderCode: generateOrderCode, |
| getQuoteData: getQuoteData |
| }; |
|
|
| })(); |