Spaces:
Running
Running
web: bulk CK edit on customer list (admin, after access code) + send-per-customer via Zalo
Browse files- Customer list rows get an admin-only bulk checkbox; a bulk bar lets the
admin type a CK% (global or per-brand) and apply it to all selected
customers at once (POST /api/customers, V.AISTUDIO-gated).
- Customer DETAIL modal gets a 'Gửi qua Zalo' button (admin, after unlock)
that saves the current CK + all filled fields and DMs that customer a
message containing every field that has content (ma KH, CK, company,
address, representative, recipient, tax code...).
- Bulk 'Gửi qua Zalo' sends the same to each selected customer.
- Access code is no longer auto-filled into quote modal input (hidden).
- index.html +2 -2
- src/cart-quote.js +3 -1
- src/customers.js +187 -2
index.html
CHANGED
|
@@ -551,8 +551,8 @@
|
|
| 551 |
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
| 552 |
<!-- App modules: defer keeps execution order (parse order) but doesn't block
|
| 553 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 554 |
-
<script defer src="./src/cart-quote.js?gc=
|
| 555 |
-
<script defer src="./src/customers.js?gc=
|
| 556 |
<script defer src="./src/vaix-rag.js?gc=48"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
|
| 557 |
<script defer src="./src/greeting-news.js?gc=27"></script> <!-- Greeting HOT news cards + source links -->
|
| 558 |
<script defer src="./src/order-sync.js?gc=15"></script> <!-- Order sync with V.AISTUDIO backend -->
|
|
|
|
| 551 |
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
| 552 |
<!-- App modules: defer keeps execution order (parse order) but doesn't block
|
| 553 |
first paint — the welcome/avatar-picker modal renders immediately. -->
|
| 554 |
+
<script defer src="./src/cart-quote.js?gc=24"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
|
| 555 |
+
<script defer src="./src/customers.js?gc=13"></script> <!-- Customer list (Danh sách khách hàng) -->
|
| 556 |
<script defer src="./src/vaix-rag.js?gc=48"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
|
| 557 |
<script defer src="./src/greeting-news.js?gc=27"></script> <!-- Greeting HOT news cards + source links -->
|
| 558 |
<script defer src="./src/order-sync.js?gc=15"></script> <!-- Order sync with V.AISTUDIO backend -->
|
src/cart-quote.js
CHANGED
|
@@ -233,7 +233,9 @@ window.openQuotation = function() {
|
|
| 233 |
if (addr && !addr.value) addr.value = saved.addr || '';
|
| 234 |
if (codeEl) codeEl.value = genOrderCode();
|
| 235 |
if (dateEl) dateEl.value = new Date().toISOString().slice(0, 10);
|
| 236 |
-
|
|
|
|
|
|
|
| 237 |
_pushTopbarDown();
|
| 238 |
ov.style.display = 'flex';
|
| 239 |
ov.classList.add('open');
|
|
|
|
| 233 |
if (addr && !addr.value) addr.value = saved.addr || '';
|
| 234 |
if (codeEl) codeEl.value = genOrderCode();
|
| 235 |
if (dateEl) dateEl.value = new Date().toISOString().slice(0, 10);
|
| 236 |
+
// NOTE: the V.AISTUDIO access code is NEVER auto-filled into the modal input —
|
| 237 |
+
// it stays hidden (masked, type=password) and the admin types it manually.
|
| 238 |
+
// This prevents the access code from being displayed/revealed on any modal.
|
| 239 |
_pushTopbarDown();
|
| 240 |
ov.style.display = 'flex';
|
| 241 |
ov.classList.add('open');
|
src/customers.js
CHANGED
|
@@ -352,7 +352,12 @@
|
|
| 352 |
var editBtn = allowEdit
|
| 353 |
? '<button class="cus-edit-btn" data-cid="' + esc(cid) + '" title="Sửa">✎</button>'
|
| 354 |
: '';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
return '<div class="cus-row" data-cid="' + esc(cid) + '">'
|
|
|
|
| 356 |
+ '<div class="cus-row-main">'
|
| 357 |
+ '<div class="cus-row-name">' + esc(name) + '</div>'
|
| 358 |
+ '<div class="cus-row-meta">' + esc(cid.slice(0, 8)) + ma + ck + '</div>'
|
|
@@ -363,6 +368,16 @@
|
|
| 363 |
+ '</div>'
|
| 364 |
+ '</div>';
|
| 365 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
// ── Detail editor modal (V.AISTUDIO access-code gated in normal mode) ──
|
| 368 |
function openCustomerDetail(cid, c) {
|
|
@@ -433,7 +448,7 @@
|
|
| 433 |
+ '<div style="border-top:1px solid #e2e8f0;padding-top:12px"></div>'
|
| 434 |
+ fieldsHtml
|
| 435 |
+ (allowEdit
|
| 436 |
-
? '<div style="text-align:right;margin-top:16px"><button id="cus-save" style="padding:10px 22px;background:#0077b6;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:700">Lưu</button></div>'
|
| 437 |
: '')
|
| 438 |
+ '</div>';
|
| 439 |
document.body.appendChild(ov);
|
|
@@ -528,7 +543,155 @@
|
|
| 528 |
}
|
| 529 |
};
|
| 530 |
} // end if (saveBtn)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
function renderCustomerList() {
|
| 534 |
var box = document.getElementById('cus-list');
|
|
@@ -569,10 +732,17 @@
|
|
| 569 |
+ '<button id="cus-close" style="background:none;border:none;color:#fff;font-size:22px;cursor:pointer">✕</button></div>'
|
| 570 |
+ '<div id="cus-gate"></div>'
|
| 571 |
+ '<div style="padding:10px 18px;border-bottom:1px solid #e2e8f0"><input id="cus-q" placeholder="Tìm khách hàng..." style="width:100%;padding:9px;border:2px solid #e2e8f0;border-radius:8px;font-size:13px;box-sizing:border-box;outline:none"></div>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
+ '<div id="cus-list" style="flex:1;overflow-y:auto;padding:10px 18px"></div>'
|
| 573 |
+ '</div>';
|
| 574 |
document.body.appendChild(ov);
|
| 575 |
-
|
|
|
|
| 576 |
|
| 577 |
// Access gate for NORMAL mode (no ?kh link): the full customer directory is
|
| 578 |
// revealed only after the V.AISTUDIO access code is entered. In
|
|
@@ -618,6 +788,21 @@
|
|
| 618 |
return;
|
| 619 |
}
|
| 620 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
|
| 622 |
// Search filter (non-admin sees only their own row, so the filter is no-op)
|
| 623 |
document.getElementById('cus-q').oninput = function () {
|
|
|
|
| 352 |
var editBtn = allowEdit
|
| 353 |
? '<button class="cus-edit-btn" data-cid="' + esc(cid) + '" title="Sửa">✎</button>'
|
| 354 |
: '';
|
| 355 |
+
// Admin-only checkbox for bulk CK editing (homepage, after access code).
|
| 356 |
+
var chk = allowEdit
|
| 357 |
+
? '<label class="cus-chk" title="Chọn chỉnh CK hàng loạt" style="display:flex;align-items:center;margin-right:10px;cursor:pointer"><input type="checkbox" class="cus-bulk-chk" data-cid="' + esc(cid) + '" style="width:16px;height:16px;cursor:pointer"></label>'
|
| 358 |
+
: '';
|
| 359 |
return '<div class="cus-row" data-cid="' + esc(cid) + '">'
|
| 360 |
+
+ (chk)
|
| 361 |
+ '<div class="cus-row-main">'
|
| 362 |
+ '<div class="cus-row-name">' + esc(name) + '</div>'
|
| 363 |
+ '<div class="cus-row-meta">' + esc(cid.slice(0, 8)) + ma + ck + '</div>'
|
|
|
|
| 368 |
+ '</div>'
|
| 369 |
+ '</div>';
|
| 370 |
}
|
| 371 |
+
// Collect the cids currently ticked in the (admin) bulk-CK checkboxes.
|
| 372 |
+
function selectedBulkCids() {
|
| 373 |
+
var out = [];
|
| 374 |
+
(window.__cusModal && window.__cusModal.querySelectorAll ? window.__cusModal.querySelectorAll('.cus-bulk-chk:checked') : []).forEach(function (chk) {
|
| 375 |
+
var cid = chk.getAttribute('data-cid');
|
| 376 |
+
if (cid) out.push(cid);
|
| 377 |
+
});
|
| 378 |
+
return out;
|
| 379 |
+
}
|
| 380 |
+
window.selectedBulkCids = selectedBulkCids;
|
| 381 |
|
| 382 |
// ── Detail editor modal (V.AISTUDIO access-code gated in normal mode) ──
|
| 383 |
function openCustomerDetail(cid, c) {
|
|
|
|
| 448 |
+ '<div style="border-top:1px solid #e2e8f0;padding-top:12px"></div>'
|
| 449 |
+ fieldsHtml
|
| 450 |
+ (allowEdit
|
| 451 |
+
? '<div style="text-align:right;margin-top:16px"><button id="cus-save" style="padding:10px 22px;background:#0077b6;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:700">Lưu</button> <button id="cus-send-zalo" style="padding:10px 16px;background:#25D366;color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:700"><i class="fab fa-whatsapp"></i> Gửi qua Zalo</button></div>'
|
| 452 |
: '')
|
| 453 |
+ '</div>';
|
| 454 |
document.body.appendChild(ov);
|
|
|
|
| 543 |
}
|
| 544 |
};
|
| 545 |
} // end if (saveBtn)
|
| 546 |
+
|
| 547 |
+
// ── "Gửi qua Zalo" on the customer DETAIL modal: saves the current CK + all
|
| 548 |
+
// filled fields to the dataset, then DMs this customer a message containing
|
| 549 |
+
// every field that has content (mã KH, CK, Zalo name, company, address,
|
| 550 |
+
// representative, recipient, tax code...). Only shown for admin after unlock.
|
| 551 |
+
var sendZaloBtn = document.getElementById('cus-send-zalo');
|
| 552 |
+
if (sendZaloBtn) {
|
| 553 |
+
sendZaloBtn.onclick = async function () {
|
| 554 |
+
var record = Object.assign({}, c);
|
| 555 |
+
record.cid = cid;
|
| 556 |
+
record.ma_kh = document.getElementById('cus-ma').value.trim();
|
| 557 |
+
record.ck = document.getElementById('cus-ck').value.trim();
|
| 558 |
+
MULTI_FIELDS.forEach(function (f) {
|
| 559 |
+
var vals = [];
|
| 560 |
+
ov.querySelectorAll('.cus-mv-row[data-field="' + f.key + '"] .cus-mv-input').forEach(function (i) {
|
| 561 |
+
var v = i.value.trim(); if (v) vals.push(v);
|
| 562 |
+
});
|
| 563 |
+
if (vals.length) record[f.key] = vals; else delete record[f.key];
|
| 564 |
+
});
|
| 565 |
+
// Resolve the real Zalo cid to send to.
|
| 566 |
+
var zcid = customerCidForSend(record);
|
| 567 |
+
if (!zcid) { toast('❌ Khách hàng này chưa có Zalo ID để gửi.'); return; }
|
| 568 |
+
try {
|
| 569 |
+
// Persist first (so the Zalo message reflects the just-saved CK).
|
| 570 |
+
var resp = await saveCustomers([record], ACCESS_CODE);
|
| 571 |
+
if (resp && resp.customers) window.__customers = resp.customers;
|
| 572 |
+
else { try { window.__customers = await fetchCustomers(); } catch (_e) {} }
|
| 573 |
+
var msg = buildCustomerSendMessage(record);
|
| 574 |
+
toast('📨 Đang gửi qua Zalo cho ' + (record.name || record.ma_kh || '') + '...');
|
| 575 |
+
var r = await fetch('https://bep40-vaistudio-zalo-bot.hf.space/send', {
|
| 576 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 577 |
+
body: JSON.stringify({ user_id: zcid, message: msg })
|
| 578 |
+
});
|
| 579 |
+
var d = await r.json().catch(function () { return {}; });
|
| 580 |
+
if (r.ok && d && d.ok) toast('✅ Đã lưu & gửi qua Zalo.');
|
| 581 |
+
else toast('✅ Đã lưu, nhưng gửi qua Zalo thất bại.');
|
| 582 |
+
renderCustomerList();
|
| 583 |
+
} catch (e) {
|
| 584 |
+
toast('❌ Lỗi: ' + e.message);
|
| 585 |
+
}
|
| 586 |
+
};
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
// ── Bulk CK editing on the customer list (homepage, after access code) ──
|
| 591 |
+
// The admin selects multiple customers, types a CK% (global "35" or per-brand
|
| 592 |
+
// "Malloca 35, Grob 20") and hits "Áp cho đã chọn". Saves every selected
|
| 593 |
+
// customer via POST /api/customers (V.AISTUDIO-gated, dataset-backed).
|
| 594 |
+
window.bulkApplyCk = async function () {
|
| 595 |
+
var cids = selectedBulkCids();
|
| 596 |
+
if (!cids.length) { toast('⚠️ Chưa chọn khách h��ng nào.'); return; }
|
| 597 |
+
if (!(window.canEditCk ? window.canEditCk() : false)) { toast('🔒 Cần nhập mã truy cập để chỉnh CK hàng loạt.'); return; }
|
| 598 |
+
var ckEl = document.getElementById('cus-bulk-ck');
|
| 599 |
+
var ckVal = (ckEl && ckEl.value || '').trim();
|
| 600 |
+
if (!ckVal) { toast('⚠️ Nhập giá trị chiết khấu (VD: 35 hoặc Malloca 35, Grob 20).'); ckEl && ckEl.focus(); return; }
|
| 601 |
+
// Basic validation so junk like "abc" doesn't reach the server.
|
| 602 |
+
var parsed = parseCk(ckVal);
|
| 603 |
+
if (parsed.all == null && !parsed.hasBrand) { toast('❌ CK không hợp lệ. VD: 35 hoặc Malloca 35, Grob 20'); return; }
|
| 604 |
+
var records = cids.map(function (cid) {
|
| 605 |
+
var c = (window.__customers || {})[cid] || {};
|
| 606 |
+
var rec = {};
|
| 607 |
+
Object.keys(c).forEach(function (k) { rec[k] = c[k]; });
|
| 608 |
+
rec.cid = cid;
|
| 609 |
+
rec.ck = ckVal;
|
| 610 |
+
return rec;
|
| 611 |
+
});
|
| 612 |
+
try {
|
| 613 |
+
toast('💾 Đang lưu CK cho ' + records.length + ' khách...');
|
| 614 |
+
var resp = await saveCustomers(records, ACCESS_CODE);
|
| 615 |
+
if (resp && resp.customers) window.__customers = resp.customers;
|
| 616 |
+
else { try { window.__customers = await fetchCustomers(); } catch (_e) {} }
|
| 617 |
+
toast('✅ Đã cập nhật chiết khấu cho ' + records.length + ' khách hàng.');
|
| 618 |
+
if (window.__cusModal) window.__cusModal.querySelectorAll('.cus-bulk-chk:checked').forEach(function (chk) { chk.checked = false; });
|
| 619 |
+
var n = document.getElementById('cus-bulk-count'); if (n) n.textContent = 0;
|
| 620 |
+
renderCustomerList();
|
| 621 |
+
} catch (e) {
|
| 622 |
+
toast('❌ Lưu CK thất bại: ' + e.message);
|
| 623 |
+
}
|
| 624 |
+
};
|
| 625 |
+
// ── Send the edited CK / customer info to each selected customer via the Zalo
|
| 626 |
+
// bot (/send). Content = the fields that have content on the customer's detail
|
| 627 |
+
// record (mã KH, Zalo name, company, address, representative, recipient, tax
|
| 628 |
+
// code, shipping address, CK...) — so each customer receives their own updated
|
| 629 |
+
// policy. The bot prepends the "📋 Mã KH + 🔗 Chiết khấu" header automatically.
|
| 630 |
+
function buildCustomerSendMessage(c) {
|
| 631 |
+
var lines = [];
|
| 632 |
+
var name = c.name || (Array.isArray(c.zalo_name) ? c.zalo_name[0] : c.zalo_name) || '';
|
| 633 |
+
var ma = c.ma_kh || '';
|
| 634 |
+
if (name) lines.push('👋 Chào ' + name + '!');
|
| 635 |
+
if (ma) lines.push('📋 Mã khách hàng: ' + ma);
|
| 636 |
+
if (c.ck) lines.push('💳 Chiết khấu cập nhật: ' + c.ck + '%');
|
| 637 |
+
lines.push('');
|
| 638 |
+
// Fields that HAVE content — same set the customer detail modal shows.
|
| 639 |
+
[
|
| 640 |
+
['Công ty', c.company], ['Người đại diện', c.representative], ['Người nhận', c.recipient],
|
| 641 |
+
['Địa chỉ giao hàng', c.shipping_address], ['Địa chỉ công ty', c.company_address],
|
| 642 |
+
['Mã số thuế', c.tax_code]
|
| 643 |
+
].forEach(function (pair) {
|
| 644 |
+
var label = pair[0], v = pair[1];
|
| 645 |
+
if (v == null) return;
|
| 646 |
+
var arr = Array.isArray(v) ? v.filter(Boolean) : [String(v).trim()];
|
| 647 |
+
arr.forEach(function (item) { if (item) lines.push('• ' + label + ': ' + item); });
|
| 648 |
+
});
|
| 649 |
+
lines.push('');
|
| 650 |
+
lines.push('Thông tin chiết khấu mới nhất của anh/chị đã được cập nhật. Truy cập web để xem báo giá kèm chiết khấu nhé!');
|
| 651 |
+
return lines.join('\n');
|
| 652 |
+
}
|
| 653 |
+
// Resolve a customer to a sendable Zalo cid (the customer record's own cid).
|
| 654 |
+
// Zalo chat ids are long NUMERIC strings (e.g. "479b1..." is hex, but real Zalo
|
| 655 |
+
// OA user cids are numeric ~13-24 digits; the web/zalo customers use those).
|
| 656 |
+
// Ketoan-directory rows are keyed by ma_kh with an EMPTY cid — those can't be
|
| 657 |
+
// DMed until the real Zalo cid is present. Skip (return '').
|
| 658 |
+
function customerCidForSend(cust) {
|
| 659 |
+
if (!cust) return '';
|
| 660 |
+
var cidv = cust.cid || '';
|
| 661 |
+
var s = String(cidv).trim();
|
| 662 |
+
if (s && s.length >= 10) {
|
| 663 |
+
// Accept both numeric Zalo ids AND the known admin/hex format; but reject
|
| 664 |
+
// ma_kh-lookalike keys (letters like VASCAS...) with no numeric core that
|
| 665 |
+
// are really directory keys, not Zalo chat ids.
|
| 666 |
+
if (/^\d+$/.test(s)) return s;
|
| 667 |
+
// Known OA/hex cids used in this system (admin 479b1cfad6a83ff666b9 etc.).
|
| 668 |
+
if (/^[0-9a-f]{8,}$/i.test(s) && /^[a-f0-9]+$/i.test(s)) return s;
|
| 669 |
+
return '';
|
| 670 |
+
}
|
| 671 |
+
return '';
|
| 672 |
}
|
| 673 |
+
window.bulkSendViaZalo = async function () {
|
| 674 |
+
var cids = selectedBulkCids();
|
| 675 |
+
if (!cids.length) { toast('⚠️ Chưa chọn khách hàng nào.'); return; }
|
| 676 |
+
if (!(window.canEditCk ? window.canEditCk() : false)) { toast('🔒 Cần nhập mã truy cập.'); return; }
|
| 677 |
+
var sent = 0, skipped = 0;
|
| 678 |
+
for (var i = 0; i < cids.length; i++) {
|
| 679 |
+
var cid = cids[i];
|
| 680 |
+
var cust = (window.__customers || {})[cid] || {};
|
| 681 |
+
var zcid = customerCidForSend(cust);
|
| 682 |
+
if (!zcid) { skipped++; continue; }
|
| 683 |
+
var msg = buildCustomerSendMessage(cust);
|
| 684 |
+
try {
|
| 685 |
+
var r = await fetch('https://bep40-vaistudio-zalo-bot.hf.space/send', {
|
| 686 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 687 |
+
body: JSON.stringify({ user_id: zcid, message: msg })
|
| 688 |
+
});
|
| 689 |
+
var d = await r.json().catch(function () { return {}; });
|
| 690 |
+
if (r.ok && d && d.ok) sent++; else skipped++;
|
| 691 |
+
} catch (e) { skipped++; }
|
| 692 |
+
}
|
| 693 |
+
toast('✅ Đã gửi qua Zalo cho ' + sent + ' khách' + (skipped ? ', bỏ qua ' + skipped + ' (thiếu Zalo ID)' : ''));
|
| 694 |
+
};
|
| 695 |
|
| 696 |
function renderCustomerList() {
|
| 697 |
var box = document.getElementById('cus-list');
|
|
|
|
| 732 |
+ '<button id="cus-close" style="background:none;border:none;color:#fff;font-size:22px;cursor:pointer">✕</button></div>'
|
| 733 |
+ '<div id="cus-gate"></div>'
|
| 734 |
+ '<div style="padding:10px 18px;border-bottom:1px solid #e2e8f0"><input id="cus-q" placeholder="Tìm khách hàng..." style="width:100%;padding:9px;border:2px solid #e2e8f0;border-radius:8px;font-size:13px;box-sizing:border-box;outline:none"></div>'
|
| 735 |
+
+ '<div id="cus-bulkbar" style="display:none;padding:8px 18px;background:#e0f2fe;border-bottom:1px solid #bae6fd;align-items:center;gap:8px;flex-wrap:wrap">'
|
| 736 |
+
+ '<b style="font-size:12px;color:#0369a1">Chiết khấu hàng loạt:</b>'
|
| 737 |
+
+ '<input id="cus-bulk-ck" placeholder="CK%: 35 hoặc Malloca 35, Grob 20" style="flex:1;min-width:180px;padding:7px 10px;border:1.5px solid #7dd3fc;border-radius:8px;font-size:13px;outline:none">'
|
| 738 |
+
+ '<button id="cus-bulk-apply" style="padding:8px 14px;background:#0284c7;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer">Áp cho đã chọn (<span id="cus-bulk-count">0</span>)</button>'
|
| 739 |
+
+ '<button id="cus-bulk-send" style="padding:8px 14px;background:#25D366;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer">Gửi qua Zalo</button>'
|
| 740 |
+
+ '</div>'
|
| 741 |
+ '<div id="cus-list" style="flex:1;overflow-y:auto;padding:10px 18px"></div>'
|
| 742 |
+ '</div>';
|
| 743 |
document.body.appendChild(ov);
|
| 744 |
+
window.__cusModal = ov;
|
| 745 |
+
document.getElementById('cus-close').onclick = function () { ov.remove(); window.__cusModal = null; };
|
| 746 |
|
| 747 |
// Access gate for NORMAL mode (no ?kh link): the full customer directory is
|
| 748 |
// revealed only after the V.AISTUDIO access code is entered. In
|
|
|
|
| 788 |
return;
|
| 789 |
}
|
| 790 |
});
|
| 791 |
+
// ── Live bulk-CK counter: as the admin ticks/unticks checkboxes, show how
|
| 792 |
+
// many are selected (requires admin identity — checked in openCustomerModal).
|
| 793 |
+
ov.addEventListener('change', function (e) {
|
| 794 |
+
if (e.target && e.target.classList && e.target.classList.contains('cus-bulk-chk')) {
|
| 795 |
+
var n = document.getElementById('cus-bulk-count');
|
| 796 |
+
if (n) n.textContent = selectedBulkCids().length;
|
| 797 |
+
}
|
| 798 |
+
});
|
| 799 |
+
// Bulk bar visible only for the admin (after V.AISTUDIO unlock in normal mode).
|
| 800 |
+
var bulkBar = document.getElementById('cus-bulkbar');
|
| 801 |
+
if (bulkBar) bulkBar.style.display = (window.isAdmin ? window.isAdmin() : false) ? 'flex' : 'none';
|
| 802 |
+
var bapply = document.getElementById('cus-bulk-apply');
|
| 803 |
+
if (bapply) bapply.onclick = function () { if (window.bulkApplyCk) window.bulkApplyCk(); };
|
| 804 |
+
var bsend = document.getElementById('cus-bulk-send');
|
| 805 |
+
if (bsend) bsend.onclick = function () { if (window.bulkSendViaZalo) window.bulkSendViaZalo(); };
|
| 806 |
|
| 807 |
// Search filter (non-admin sees only their own row, so the filter is no-op)
|
| 808 |
document.getElementById('cus-q').oninput = function () {
|