bep40 commited on
Commit
b6767ce
·
1 Parent(s): 9528391

Excel bulk-import: drop 'Ngày ... tháng ... năm ...' + 'Người lập báo giá' rows; add paste-from-clipboard / image-URL image picker

Browse files

- bulk-import.ts: LABEL_START_RE now also rejects order/prepared-by rows —
'người lập', 'lập báo giá', 'báo giá', 'người duyệt', 'người kiểm',
'người giao', 'người xem' — so 'Ngày 18 tháng 8 năm 2026' /
'Người lập báo giá: ...' footer rows never become products.
- src/vaix-bulk-import.js: replacement per-row image picker with 3 inputs —
📁 upload, 📋 paste-image-from-clipboard (Ctrl+V, image blob → crop
directly; pasted text-URL → fetch), 🔗 paste external image URL →
load (CORS-safe via canvas + /api/img proxy fallback) → crop modal. Help
text updated. index.html cache-bust v9.

Files changed (3) hide show
  1. bulk-import.ts +1 -1
  2. index.html +1 -1
  3. src/vaix-bulk-import.js +118 -21
bulk-import.ts CHANGED
@@ -399,7 +399,7 @@ function _cellIsNoise(v: any): boolean {
399
  // 3. The row is not a total/sum row anywhere.
400
  // This completely removes non-product rows (section titles, notes, taxes, page
401
  // footers, totals) while keeping every real product row.
402
- const LABEL_START_RE = /^(?:tong cong|tong tien|tong so|tong|total|sub ?total|stt|so thu tu|cong tien|cong thanh|cong don|thanh tien|thanh toan|don vi|dvt|doi vi|don gia|ghi chu|luu y|ky hieu|loai|nhom|phan nhom|danh muc|muc luc|bang gia|toan bo|san pham|huong dan|ngay|thang|trang|page|hotline|lien he|dia chi|cong ty|thue|vat|phi|chiet khau|giam gia|khuyen mai|bao hanh|van chuyen|xem them|tai ve|mua ngay|ma don|so don|don hang|khach hang|khach|dien thoai|sdt|sdt khach|email|nguoi nhan|nguoi mua|dia chi giao|phuong thuc|hinh thuc|ngay dat|ngay giao|ky gui|len don|don vi ban)\b/;
403
  // Does the normalized name start with a known label word? (diacritics already
404
  // stripped by _normHeader → "Lưu ý" = "luu y", "Thuế VAT" = "thue vat".)
405
  function _isLabelLine(n: string): boolean {
 
399
  // 3. The row is not a total/sum row anywhere.
400
  // This completely removes non-product rows (section titles, notes, taxes, page
401
  // footers, totals) while keeping every real product row.
402
+ const LABEL_START_RE = /^(?:tong cong|tong tien|tong so|tong|total|sub ?total|stt|so thu tu|cong tien|cong thanh|cong don|thanh tien|thanh toan|don vi|dvt|doi vi|don gia|ghi chu|luu y|ky hieu|loai|nhom|phan nhom|danh muc|muc luc|bang gia|toan bo|san pham|huong dan|ngay|thang|trang|page|hotline|lien he|dia chi|cong ty|thue|vat|phi|chiet khau|giam gia|khuyen mai|bao hanh|van chuyen|xem them|tai ve|mua ngay|ma don|so don|don hang|khach hang|khach|dien thoai|sdt|sdt khach|email|nguoi nhan|nguoi mua|nguoi lap|nguoi duyet|nguoi kiem|nguoi giao|nguoi xem|lap bao gia|bao gia|dia chi giao|phuong thuc|hinh thuc|ngay dat|ngay giao|ky gui|len don|don vi ban)\b/;
403
  // Does the normalized name start with a known label word? (diacritics already
404
  // stripped by _normHeader → "Lưu ý" = "luu y", "Thuế VAT" = "thue vat".)
405
  function _isLabelLine(n: string): boolean {
index.html CHANGED
@@ -866,6 +866,6 @@
866
  <!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
867
  homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
868
  <script defer src="./src/vaix-add-product.js?v=16"></script>
869
- <script defer src="./src/vaix-bulk-import.js?v=8"></script>
870
  </body>
871
  </html>
 
866
  <!-- ── Thêm SP (Add Product) module: 100% port from V.AISTUDIO, gated by the
867
  homepage V.AISTUDIO access code (window.isVaAdmin()). ── -->
868
  <script defer src="./src/vaix-add-product.js?v=16"></script>
869
+ <script defer src="./src/vaix-bulk-import.js?v=9"></script>
870
  </body>
871
  </html>
src/vaix-bulk-import.js CHANGED
@@ -370,7 +370,7 @@
370
  tbody.innerHTML = rows;
371
  updateCommitBtn();
372
  var sum = $('bulkResultSummary');
373
- if (sum) sum.textContent = 'Số dòng: ' + _bulkProducts.length + ' — bấm vào ô để sửa. Bấm "✂ Ảnh" để tải/crop ảnh cho từng SP. Giá nhập số (VD: 5500000 hoặc 5.500.000).';
374
  }
375
  window.bulkRowSel = function (i, on) { if (_bulkProducts[i]) _bulkProducts[i]._sel = !!on; updateCommitBtn(); };
376
  window.bulkTglAll = function (on) {
@@ -396,30 +396,127 @@
396
  openBulkImagePicker(i);
397
  };
398
 
399
- // ── Bulk per-row image picker + crop (reuses the single-product crop modal) ──
400
- var _bulkImgIdx = -1;
401
- var _bulkCropIdx = -1, _bulkCropImg = null, _bulkCrop = null;
402
  function openBulkImagePicker(i) {
 
403
  _bulkImgIdx = i;
404
- var input = $('bulkRowImgInput');
405
- if (!input) {
406
- input = document.createElement('input');
407
- input.type = 'file';
408
- input.id = 'bulkRowImgInput';
409
- input.accept = 'image/*';
410
- input.style.display = 'none';
411
- document.body.appendChild(input);
412
- input.addEventListener('change', function () {
413
- var f = input.files && input.files[0];
414
- input.value = '';
415
- if (!f || _bulkImgIdx < 0) return;
416
- var r = new FileReader();
417
- r.onload = function (e) { openBulkCrop(e.target.result); };
418
- r.readAsDataURL(f);
419
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  }
421
- input.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  }
 
 
 
423
  function openBulkCrop(src) {
424
  var idx = _bulkImgIdx;
425
  if (idx < 0) return;
 
370
  tbody.innerHTML = rows;
371
  updateCommitBtn();
372
  var sum = $('bulkResultSummary');
373
+ if (sum) sum.textContent = 'Số dòng: ' + _bulkProducts.length + ' — bấm vào ô để sửa. Bấm "✂ Ảnh" để tải lên / dán ảnh (Ctrl+V) / dán link ảnh ngoài rồi cắt ảnh cho từng SP. Giá nhập số (VD: 5500000 hoặc 5.500.000).';
374
  }
375
  window.bulkRowSel = function (i, on) { if (_bulkProducts[i]) _bulkProducts[i]._sel = !!on; updateCommitBtn(); };
376
  window.bulkTglAll = function (on) {
 
396
  openBulkImagePicker(i);
397
  };
398
 
399
+ // ── Spreadsheet-image source picker: upload / paste-from-clipboard / URL ──
400
+ var _bulkImgIdx = -1; // row index whose image is being set
401
+ var _bulkImgPick = null; // DOM overlay
402
  function openBulkImagePicker(i) {
403
+ if (typeof _bulkProducts[i] === 'undefined') return;
404
  _bulkImgIdx = i;
405
+ if (_bulkImgPick) { _bulkImgPick.style.display = 'flex'; return; }
406
+ var ov = document.createElement('div');
407
+ ov.style.cssText = 'position:fixed;inset:0;z-index:2000;background:rgba(15,23,42,0.55);display:flex;align-items:center;justify-content:center';
408
+ ov.id = 'bulkImgPickerOverlay';
409
+ var card = document.createElement('div');
410
+ card.style.cssText = 'background:#fff;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.3);padding:20px 22px;width:min(440px,92vw);font-family:inherit';
411
+ var html = ''
412
+ + '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">'
413
+ + '<b style="font-size:.95rem;color:#0f172a">Ảnh sản phẩm</b>'
414
+ + '<button type="button" id="bulkImgPickClose" style="background:none;border:none;font-size:1.2rem;color:#64748b;cursor:pointer">✕</button></div>'
415
+ + '<div style="display:flex;gap:6px;margin-bottom:12px">'
416
+ + '<button type="button" id="bulkImgPickFile" style="flex:1;padding:8px;border:1px solid #b8d4e8;border-radius:8px;background:#f0f9ff;color:#1e40af;cursor:pointer;font:inherit">📁 Tải lên</button>'
417
+ + '<button type="button" id="bulkImgPickPaste" style="flex:1;padding:8px;border:1px solid #d8b4fe;border-radius:8px;background:#faf5ff;color:#7e22ce;cursor:pointer;font:inherit">📋 Dán ảnh</button>'
418
+ + '</div>'
419
+ + '<label style="display:block;font-size:.72rem;color:#475569;margin-bottom:4px">Hoặc dán link ảnh ngoài:</label>'
420
+ + '<div style="display:flex;gap:6px">'
421
+ + '<input id="bulkImgPickUrl" type="text" placeholder="https://…/anh.jpg (Ctrl+V để dán link)" style="flex:1;padding:7px 9px;border:1px solid #cbd5e1;border-radius:8px;font:inherit;font-size:.82rem">'
422
+ + '<button type="button" id="bulkImgPickGo" style="padding:7px 12px;border:1px solid #2563eb;border-radius:8px;background:#2563eb;color:#fff;cursor:pointer;font:inherit;font-size:.82rem">Tải</button>'
423
+ + '</div>'
424
+ + '<div id="bulkImgPickMsg" style="margin-top:8px;font-size:.75rem;color:#94a3b8;min-height:16px"></div>';
425
+ card.innerHTML = html;
426
+ ov.appendChild(card);
427
+ document.body.appendChild(ov);
428
+ _bulkImgPick = ov;
429
+ var msg = card.querySelector('#bulkImgPickMsg');
430
+ function setMsg(t, isErr) { msg.textContent = t; msg.style.color = isErr ? '#dc2626' : '#475569'; }
431
+ function closePick() { if (_bulkImgPick) _bulkImgPick.style.display = 'none'; }
432
+ card.querySelector('#bulkImgPickClose').addEventListener('click', closePick);
433
+ ov.addEventListener('click', function (e) { if (e.target === ov) closePick(); });
434
+ card.querySelector('#bulkImgPickFile').addEventListener('click', function () {
435
+ var input = $('bulkRowImgInput');
436
+ if (!input) {
437
+ input = document.createElement('input');
438
+ input.type = 'file'; input.id = 'bulkRowImgInput'; input.accept = 'image/*';
439
+ input.style.display = 'none';
440
+ document.body.appendChild(input);
441
+ input.addEventListener('change', function () {
442
+ var f = input.files && input.files[0];
443
+ input.value = '';
444
+ if (!f || _bulkImgIdx < 0) return;
445
+ var r = new FileReader();
446
+ r.onload = function (e) { openBulkCrop(e.target.result); };
447
+ r.readAsDataURL(f);
448
+ });
449
+ }
450
+ closePick();
451
+ input.click();
452
+ });
453
+ function loadFromUrl(url) {
454
+ url = String(url || '').trim();
455
+ if (!url) { setMsg('Nhập link ảnh để tải.', true); return; }
456
+ if (!/^https?:\/\//i.test(url)) { setMsg('Link phải bắt đầu bằng http:// hoặc https://', true); return; }
457
+ setMsg('⏳ Đang tải ảnh từ link…');
458
+ var im = new Image();
459
+ im.crossOrigin = 'anonymous';
460
+ im.onload = function () {
461
+ // draw to canvas to (a) get a clean data-URL for the crop modal and
462
+ // (b) bypass CORS/canvas-tainting so toDataURL below doesn't throw.
463
+ var cv = document.createElement('canvas');
464
+ cv.width = im.naturalWidth || im.width; cv.height = im.naturalHeight || im.height;
465
+ var ctx = cv.getContext('2d');
466
+ try { ctx.drawImage(im, 0, 0); } catch (e) { setMsg('Không tải được ảnh (CORS của trang nguồn).', true); return; }
467
+ try {
468
+ var du = cv.toDataURL('image/jpeg', 0.92);
469
+ closePick();
470
+ openBulkCrop(du);
471
+ setMsg('');
472
+ } catch (e2) {
473
+ // CORS blocked even after draw → proxy through the app's /api/img
474
+ // (server-side fetch w/ permissive CORS + cache) so hotlinked CDN
475
+ // images that block cross-origin browser loads still work.
476
+ fetch('/api/img?url=' + encodeURIComponent(url)).then(function (r2) {
477
+ if (!r2.ok) throw new Error('proxy ' + r2.status);
478
+ return r2.blob();
479
+ }).then(function (b) {
480
+ var fr = new FileReader();
481
+ fr.onload = function () { closePick(); openBulkCrop(fr.result); };
482
+ fr.readAsDataURL(b);
483
+ }).catch(function () { setMsg('Không tải được ảnh từ link này. Thử dán trực tiếp ảnh.', true); });
484
+ }
485
+ };
486
+ im.onerror = function () { setMsg('Không mở được ảnh từ link. Kiểm tra link hoặc mạng.', true); };
487
+ im.src = url;
488
  }
489
+ card.querySelector('#bulkImgPickGo').addEventListener('click', function () { loadFromUrl(card.querySelector('#bulkImgPickUrl').value); });
490
+ card.querySelector('#bulkImgPickUrl').addEventListener('keydown', function (e) { if (e.key === 'Enter') loadFromUrl(this.value); });
491
+ card.querySelector('#bulkImgPickPaste').addEventListener('click', function () {
492
+ setMsg('Bấm Ctrl+V (hoặc chuột phải → Dán) vào đây để dán ảnh từ clipboard.');
493
+ var inp = card.querySelector('#bulkImgPickUrl');
494
+ inp.focus(); inp.select();
495
+ });
496
+ // Paste an IMAGE (not text) → open crop directly.
497
+ card.addEventListener('paste', function (e) {
498
+ var items = (e.clipboardData && e.clipboardData.items) || [];
499
+ for (var k = 0; k < items.length; k++) {
500
+ if (items[k].type && items[k].type.indexOf('image') === 0) {
501
+ e.preventDefault();
502
+ var blob = items[k].getAsFile();
503
+ if (!blob) continue;
504
+ setMsg('⏳ Đang đọc ảnh đã dán…');
505
+ var fr = new FileReader();
506
+ fr.onload = function (ev) { closePick(); openBulkCrop(ev.target.result); };
507
+ fr.readAsDataURL(blob);
508
+ return;
509
+ }
510
+ }
511
+ // Otherwise treat pasted text as a URL.
512
+ var txt = (e.clipboardData && e.clipboardData.getData('text')) || '';
513
+ if (txt.trim() && /^https?:\/\//i.test(txt.trim())) { e.preventDefault(); loadFromUrl(txt); return; }
514
+ });
515
+ return _bulkImgPick;
516
  }
517
+
518
+ // Shared crop-modal state (declared here; _bulkImgIdx is set in picker)
519
+ var _bulkCropIdx = -1, _bulkCropImg = null, _bulkCrop = null;
520
  function openBulkCrop(src) {
521
  var idx = _bulkImgIdx;
522
  if (idx < 0) return;