vai-market / src /order-import.js
bep40's picture
order-import: wire Zalo picker button + commit CK apply (fix handlers)
0f9e8ca verified
Raw
History Blame
37.3 kB
/* V.AI Avatar2 - Import order from PDF/Excel/CSV (images!) + Merge orders
* v2 (order-import images + robust parsing):
* - Excel "Hinh anh" column now yields REAL images three ways:
* a) URL/hyperlink cells -> URL read via SheetJS (cellHyperlinks multi),
* b) EMBEDDED pictures (image anchored to the cell) -> extracted with ExcelJS
* (getImages + wb.getImage) when ExcelJS is present (index.html loads it),
* c) fallback zip/XML parser (jszip) handling BOTH xdr:-prefixed AND
* default-namespace <wsDr> drawing files (the ones ExcelJS 4.4.0 crashes on).
* Images are stored as small data: URLs (downscaled via canvas when larger
* than 160px) so they survive page reloads and sync payloads.
* - toNum() handles "1.550.000d", "15,5tr", "15.5tr", "1,5 trieu", "1 500 000".
* - CSV/TXT/OCR rows and PDF rows keep the same shape as before (arrays), so
* preview()/commit() work identically.
* - readFile always returns a parsed OBJECT {rows, links, imgs}; buildItems()
* still accepts a plain array for backward compatibility.
*/
(function () {
'use strict';
if (window.__VAI_ORDER_IMPORT__) return;
window.__VAI_ORDER_IMPORT__ = true;
var K = 'malloca_orders';
function lsr(k, d) { try { var v = JSON.parse(localStorage.getItem(k)); return v == null ? d : v; } catch (e) { return d; } }
function lsw(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }
/* Bao gia / quote export columns:
STT | Hinh anh | Ten | Ma SP | Thong tin | SL | Don gia | Don gia CK | Thanh tien | Ghi chu */
var MAP = [
['image', ['hinh anh', 'hinh', 'image', 'img', 'anh']],
['name', ['ten sp', 'ten', 'name', 'san pham']],
['model', ['ma sp', 'ma', 'model', 'sku', 'code', 'ma san pham']],
['info', ['thong tin', 'thongtin', 'thong so', 'mota', 'desc', 'specs']],
['qty', ['sl', 'so luong', 'qty', 'quantity', 'soluong']],
['price', ['don gia', 'gia', 'price', 'gia goc']],
['disc', ['don gia ck', 'gia ck', 'ck', 'disc', 'giam gia', 'gia da giam']],
['total', ['thanh tien', 'total', 'sum', 'tong']],
['note', ['ghi chu', 'note', 'remark']],
['stt', ['stt', 'so tt', 'no']]
];
function stripDia(s) { return String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim(); }
function classifyHeader(h) {
var n = stripDia(h); if (!n) return null;
var bestKey = null, bestLen = -1;
for (var i = 0; i < MAP.length; i++) {
for (var j = 0; j < MAP[i][1].length; j++) {
var kw = MAP[i][1][j];
// most specific keyword wins: "don gia ck" beats "don gia" (disc vs price)
if (n.indexOf(kw) !== -1 && kw.length > bestLen) { bestKey = MAP[i][0]; bestLen = kw.length; }
}
}
return bestKey;
}
function cellVal(c) { if (c == null) return ''; if (typeof c === 'number') return String(c); if (typeof c === 'object' && c.text) return String(c.text); return String(c).trim(); }
/* Robust VND parsing: "1.550.000d" -> 1550000, "15,5tr" -> 15500000,
"15.5tr" -> 15500000, "1,5 trieu" -> 1500000, "1 500 000" -> 1500000. */
function toNum(s) {
if (s == null) return 0;
if (typeof s === 'number') return Math.round(s);
var t = String(s).replace(/\s+/g, '').trim().toLowerCase();
if (!t) return 0;
var mult = 1;
if (/(tr|trieu|\bm\b)/.test(t)) mult = 1000000;
else if (/(k\b|nghin|ngan)/.test(t)) mult = 1000;
var m = t.replace(/[đ₫]/g, '').match(/(\d+(?:[.,]\d+)*)/);
if (!m) return 0;
var digits = m[1];
var parts = digits.split(/[.,]/);
var isGrouped = parts.length >= 2 && parts.slice(1).every(function (g) { return g.length === 3; });
var num = isGrouped ? parseInt(parts.join(''), 10) : parseFloat(digits.replace(/,/g, '.'));
if (isNaN(num)) return 0;
return Math.round(num * mult);
}
var libCache = {};
var _importFile = null;
/* Load a CDN script. Skips when a known global already exists (so CDN hiccups
can never hang an import). resolve() is also fire-and-forget tolerant: the
caller's .then continues once the script loads. */
function globalPresentFor(src) {
if (/exceljs/.test(src)) return typeof ExcelJS !== 'undefined';
if (/jszip/.test(src)) return typeof JSZip !== 'undefined';
if (/xlsx/.test(src)) return typeof XLSX !== 'undefined';
if (/pdf\.min\.js/.test(src)) return !!(window.pdfjsLib && window.pdfjsLib.getDocument);
if (/tesseract/.test(src)) return !!(window.Tesseract && window.Tesseract.recognize);
return false;
}
function loadScript(src) {
if (libCache[src]) return Promise.resolve();
if (globalPresentFor(src)) { libCache[src] = true; return Promise.resolve(); }
return new Promise(function (resolve, reject) {
var s = document.createElement('script');
s.src = src; s.async = true;
var done = false;
s.onload = function () { libCache[src] = true; done = true; resolve(); };
s.onerror = function () { if (!done) { done = true; reject(new Error('fail load ' + src)); } };
document.head.appendChild(s);
});
}
function parseDelimited(text) { var lines = String(text).split(/\r?\n/); var rows = []; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line) continue; rows.push(line.split(/[,;\t]+/).map(function (c) { return c.replace(/^"|"$/g, ''); })); } return rows; }
/* ArrayBuffer -> data: URL (persists in localStorage & sync payloads) */
function bufferToDataUrl(buf, mime) {
try {
var bytes = new Uint8Array(buf); var b64 = ''; var chunk = 0x8000;
for (var j = 0; j < bytes.length; j += chunk) b64 += String.fromCharCode.apply(null, bytes.subarray(j, j + chunk));
return 'data:' + (mime || 'image/png') + ';base64,' + btoa(b64);
} catch (e) { return ''; }
}
/* Downscale data: URL via canvas to keep orders/sync small (best effort) */
function downscaleDataUrl(dataUrl, maxW, maxH) {
return new Promise(function (resolve) {
if (!dataUrl || String(dataUrl).indexOf('data:') !== 0) return resolve(dataUrl);
try {
var img = new Image();
img.onload = function () {
try {
var w = img.naturalWidth || 1, h = img.naturalHeight || 1;
var k = Math.min(1, (maxW || 160) / w, (maxH || 160) / h);
if (k >= 1) return resolve(dataUrl);
var cv = document.createElement('canvas'); cv.width = Math.max(1, Math.round(w * k)); cv.height = Math.max(1, Math.round(h * k));
var c = cv.getContext('2d'); c.drawImage(img, 0, 0, cv.width, cv.height);
try { resolve(cv.toDataURL('image/jpeg', 0.82)); } catch (e2) { resolve(dataUrl); }
} catch (e) { resolve(dataUrl); }
};
img.onerror = function () { resolve(dataUrl); };
img.src = dataUrl;
} catch (e) { resolve(dataUrl); }
});
}
function extractImages(arrBufs) {
return Promise.all(arrBufs.map(function (x) {
var url = bufferToDataUrl(x.arrayBuffer, x.mime);
if (!url) return Promise.resolve('');
return downscaleDataUrl(url);
}));
}
/* --- ExcelJS extraction (xdr:-prefixed drawings; ExcelJS 4.4.0 crashes on
default-namespace <wsDr>, so keep jszip fallback below) --- */
function exceljsImages(buf) {
return loadScript('https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js').then(function () {
if (typeof ExcelJS === 'undefined' || !ExcelJS.Workbook) return [];
var wb = new ExcelJS.Workbook();
return wb.xlsx.load(buf).then(function (wb2) {
var ws = wb2.worksheets && wb2.worksheets[0];
if (!ws || typeof ws.getImages !== 'function') return [];
var mediaList = [];
try { mediaList = ws.getImages(); } catch (e) { return []; }
var out = [];
mediaList.forEach(function (im) {
try {
var tl = im.range && im.range.tl;
if (!tl || tl.nativeRow == null || tl.nativeCol == null) return;
var med = wb2.getImage(im.imageId);
if (!med || !med.buffer) return;
var ext = String(med.extension || 'png').toLowerCase();
var mime = ext === 'jpeg' || ext === 'jpg' ? 'image/jpeg' : (ext === 'gif' ? 'image/gif' : 'image/png');
out.push({ rowIdx: tl.nativeRow, colIdx: tl.nativeCol, arrayBuffer: med.buffer, mime: mime });
} catch (e) { }
});
return extractImages(out).then(function (urls) {
return out.map(function (o, i) { return { rowIdx: o.rowIdx, colIdx: o.colIdx, dataUrl: urls[i] }; });
});
});
}).catch(function () { return []; });
}
/* --- Manual jszip/XML fallback (works on default-NS <wsDr> files) --- */
function normRelPath(relDir, target) {
var t = String(target || '').replace(/\\/g, '/');
if (t.charAt(0) === '/') return t.slice(1);
var parts = String(relDir || '').split('/').filter(Boolean);
var segs = t.split('/');
for (var i = 0; i < segs.length; i++) {
var sg = segs[i];
if (sg === '..') parts.pop();
else if (sg && sg !== '.') parts.push(sg);
}
return parts.join('/');
}
function manualImages(buf) {
return loadScript('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js').then(function () {
if (typeof JSZip === 'undefined') return [];
return JSZip.loadAsync(buf).then(function (zip) {
var sheetNames = Object.keys(zip.files).filter(function (n) { return /^xl\/worksheets\/sheet\d+\.xml$/.test(n); });
if (!sheetNames.length) return [];
var first = sheetNames[0];
var relsFile = zip.files['xl/worksheets/_rels/' + first.replace(/^.*\//, '') + '.rels'];
return Promise.resolve(relsFile ? relsFile.async('string') : '').then(function (relsXml) {
// sheet<->drawing rel (accept Id=any order)
var drawRel = null; var drawPathRaw = null;
var segRe = /<Relationship\b[^>]*>/g; var sm;
while ((sm = segRe.exec(relsXml || '')) !== null) {
var seg = sm[0];
var idm = /Id="([^"]+)"/.exec(seg); var tm = /Target="([^"]+)"/.exec(seg);
if (idm && tm && /drawing/.test(tm[1])) { drawRel = idm[1]; drawPathRaw = tm[1]; break; }
}
if (!drawRel || !drawPathRaw) return [];
var drawPath = normRelPath('xl/worksheets', drawPathRaw);
var drawingZip = zip.file(drawPath);
if (!drawingZip) return [];
var mediaRelsPath = drawPath.replace(/\/[^/]+$/, '') + '/_rels/' + drawPath.slice(drawPath.lastIndexOf('/') + 1) + '.rels';
var mediaRelFile = zip.file(mediaRelsPath);
return Promise.all([
drawingZip.async('string').catch(function () { return ''; }),
mediaRelFile ? mediaRelFile.async('string').catch(function () { return ''; }) : Promise.resolve('')
]).then(function (res) {
var drawXml = res[0], mediaRelsXml = res[1];
var id2path = {};
var relDir = drawPath.replace(/\/[^/]+$/, '');
var rr = /<Relationship\b[^>]*>/g; var m2;
while ((m2 = rr.exec(mediaRelsXml || '')) !== null) {
var seg2 = m2[0];
var idm2 = /Id="([^"]+)"/.exec(seg2); var tm2 = /Target="([^"]+)"/.exec(seg2);
if (idm2 && tm2) id2path[idm2[1]] = normRelPath(relDir, tm2[1]);
}
if (!Object.keys(id2path).length) return [];
// anchors: works for BOTH xdr: and default namespace
var out = [];
var ano = /<(?:xdr:)?(oneCellAnchor|twoCellAnchor)[\s\S]*?<\/(?:xdr:)?\1>/g; var am;
while ((am = ano.exec(drawXml)) !== null) {
var block = am[0];
var fromM = /<(?:xdr:)?from>[\s\S]*?<(?:xdr:)?col>(\d+)<\/(?:xdr:)?col>[\s\S]*?<(?:xdr:)?row>(\d+)<\/(?:xdr:)?row>/.exec(block);
if (!fromM) continue;
var col = parseInt(fromM[1], 10), row = parseInt(fromM[2], 10);
var emb = /r:embed="([^"]+)"/.exec(block);
var path2 = emb && id2path[emb[1]];
if (!path2) continue;
out.push({ rowIdx: row, colIdx: col, path: path2 });
}
return Promise.all(out.map(function (o) {
var ff = zip.file(o.path);
if (!ff) return Promise.resolve(null);
return ff.async('arraybuffer').then(function (ab) {
var b = String(o.path).toLowerCase();
var mime = /\.jpe?g$/.test(b) ? 'image/jpeg' : (/\.gif$/.test(b) ? 'image/gif' : 'image/png');
return { rowIdx: o.rowIdx, colIdx: o.colIdx, arrayBuffer: ab, mime: mime };
});
})).then(function (rows) {
return extractImages(rows.filter(Boolean)).then(function (urls) {
var res2 = [];
rows.filter(Boolean).forEach(function (o, i) { res2.push({ rowIdx: o.rowIdx, colIdx: o.colIdx, dataUrl: urls[i] }); });
return res2;
});
});
});
});
});
}).catch(function () { return []; });
}
/* --- XLSX parse: text grid + hyperlinks + embedded images --- */
function parseXlsx(buf) {
var gridP = loadScript('https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js').then(function () {
var wb = XLSX.read(buf, { type: 'array', cellHyperlinks: true });
var sheet = wb.Sheets[wb.SheetNames[0]];
var rows = XLSX.utils.sheet_to_json(sheet, { header: 1, raw: false });
// hyperlink targets (image col is often a URL hyperlink). sheet_to_json
// returns FORMATTED STRINGS, so read .l.Target directly from the raw cells.
var links = {};
try {
var ref = sheet['!ref'] || '';
var refRange = XLSX.utils.decode_range(ref);
for (var ri = refRange.s.r; ri <= refRange.e.r; ri++) {
for (var ci = refRange.s.c; ci <= refRange.e.c; ci++) {
var cell = sheet[XLSX.utils.encode_cell({ r: ri, c: ci })];
if (cell && cell.l && (cell.l.Target || cell.l.target)) {
links[ri + '|' + ci] = cell.l.Target || cell.l.target || '';
}
}
}
} catch (e) { }
return { rows: rows, links: links };
});
var imgP = exceljsImages(buf).then(function (imgs) { return imgs.length ? imgs : manualImages(buf); });
return Promise.all([gridP, imgP]).then(function (res) {
return { rows: res[0].rows, links: res[0].links, imgs: res[1] };
});
}
/* --- PDF parse: text rows (kept as arrays for buildItems compatibility) --- */
function parsePdf(buf) {
return loadScript('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js').then(function () {
if (window.pdfjsLib) window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
var data = new Uint8Array(buf);
return window.pdfjsLib.getDocument({ data: data }).promise.then(function (doc) {
var tasks = [];
for (var p = 1; p <= doc.numPages; p++) tasks.push(doc.getPage(p).then(function (pg) { return pg.getTextContent().then(function (tc) { return { page: pg.pageNumber, items: tc.items }; }); }));
return Promise.all(tasks).then(function (pages) {
var rows = [];
for (var i = 0; i < pages.length; i++) {
var lines = {};
pages[i].items.forEach(function (it) { var y = Math.round(it.transform[5]); lines[y] = (lines[y] || '') + ' ' + (it.str || ''); });
Object.keys(lines).sort(function (a, b) { return b - a; }).forEach(function (y) { var t = lines[y].trim(); if (t) rows.push(t.split(/\s{2,}/)); });
}
return rows;
});
});
});
}
/* --- catalog enrichment (image/brand/specs/price by Ma SP) --- */
function catalogProducts() { try { if (!window.vaix || !window.vaix.allProducts) return []; return window.vaix.allProducts() || []; } catch (e) { return []; } }
function catKey(p) { return stripDia(String((p && p.sku) || '') + ' ' + String((p && p.model) || '') + ' ' + String((p && p.slug) || '')); }
function findCatalogProduct(model, name) {
var mk = stripDia(model); var nk = stripDia(name); if (!mk && !nk) return null;
var all = catalogProducts(); if (!all || !all.length) return null;
for (var s = 0; s < all.length; s++) { var ps = all[s]; var sk = stripDia(String((ps && (ps.sku || ps.model)) || '')); if (mk && sk && sk === mk) return ps; }
for (var i = 0; i < all.length; i++) { var p = all[i]; var k = catKey(p); if (mk && k.indexOf(mk) !== -1) return p; }
for (var j = 0; j < all.length; j++) { var p2 = all[j]; var t2 = stripDia(String((p2 && p2.title_clean) || (p2 && p2.name) || '')); if (mk && t2.indexOf(mk) !== -1) return p2; }
for (var x = 0; x < all.length; x++) { var p3 = all[x]; var t3 = catKey(p3); if (nk && t3.indexOf(nk) !== -1) return p3; }
return null;
}
function attachCatalog(it) {
if (!it) return it;
var cp = findCatalogProduct(it.model, it.name);
if (cp) {
if (!it.image && cp.image) it.image = cp.image;
if (!it.brand && cp.brand) it.brand = cp.brand;
if (!it.specs) { try { if (cp.specs && typeof cp.specs === 'object' && Object.keys(cp.specs).length) it.specs = Object.keys(cp.specs).slice(0, 3).map(function (k) { return k + ': ' + cp.specs[k]; }).join(', '); else if (cp.summary) it.specs = String(cp.summary).slice(0, 120); } catch (e) {} it.info = it.specs; }
if (!it.price && cp.priceNum) { it.price = cp.priceNum; if (!it.discPrice) it.discPrice = cp.priceNum; }
}
return it;
}
function buildItems(rows) {
if (!rows) return [];
if (rows.rows && Array.isArray(rows.rows)) rows = rows.rows;
var headerIdx = -1, cols = [];
for (var i = 0; i < rows.length; i++) { var mapped = {}; var used = 0; for (var j = 0; j < rows[i].length; j++) { var key = classifyHeader(rows[i][j]); if (key) { mapped[key] = j; used++; } } if (used >= 3) { headerIdx = i; cols = mapped; break; } }
var out = [], seen = {};
if (headerIdx >= 0) {
for (var r = headerIdx + 1; r < rows.length; r++) {
var row = rows[r];
var name = cols.name != null ? cellVal(row[cols.name]) : '';
var model = cols.model != null ? cellVal(row[cols.model]) : '';
if (!name && !model) continue;
var qty = cols.qty != null ? (toNum(cellVal(row[cols.qty])) || 1) : 1;
var price = cols.price != null ? toNum(cellVal(row[cols.price])) : 0;
var disc = cols.disc != null ? (toNum(cellVal(row[cols.disc])) || price) : price;
var total = cols.total != null ? toNum(cellVal(row[cols.total])) : (disc * qty);
var key = stripDia(name) + '|' + stripDia(model); if (seen[key]) continue; seen[key] = 1;
var rawImg = cols.image != null ? cellVal(row[cols.image]) : '';
out.push(attachCatalog({ name: name, model: model, sku: model, brand: '', image: rawImg, specs: cols.info != null ? cellVal(row[cols.info]) : '', info: cols.info != null ? cellVal(row[cols.info]) : '', qty: qty, price: price, discPrice: disc, total: total, note: cols.note != null ? cellVal(row[cols.note]) : '' }));
}
} else {
for (var x = 0; x < rows.length; x++) {
var rr = rows[x]; var nm = cellVal(rr[0]), md = cellVal(rr[1]); if (!nm && !md) continue;
out.push(attachCatalog({ name: nm, model: md, sku: md, image: '', specs: '', info: '', qty: toNum(rr[2]) || 1, price: 0, discPrice: 0, total: 0, note: '' }));
}
}
return out;
}
function isAdmin() { try { if (window.isAdmin) { var a = window.isAdmin(); if (a) return true; } } catch (e) {} try { return localStorage.getItem('vas_quote_unlocked') === '1'; } catch (e) { return false; } }
function status(m) { var el = document.getElementById('vit'); if (el) el.textContent = m; }
/* --- merge parsed {rows, links, imgs} -> items, injecting embedded images --- */
function findHeaderIdx(rows) {
for (var i = 0; i < rows.length; i++) { var used = 0; for (var j = 0; j < rows[i].length; j++) if (classifyHeader(rows[i][j])) used++; if (used >= 3) return i; }
return -1;
}
function buildItemsFromParsed(parsed) {
if (!parsed) return [];
var rows = Array.isArray(parsed) ? parsed : (parsed.rows || []);
var items = buildItems(rows);
var headerIdx = findHeaderIdx(rows);
if (headerIdx < 0) return items;
// map grid row -> item index (mirror buildItems dedup)
var rowToItem = {}; var seen = {}; var itemIdx = 0;
for (var r = headerIdx + 1; r < rows.length; r++) {
var nm = '', md = '';
for (var hc = 0; hc < rows[headerIdx].length; hc++) { var kk = classifyHeader(rows[headerIdx][hc]); if (kk === 'name') nm = cellVal(rows[r][hc]); if (kk === 'model') md = cellVal(rows[r][hc]); }
if (!nm && !md) continue;
var key = stripDia(nm) + '|' + stripDia(md); if (seen[key]) continue; seen[key] = 1;
rowToItem[r] = itemIdx; itemIdx++;
}
// 1) embedded images (nativeRow/nativeCol are 0-based grid indexes)
// These OVERRIDE catalog-filled images: the Excel file's own image column
// (pictures anchored to the cell) is what the user wants in the order.
var imgs = parsed.imgs || [];
imgs.forEach(function (im) {
var gi = rowToItem[im.rowIdx];
if (gi == null || gi >= items.length) return;
if (im.dataUrl) items[gi].image = im.dataUrl;
});
// 2) hyperlink cells -> URL for the image column (overrides too)
var links = parsed.links || {};
var imgCol = -1;
for (var hc2 = 0; hc2 < rows[headerIdx].length; hc2++) if (classifyHeader(rows[headerIdx][hc2]) === 'image') { imgCol = hc2; break; }
if (Object.keys(links).length && imgCol >= 0) {
for (var r2 = headerIdx + 1; r2 < rows.length; r2++) {
var lk = links[r2 + '|' + imgCol];
if (!lk) continue;
var gi2 = rowToItem[r2];
if (gi2 != null) items[gi2].image = lk;
}
}
return items;
}
/* 'Tai file tu Zalo' — list + download import files from the backend */
function fetchZaloFiles() {
var listEl = document.getElementById('vizalo-list');
var stEl = document.getElementById('vizalo-status');
if (stEl) stEl.textContent = '⏳ Đang tải danh sách file...';
fetch('/api/zalo-imports').then(function (r) { return r.json(); }).then(function (data) {
var files = (data && Array.isArray(data.files)) ? data.files : [];
if (!files.length) { if (listEl) listEl.innerHTML = '<div style="color:#94a3b8;font-size:12px;padding:4px 0;">Chưa có file nào từ Zalo.</div>'; if (stEl) stEl.textContent = (data && data.error) ? ('⚠️ ' + data.error) : ''; return; }
if (listEl) {
listEl.innerHTML = '<div style="font-size:12px;color:#003f62;font-weight:700;margin-bottom:4px;">📥 File từ Zalo (' + files.length + '):</div>' + files.map(function (f, i) {
var nm = String(f.name || ('file-' + (i + 1)));
var sz = f.size ? ' <span style="color:#94a3b8;font-size:11px">(' + (f.size > 1048576 ? (f.size / 1048576).toFixed(1) + 'MB' : Math.max(1, Math.round(f.size / 1024)) + 'KB') + ')</span>' : '';
return '<button class="vizalo-file" data-name="' + nm.replace(/"/g, '&quot;') + '" style="display:block;width:100%;text-align:left;margin:3px 0;padding:7px 10px;border:1px solid #dbeafe;border-radius:6px;background:#f0f7ff;color:#1d4ed8;cursor:pointer;font-size:12.5px;">📄 ' + nm + sz + '</button>';
}).join('');
listEl.querySelectorAll('.vizalo-file').forEach(function (btn) { btn.onclick = function () { loadZaloFile(btn.getAttribute('data-name')); }; });
}
if (stEl) stEl.textContent = '';
}).catch(function (e) { if (listEl) listEl.innerHTML = '<div style="color:#b91c1c;font-size:12px;">⚠️ Không tải được danh sách file.</div>'; if (stEl) stEl.textContent = (e && e.message) || ''; });
}
function loadZaloFile(name) {
var stEl = document.getElementById('vizalo-status');
if (stEl) stEl.textContent = '⏳ Đang tải ' + name + '...';
status('Đang tải ' + name + ' từ Zalo...');
fetch('/api/zalo-import-file?name=' + encodeURIComponent(name)).then(function (res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.arrayBuffer(); }).then(function (buf) {
var f = null; try { f = new File([buf], String(name || 'zalo-import.xlsx')); } catch (e) { f = { name: String(name || 'zalo-import.xlsx'), size: buf.byteLength }; }
_importFile = f;
if (stEl) stEl.textContent = '';
status('Reading ' + f.name + '...');
readFile(f, function (parsed) { var items = buildItemsFromParsed(parsed); if (!items.length) { status('No valid rows found - check column headers.'); return; } status('Read ' + items.length + ' products from Zalo file ' + name + '.'); preview(parsed); });
}).catch(function (e) { if (stEl) stEl.textContent = '⚠️ ' + ((e && e.message) || e); status('Lỗi tải file từ Zalo: ' + ((e && e.message) || e)); });
}
function applyImportCk(items) {
if (!items || !items.length) return 0;
var sel = null; try { sel = JSON.parse(localStorage.getItem('vas_selected_customer') || 'null'); } catch (e) { sel = null; }
if (!sel || !sel.ck || String(sel.ck).trim() === '') return 0;
var parsed = (window.parseCk || function (s) {
var map = {}; var all = null;
String(s || '').trim().split(/[,;\n]+/).forEach(function (pt) {
pt = pt.trim(); if (!pt) return;
var gm = pt.match(/^(\d+(?:[.,]\d+)?)\s*%?$/);
if (gm) { var g = parseFloat(gm[1].replace(',', '.')); if (g > 0 && g <= 90) all = g; return; }
var bm = pt.match(/^([A-Za-z\u00c0-\u1ef9\s-]+?)\s*(\d+(?:[.,]\d+)?)\s*%?$/);
if (bm) { var bp = parseFloat(bm[2].replace(',', '.')); if (bp > 0 && bp <= 90) map[bm[1].trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[\u0111\u0110]/g, 'd')] = bp; }
});
if (all != null) map.__all = all;
return { map: map, all: all };
})(sel.ck);
var count = 0;
items.forEach(function (it) {
if (!it || !(Number(it.price) > 0)) return;
var pct = null;
if (window.ckPercentForProduct) { try { pct = window.ckPercentForProduct(it, parsed.map); } catch (e) { pct = null; } }
if (pct == null && parsed.all != null) {
try {
var noCk = it.noCk === true || it.promo === 'bigsale' || it.promo === 'combo' || String(it.name || '').indexOf('BIG SALE') !== -1 || String(it.name || '').indexOf('COMBO') !== -1;
if (!noCk && !((window.isEcoKitchen || function () { return false; })(it))) pct = parsed.all;
} catch (e) {}
}
if (pct == null || !(pct > 0 && pct <= 90)) return;
it.discPrice = Math.round(it.price * (1 - pct / 100));
it.total = it.discPrice * (Number(it.qty || 1));
count++;
});
return count;
}
function fireSave(orders) { try { if (window.saveOrders) window.saveOrders(orders); else if (window.saveOrder) window.saveOrder(); } catch (e) {} }
function commit(itemsIn, mode, targetCode) {
var items = itemsIn || [];
if (!items.length) { status('No valid rows found.'); return; }
var orders = lsr(K, []);
if (mode === 'merge') {
var tgt = null; for (var i = 0; i < orders.length; i++) if (String(orders[i].code) === String(targetCode)) { tgt = orders[i]; break; }
if (!tgt) { status('Order ' + targetCode + ' not found to merge.'); return; }
var items2 = (tgt.items || []).slice(); var byKey = {};
items2.forEach(function (it) { byKey[stripDia((it.model || '') + it.name)] = it; });
items.forEach(function (nw) { var key = stripDia((nw.model || '') + nw.name); if (byKey[key]) { byKey[key].qty = (byKey[key].qty || 0) + (nw.qty || 1); byKey[key].price = nw.price || byKey[key].price; byKey[key].discPrice = nw.discPrice || byKey[key].discPrice; byKey[key].total = byKey[key].qty * (byKey[key].discPrice || byKey[key].price || 0); } else { var it2 = JSON.parse(JSON.stringify(nw)); it2.total = (it2.discPrice || it2.price || 0) * (it2.qty || 1); items2.push(it2); byKey[key] = it2; } });
tgt.items = items2; var gt = 0; items2.forEach(function (it) { gt += Number(it.total || 0); }); tgt.grandTotal = gt; tgt.total = gt;
lsw(K, orders); status('Merged ' + items.length + ' rows into order ' + targetCode + ' (qty summed by Ma SP, totals recomputed).'); fireSave(orders);
} else {
var code = 'DH' + new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 12);
var t = 0; items.forEach(function (it) { t += Number((it.total || 0) || ((it.discPrice || it.price || 0) * it.qty)); });
var order = { code: code, customer: '', phone: '', email: '', addr: '', date: new Date().toLocaleString('vi-VN'), items: items, fees: [], grandTotal: t, total: t, status: 'pending', verified: false, savedAt: new Date().toISOString(), source: 'avatar2' };
orders.unshift(order); lsw(K, orders); status('Created new order ' + code + ' with ' + items.length + ' products.'); fireSave(orders);
}
}
function readFile(file, cb) {
if (/\.(xlsx|xls)$/i.test(file.name)) { var fr = new FileReader(); fr.onload = function () { parseXlsx(new Uint8Array(fr.result)).then(cb); }; fr.readAsArrayBuffer(file); }
else if (/\.pdf$/i.test(file.name)) { var fr2 = new FileReader(); fr2.onload = function () { parsePdf(new Uint8Array(fr2.result)).then(function (rows2) { cb({ rows: rows2, links: {}, imgs: [] }); }); }; fr2.readAsArrayBuffer(file); }
else if (/\.(png|jpe?g|webp|bmp)$/i.test(file.name)) { var fr4 = new FileReader(); fr4.onload = function () { parseImage(new Uint8Array(fr4.result)).then(function (rows4) { cb({ rows: rows4, links: {}, imgs: [] }); }); }; fr4.readAsArrayBuffer(file); }
else { var fr3 = new FileReader(); fr3.onload = function () { cb({ rows: parseDelimited(fr3.result), links: {}, imgs: [] }); }; fr3.readAsText(file); }
}
function parseImage(file) { return loadScript('https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js').then(function () { return window.Tesseract.recognize(file, 'vie+eng', { logger: function (m) { if (m.status) status('OCR ' + Math.round(m.progress * 100) + '%'); } }).then(function (r) { return parseDelimited(r.data.text); }); }); }
function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function preview(parsed) {
var items = buildItemsFromParsed(parsed);
applyImportCk(items);
var box = document.getElementById('vit');
var html = '<div style="margin-top:8px;"><b>Preview (' + items.length + ' rows):</b></div><table style="width:100%;border-collapse:collapse;font-size:12px;margin-top:6px;"><tr style="background:#f0f4ff;"><th style="border:1px solid #ddd;padding:3px;">Image</th><th style="border:1px solid #ddd;padding:3px;">Name</th><th style="border:1px solid #ddd;padding:3px;">Ma SP</th><th style="border:1px solid #ddd;padding:3px;">SL</th><th style="border:1px solid #ddd;padding:3px;">Unit CK</th><th style="border:1px solid #ddd;padding:3px;">Total</th></tr>';
for (var i = 0; i < items.length; i++) { var it = items[i]; html += '<tr><td style="border:1px solid #ddd;padding:3px;">' + (it.image ? '<img src="' + esc(it.image) + '" style="height:28px;" onerror="this.style.display=\'none\'">' : '📦') + '</td><td style="border:1px solid #ddd;padding:3px;">' + esc(it.name) + '</td><td style="border:1px solid #ddd;padding:3px;">' + esc(it.model) + '</td><td style="border:1px solid #ddd;padding:3px;">' + esc(it.qty) + '</td><td style="border:1px solid #ddd;padding:3px;">' + esc(it.discPrice) + '</td><td style="border:1px solid #ddd;padding:3px;">' + esc(it.total) + '</td></tr>'; }
html += '</table>'; box.innerHTML = html;
}
function openImportPanel() {
var existing = document.getElementById('vai-order-import-panel'); if (existing) { existing.style.display = 'block'; return; }
var wrap = document.createElement('div'); wrap.id = 'vai-order-import-panel';
wrap.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:center;justify-content:center;';
wrap.innerHTML = '<div style="background:#fff;color:#222;border-radius:12px;padding:20px;width:min(560px,92vw);max-height:86vh;overflow:auto;font-family:inherit;"><h3 style="margin:0 0 10px;">Import PDF/Excel/CSV - Order</h3><p style="font-size:13px;color:#666;margin:0 0 12px;">Columns match Bao gia: STT | Hinh | Ten | Ma SP | Info | SL | Price | Price CK | Total | Note. Ảnh cột Hình (đường dẫn / hyperlink / ảnh nhúng trong ô) được đưa thẳng vào đơn hàng; nếu trống sẽ tự lấy từ catalogue theo Ma SP.</p><input type="file" id="vif" accept=".xlsx,.xls,.csv,.txt,.pdf,.png,.jpg,.jpeg,.webp" style="margin-bottom:10px;width:100%;"><div style="margin:10px 0;border-top:1px dashed #cbd5e1;padding-top:8px;font-size:13px;"><button id="vizalo" style="padding:6px 12px;border:1px solid #1d4ed8;border-radius:6px;background:#eff6ff;color:#1d4ed8;cursor:pointer;font-weight:600;">📥 Tải file từ Zalo</button> <span id="vizalo-status" style="font-size:12px;color:#64748b;"></span></div><div id="vizalo-list" style="margin-bottom:10px;font-size:13px;max-height:220px;overflow-y:auto;"></div><div style="margin-bottom:10px;font-size:13px;"><label><input type="radio" name="vimode" value="new" checked> Create new order</label>&nbsp;&nbsp;<label><input type="radio" name="vimode" value="merge"> Merge into order</label></div><div id="vit" style="font-size:13px;color:#088;min-height:18px;margin-bottom:10px;"></div><div style="display:flex;gap:8px;justify-content:flex-end;"><button id="viclose" style="padding:6px 12px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;">Close</button><button id="vigo" style="padding:6px 14px;border:none;border-radius:6px;background:#0b6;color:#fff;cursor:pointer;">Preview / Save</button></div></div>';
document.body.appendChild(wrap);
wrap.addEventListener('click', function (e) { if (e.target === wrap) wrap.style.display = 'none'; });
document.getElementById('viclose').onclick = function () { wrap.style.display = 'none'; };
document.getElementById('vif').addEventListener('change', function (ev) { var f = ev.target.files && ev.target.files[0]; if (!f) return; _importFile = f; status('Reading ' + f.name + '...'); readFile(f, function (parsed) { var items = buildItemsFromParsed(parsed); if (!items.length) { status('No valid rows found - check column headers.'); return; } status('Read ' + items.length + ' products.'); preview(parsed); }); });
document.getElementById('vizalo').addEventListener('click', fetchZaloFiles);
document.getElementById('vigo').onclick = function () { var f = _importFile || (document.getElementById('vif').files && document.getElementById('vif').files[0]); if (!f) { status('Choose a file first.'); return; } status('Processing...'); readFile(f, function (parsed) { var items = buildItemsFromParsed(parsed); applyImportCk(items); var mode = document.querySelector('input[name="vimode"]:checked').value; if (!items.length) { status('No valid rows.'); return; } var target = ''; if (mode === 'merge') { var promptVal = prompt('Enter order code to merge into (e.g. DH...):'); if (!promptVal) { status('Merge cancelled.'); return; } target = promptVal; } commit(items, mode, target); }); };
}
function injectButton() {
var modal = document.getElementById('vai-order-modal') || document.getElementById('vaistudio-order-modal') || document.querySelector('.order-modal');
if (!modal) { setTimeout(injectButton, 800); return; }
if (!isAdmin()) { setTimeout(injectButton, 2000); return; }
if (modal.querySelector('.vai-import-btn')) return;
var btn = document.createElement('button'); btn.className = 'vai-import-btn'; btn.textContent = 'Import PDF/Excel';
btn.style.cssText = 'margin:8px;padding:8px 12px;border:1px solid #0b6;border-radius:8px;background:#0b6;color:#fff;cursor:pointer;font-weight:600;';
btn.onclick = openImportPanel; modal.appendChild(btn);
}
function boot() {
window.vaiOrderImport = { openImportPanel: openImportPanel, buildItems: buildItems, buildItemsFromParsed: buildItemsFromParsed, preview: preview, parseXlsx: parseXlsx, parsePdf: parsePdf, manualImages: manualImages, exceljsImages: exceljsImages, fetchZaloFiles: fetchZaloFiles, loadZaloFile: loadZaloFile, applyImportCk: applyImportCk, toNum: toNum };
setTimeout(injectButton, 1200);
setInterval(function () { var m = document.getElementById('vai-order-modal') || document.getElementById('vaistudio-order-modal') || document.querySelector('.order-modal'); if (m && !m.querySelector('.vai-import-btn') && isAdmin()) injectButton(); }, 3000);
}
window.vaiOrderImport = { openImportPanel: openImportPanel, buildItems: buildItems, buildItemsFromParsed: buildItemsFromParsed, preview: preview, parseXlsx: parseXlsx, parsePdf: parsePdf, manualImages: manualImages, exceljsImages: exceljsImages, fetchZaloFiles: fetchZaloFiles, loadZaloFile: loadZaloFile, applyImportCk: applyImportCk, toNum: toNum };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot();
})();