bep40 commited on
Commit
506a118
·
1 Parent(s): d9aa119

Fix customer persistence (POST writes local file + GET reads live repo), fix + Thêm row, add customer-list CSS contrast, add role-based visibility (admin 479b... sees all customers/orders; others only own orders, no customer-list button)

Browse files
Files changed (5) hide show
  1. index.html +21 -4
  2. index.ts +25 -4
  3. src/cart-quote.js +31 -2
  4. src/customers.js +70 -2
  5. src/order-sync.js +37 -2
index.html CHANGED
@@ -523,19 +523,36 @@
523
  .source-card-askai{display:flex;align-items:center;gap:6px;margin:8px 4px 2px;padding:7px 12px;background:linear-gradient(135deg,#003f62,#0077b6);color:#fff;border:none;border-radius:10px;font-size:.72rem;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s;width:fit-content}
524
  .source-card-askai:hover{opacity:.9;transform:translateY(-1px)}
525
 
526
- /* ── Đơn hàng (order) buttons — always visible ── */
527
  #order-btn, #vaistudio-order-btn { display: flex !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  </style>
529
 
530
  <!-- V.AI STUDIO RAG Module - loads products + integrates search/showProduct -->
531
  <script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
532
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
533
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
534
- <script src="./src/cart-quote.js?gc=17"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
535
- <script src="./src/customers.js?gc=3"></script> <!-- Customer list (Danh sách khách hàng) -->
536
  <script src="./src/vaix-rag.js?gc=45"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
537
  <script src="./src/greeting-news.js?gc=21"></script> <!-- Greeting HOT news cards + source links -->
538
- <script src="./src/order-sync.js?gc=12"></script> <!-- Order sync with V.AISTUDIO backend -->
539
  <script type="module" src="./src/app.js?gc=50"></script>
540
  <script src="./src/greeting-source-cards.js?gc=14"></script> <!-- FIX: Nguồn tin cards under greeting -->
541
  <script src="./src/avatar-picker.js?gc=12"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
 
523
  .source-card-askai{display:flex;align-items:center;gap:6px;margin:8px 4px 2px;padding:7px 12px;background:linear-gradient(135deg,#003f62,#0077b6);color:#fff;border:none;border-radius:10px;font-size:.72rem;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s;width:fit-content}
524
  .source-card-askai:hover{opacity:.9;transform:translateY(-1px)}
525
 
526
+ /* ── V.AI STUDIO order buttons ── */
527
  #order-btn, #vaistudio-order-btn { display: flex !important; }
528
+
529
+ /* ── Customer list (Danh sách khách hàng) row styling ──
530
+ Rows use semantic classes injected by customers.js. Style them with
531
+ full contrast so they read clearly against the white modal background. */
532
+ #cus-list .cus-row{display:flex;align-items:center;gap:12px;padding:12px 10px;border:1px solid #e2e8f0;border-radius:12px;margin-bottom:8px;background:#f8fafc;transition:background .15s}
533
+ #cus-list .cus-row:hover{background:#eef6ff;border-color:#b8d4e8}
534
+ #cus-list .cus-row-main{flex:1;min-width:0}
535
+ #cus-list .cus-row-name{font-size:0.86rem;font-weight:700;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
536
+ #cus-list .cus-row-meta{font-size:0.7rem;color:#64748b;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
537
+ #cus-list .cus-row-actions{display:flex;gap:6px;flex-shrink:0}
538
+ #cus-list .cus-select-btn{padding:7px 14px;border:none;border-radius:8px;background:#0077b6;color:#fff;font-size:0.74rem;font-weight:700;cursor:pointer}
539
+ #cus-list .cus-select-btn:hover{background:#005f94}
540
+ #cus-list .cus-edit-btn{padding:7px 12px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;color:#334155;font-size:0.78rem;cursor:pointer}
541
+ #cus-list .cus-edit-btn:hover{border-color:#0077b6;color:#0077b6}
542
+ /* Multi-value rows: give each input row a clear background */
543
+ #cus-detail-modal .cus-mv-row{display:flex;gap:8px;margin-bottom:6px;align-items:center}
544
+ #cus-detail-modal .cus-mv-row input{background:#fff !important}
545
  </style>
546
 
547
  <!-- V.AI STUDIO RAG Module - loads products + integrates search/showProduct -->
548
  <script src="https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js"></script>
549
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
550
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
551
+ <script src="./src/cart-quote.js?gc=18"></script> <!-- Cart + Quote module (Giỏ hàng / Báo giá) -->
552
+ <script src="./src/customers.js?gc=4"></script> <!-- Customer list (Danh sách khách hàng) -->
553
  <script src="./src/vaix-rag.js?gc=45"></script> <!-- V.AI STUDIO RAG Module - loads FASTER via light index; lazy galleries; spec-aware search -->
554
  <script src="./src/greeting-news.js?gc=21"></script> <!-- Greeting HOT news cards + source links -->
555
+ <script src="./src/order-sync.js?gc=13"></script> <!-- Order sync with V.AISTUDIO backend -->
556
  <script type="module" src="./src/app.js?gc=50"></script>
557
  <script src="./src/greeting-source-cards.js?gc=14"></script> <!-- FIX: Nguồn tin cards under greeting -->
558
  <script src="./src/avatar-picker.js?gc=12"></script> <!-- ✨ Circular avatar picker (Mr V / Lisamy) + loading progress bar -->
index.ts CHANGED
@@ -7,7 +7,7 @@
7
  * S2S upstream: victor-gemma-avatar.hf.space S2S backend (smolagents now gates access).
8
  * Text-only chat: /api/chat — uses victor's S2S backend in text-only mode.
9
  */
10
- import { readFileSync } from "fs";
11
  // Read index.html directly as text to avoid Bun HTMLBundle object
12
  const index = readFileSync(new URL("./index.html", import.meta.url), "utf-8");
13
  import { readdir } from "fs/promises";
@@ -1232,10 +1232,26 @@ const server = Bun.serve({
1232
  GET: async () => {
1233
  try {
1234
  const f = join("/app", "customers.json");
1235
- if (!existsSync(f)) return Response.json({});
1236
- const raw = readFileSync(f, "utf-8");
1237
  let data: any = {};
1238
- try { data = JSON.parse(raw); } catch (_e) { data = {}; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1239
  return new Response(JSON.stringify(data), {
1240
  headers: { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" }
1241
  });
@@ -1284,6 +1300,11 @@ const server = Bun.serve({
1284
  const errText = await commit.text().catch(() => "");
1285
  return Response.json({ error: "Commit failed: HTTP " + commit.status + " " + errText.slice(0, 300) }, { status: 502 });
1286
  }
 
 
 
 
 
1287
  return Response.json({ ok: true, updated: changed, customers: data });
1288
  } catch (e: any) {
1289
  return Response.json({ error: e.message }, { status: 500 });
 
7
  * S2S upstream: victor-gemma-avatar.hf.space S2S backend (smolagents now gates access).
8
  * Text-only chat: /api/chat — uses victor's S2S backend in text-only mode.
9
  */
10
+ import { readFileSync, writeFileSync } from "fs";
11
  // Read index.html directly as text to avoid Bun HTMLBundle object
12
  const index = readFileSync(new URL("./index.html", import.meta.url), "utf-8");
13
  import { readdir } from "fs/promises";
 
1232
  GET: async () => {
1233
  try {
1234
  const f = join("/app", "customers.json");
 
 
1235
  let data: any = {};
1236
+ // Prefer LIVE repo HEAD so edits from any writer (Zalo bot upload, new
1237
+ // admin saves) appear immediately without waiting for a rebuild.
1238
+ try {
1239
+ const token = (process.env.HF_INFERENCE_TOKEN || process.env.HF_TOKEN || "").trim();
1240
+ const r = await fetch("https://huggingface.co/spaces/bep40/vai-avatar2/raw/main/customers.json", {
1241
+ headers: token ? { Authorization: "Bearer " + token } : {},
1242
+ signal: AbortSignal.timeout(6000),
1243
+ });
1244
+ if (r.ok) {
1245
+ const raw = await r.text();
1246
+ if (raw && raw.trim()) { try { data = JSON.parse(raw); } catch (_e) {} }
1247
+ }
1248
+ } catch (_e) {}
1249
+ // Fallback: local file (fast path, also fine after a local POST write).
1250
+ if (!data || typeof data !== "object" || !Object.keys(data).length) {
1251
+ if (existsSync(f)) {
1252
+ try { data = JSON.parse(readFileSync(f, "utf-8")); } catch (_e) { data = {}; }
1253
+ }
1254
+ }
1255
  return new Response(JSON.stringify(data), {
1256
  headers: { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" }
1257
  });
 
1300
  const errText = await commit.text().catch(() => "");
1301
  return Response.json({ error: "Commit failed: HTTP " + commit.status + " " + errText.slice(0, 300) }, { status: 502 });
1302
  }
1303
+ // Persist locally too — without this, the live GET keeps returning the
1304
+ // stale /app/customers.json until the Space rebuilds (rebuilds are slow
1305
+ // and a write shouldn't depend on one). Keep local file in sync so
1306
+ // subsequent GET /api/customers reflect the save immediately.
1307
+ try { writeFileSync(f, content); } catch (_e) {}
1308
  return Response.json({ ok: true, updated: changed, customers: data });
1309
  } catch (e: any) {
1310
  return Response.json({ error: e.message }, { status: 500 });
src/cart-quote.js CHANGED
@@ -577,6 +577,15 @@ window.saveOrder = function() {
577
  ['ma_kh','cid','zalo_name','zalo_id','shipping_address','company','company_address','tax_code','representative','recipient','ck'].forEach(function (k) {
578
  if (_cust[k] != null) order[k] = _cust[k];
579
  });
 
 
 
 
 
 
 
 
 
580
  var idx = orders.findIndex(function(o) { return o.code === code; });
581
  if (idx >= 0) orders[idx] = order; else orders.unshift(order);
582
  _lss(ORDER_KEY, orders);
@@ -594,13 +603,33 @@ window.deleteOrder = function(code) {
594
  window.openOrderModal = function() {
595
  var old = document.getElementById('vai-order-modal');
596
  if (old) old.remove();
597
- var orders = getAllOrders();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  var ov = document.createElement('div');
599
  ov.id = 'vai-order-modal';
600
  ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:5000;display:flex;align-items:flex-start;justify-content:center;padding:40px 16px;overflow-y:auto';
601
  ov.onclick = function(e){ if (e.target === ov) ov.remove(); };
602
  ov.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:720px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column">'
603
- + '<div style="padding:14px 18px;background:#003f62;display:flex;align-items:center"><div style="font-size:16px;font-weight:800;color:#fff;flex:1"><i class="fas fa-receipt"></i> Đơn hàng (' + orders.length + ')</div><button id="vai-ord-close" style="background:none;border:none;color:#fff;font-size:22px;cursor:pointer">✕</button></div>'
604
  + '<div style="padding:10px 18px;border-bottom:1px solid #e2e8f0"><input id="vai-ord-q" placeholder="Tìm mã đơn, tên KH, SĐT..." style="width:100%;padding:9px;border:2px solid #e2e8f0;border-radius:8px;font-size:12px;box-sizing:border-box"></div>'
605
  + '<div id="vai-ord-r" style="flex:1;overflow-y:auto;padding:10px 18px"></div>'
606
  + '</div>';
 
577
  ['ma_kh','cid','zalo_name','zalo_id','shipping_address','company','company_address','tax_code','representative','recipient','ck'].forEach(function (k) {
578
  if (_cust[k] != null) order[k] = _cust[k];
579
  });
580
+ // Ensure ownership fields are set so role-based order filtering works.
581
+ if (!order.cid && window.currentCid) { try { order.cid = window.currentCid() || ''; } catch (e) {} }
582
+ if (!order.ma_kh) {
583
+ try {
584
+ var sel2 = JSON.parse(localStorage.getItem('vas_selected_customer') || 'null');
585
+ if (sel2 && sel2.ma_kh) order.ma_kh = sel2.ma_kh;
586
+ else { var p2 = new URLSearchParams(location.search); if (p2.get('kh')) order.ma_kh = p2.get('kh'); }
587
+ } catch (e) {}
588
+ }
589
  var idx = orders.findIndex(function(o) { return o.code === code; });
590
  if (idx >= 0) orders[idx] = order; else orders.unshift(order);
591
  _lss(ORDER_KEY, orders);
 
603
  window.openOrderModal = function() {
604
  var old = document.getElementById('vai-order-modal');
605
  if (old) old.remove();
606
+ var allOrders = getAllOrders();
607
+ // Role-based order visibility: admin sees ALL orders; any other visitor sees
608
+ // only orders belonging to their own customer (matched by cid or ma_kh, or
609
+ // orders created in this same browser session with no owner).
610
+ var myCid = (window.currentCid ? window.currentCid() : '') || '';
611
+ var isAdmin = (window.isAdmin ? window.isAdmin() : (myCid === '479b1cfad6a83ff666b9'));
612
+ var myKh = '';
613
+ try {
614
+ var sel = JSON.parse(localStorage.getItem('vas_selected_customer') || 'null');
615
+ if (sel && sel.ma_kh) myKh = String(sel.ma_kh);
616
+ else { var khp = new URLSearchParams(location.search).get('kh'); if (khp) myKh = khp; }
617
+ } catch (e) {}
618
+ function ownsOrder(o) {
619
+ if (isAdmin) return true;
620
+ if (o.cid && myCid && String(o.cid) === String(myCid)) return true;
621
+ if (o.ma_kh && myKh && String(o.ma_kh).toUpperCase() === String(myKh).toUpperCase()) return true;
622
+ // Orders created in THIS browser without an owner are the user's own.
623
+ if (!o.cid && !o.ma_kh && o.source !== 'vaistudio') return true;
624
+ return false;
625
+ }
626
+ var orders = allOrders.filter(ownsOrder);
627
  var ov = document.createElement('div');
628
  ov.id = 'vai-order-modal';
629
  ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:5000;display:flex;align-items:flex-start;justify-content:center;padding:40px 16px;overflow-y:auto';
630
  ov.onclick = function(e){ if (e.target === ov) ov.remove(); };
631
  ov.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:720px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column">'
632
+ + '<div style="padding:14px 18px;background:#003f62;display:flex;align-items:center"><div style="font-size:16px;font-weight:800;color:#fff;flex:1"><i class="fas fa-receipt"></i> Đơn hàng (' + orders.length + (isAdmin ? ')' : ' của tôi)') + '</div><button id="vai-ord-close" style="background:none;border:none;color:#fff;font-size:22px;cursor:pointer">✕</button></div>'
633
  + '<div style="padding:10px 18px;border-bottom:1px solid #e2e8f0"><input id="vai-ord-q" placeholder="Tìm mã đơn, tên KH, SĐT..." style="width:100%;padding:9px;border:2px solid #e2e8f0;border-radius:8px;font-size:12px;box-sizing:border-box"></div>'
634
  + '<div id="vai-ord-r" style="flex:1;overflow-y:auto;padding:10px 18px"></div>'
635
  + '</div>';
src/customers.js CHANGED
@@ -105,6 +105,57 @@
105
  } catch (e) {}
106
  })();
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  // ── Discount link (?kh=MA_KH) auto-apply on load ──
109
  // The Zalo bot appends a link https://<space>/?kh=<ma_kh> next to the
110
  // customer code. When the user opens it, look up that customer's ck in
@@ -294,13 +345,23 @@
294
  var add = e.target.closest('.cus-mv-add');
295
  if (add) {
296
  var field = add.getAttribute('data-field');
297
- var list = add.closest('div').querySelector('.cus-mv-list');
 
 
 
 
 
 
 
298
  var row = document.createElement('div');
299
  row.className = 'cus-mv-row';
300
  row.setAttribute('data-field', field);
301
  row.innerHTML = '<input class="cus-mv-input" placeholder="' + esc(field) + '" style="flex:1;padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13px;outline:none">'
302
  + '<button class="cus-mv-del" style="background:#fee2e2;border:none;color:#dc2626;padding:6px 10px;border-radius:6px;cursor:pointer">−</button>';
303
  list.appendChild(row);
 
 
 
304
  return;
305
  }
306
  var del = e.target.closest('.cus-mv-del');
@@ -352,7 +413,14 @@
352
  });
353
  _lss('vas_quote_unlocked', '1'); // ensure save path unlocked
354
  try {
355
- await saveCustomers([record], ACCESS_CODE);
 
 
 
 
 
 
 
356
  toast('✅ Đã lưu khách hàng');
357
  ov.remove();
358
  renderCustomerList(); // refresh list
 
105
  } catch (e) {}
106
  })();
107
 
108
+ // ── Current visitor identity & role ──
109
+ // Admin = the V.AI STUDIO Zalo owner (cid 479b1cfad6a83ff666b9). Admin sees
110
+ // the customer-list button (all customers) and ALL orders. Any other visitor
111
+ // (e.g. a customer via a ?kh= discount link) must NOT see the customer-list
112
+ // button and must only see their OWN orders.
113
+ var ADMIN_CID = '479b1cfad6a83ff666b9';
114
+ function currentCid() {
115
+ try {
116
+ var p = new URLSearchParams(location.search);
117
+ var kh = p.get('kh');
118
+ if (kh) {
119
+ // ?kh=<ma_kh> — find the matching customer's cid.
120
+ var cust = window.__customers || {};
121
+ for (var k in cust) {
122
+ if (cust.hasOwnProperty(k) && String(cust[k].ma_kh || '').trim().toUpperCase() === String(kh).trim().toUpperCase()) {
123
+ return k;
124
+ }
125
+ }
126
+ }
127
+ } catch (e) {}
128
+ // Fall back to the selected customer (may be set by admin picking a customer).
129
+ try {
130
+ var sel = JSON.parse(localStorage.getItem('vas_selected_customer') || 'null');
131
+ if (sel && sel.cid) return sel.cid;
132
+ } catch (e) {}
133
+ return '';
134
+ }
135
+ window.currentCid = currentCid;
136
+ // Admin = V.AI STUDIO owner (cid matches) OR the user has unlocked editing
137
+ // with the V.AISTUDIO access code (admin-only signal). Keeping both lets the
138
+ // admin see the customer list even when opening the Space directly (no ?kh).
139
+ window.isAdmin = function () {
140
+ if (currentCid() === ADMIN_CID) return true;
141
+ try { return localStorage.getItem('vas_quote_unlocked') === '1'; } catch (e) { return false; }
142
+ };
143
+
144
+ // ── Role-based UI visibility ──
145
+ // Admin: sees customer-list button. Non-admin: hides it so customers cannot
146
+ // browse/manage the shared customer directory.
147
+ function applyRoleVisibility() {
148
+ var admin = (window.isAdmin ? window.isAdmin() : false);
149
+ var custBtns = document.querySelectorAll('#customer-btn, #vaistudio-customer-btn');
150
+ custBtns.forEach(function (b) { b.style.display = admin ? '' : 'none'; });
151
+ }
152
+ window.applyRoleVisibility = applyRoleVisibility;
153
+ // Re-apply after the catalog/buttons are ready and after discount-link load.
154
+ if (document.readyState !== 'loading') { setTimeout(applyRoleVisibility, 400); }
155
+ if (typeof window.addEventListener === 'function') {
156
+ window.addEventListener('load', function () { setTimeout(applyRoleVisibility, 800); });
157
+ }
158
+
159
  // ── Discount link (?kh=MA_KH) auto-apply on load ──
160
  // The Zalo bot appends a link https://<space>/?kh=<ma_kh> next to the
161
  // customer code. When the user opens it, look up that customer's ck in
 
345
  var add = e.target.closest('.cus-mv-add');
346
  if (add) {
347
  var field = add.getAttribute('data-field');
348
+ // FIND the correct list: the add button sits in the header-row <div>,
349
+ // whose PARENT (the field wrapper) contains the .cus-mv-list. Using
350
+ // closest('div') returns the header row, whose querySelector on the
351
+ // sibling list returns null -> row was never appended. Fix below.
352
+ var headerRow = add.closest('div');
353
+ var wrap = (headerRow && headerRow.parentNode) ? headerRow.parentNode : null;
354
+ var list = wrap ? wrap.querySelector('.cus-mv-list') : null;
355
+ if (!list) { toast('Không tìm thấy vùng nhập'); return; }
356
  var row = document.createElement('div');
357
  row.className = 'cus-mv-row';
358
  row.setAttribute('data-field', field);
359
  row.innerHTML = '<input class="cus-mv-input" placeholder="' + esc(field) + '" style="flex:1;padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13px;outline:none">'
360
  + '<button class="cus-mv-del" style="background:#fee2e2;border:none;color:#dc2626;padding:6px 10px;border-radius:6px;cursor:pointer">−</button>';
361
  list.appendChild(row);
362
+ // Focus the newly added input for immediate data entry.
363
+ var ni = row.querySelector('.cus-mv-input');
364
+ if (ni) ni.focus();
365
  return;
366
  }
367
  var del = e.target.closest('.cus-mv-del');
 
413
  });
414
  _lss('vas_quote_unlocked', '1'); // ensure save path unlocked
415
  try {
416
+ var resp = await saveCustomers([record], ACCESS_CODE);
417
+ // Update the in-memory cache with the server's authoritative data so the
418
+ // list reflects the just-saved values immediately (avoids stale re-open).
419
+ if (resp && resp.customers && typeof resp.customers === 'object') {
420
+ window.__customers = resp.customers;
421
+ } else {
422
+ try { window.__customers = await fetchCustomers(); } catch (_e) {}
423
+ }
424
  toast('✅ Đã lưu khách hàng');
425
  ov.remove();
426
  renderCustomerList(); // refresh list
src/order-sync.js CHANGED
@@ -53,7 +53,10 @@
53
  remaining: order.remaining || 0,
54
  status: order.status || 'pending',
55
  savedAt: order.savedAt || new Date().toISOString(),
56
- source: 'avatar2'
 
 
 
57
  };
58
  }
59
 
@@ -100,11 +103,38 @@
100
  grandTotal: so.grandTotal || so.total || 0,
101
  status: so.status || 'pending',
102
  savedAt: so.savedAt || '',
103
- source: so.source || 'vaistudio'
 
 
104
  };
105
  }
106
 
107
  // ---- merge server orders into local malloca_orders (V.AISTUDIO-style) ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  function syncFromServer() {
109
  try {
110
  var ac = new AbortController();
@@ -122,6 +152,11 @@
122
  serverOrders.forEach(function (so) {
123
  var code = so.ma_don || so.code || '';
124
  if (!code) return;
 
 
 
 
 
125
  var existingIdx = local.findIndex(function (o) { return String(o.code) === String(code); });
126
  if (existingIdx >= 0 && !so.overwrite && !so.savedAt && so.source !== 'avatar2') return;
127
  var order = toLocalOrder(so);
 
53
  remaining: order.remaining || 0,
54
  status: order.status || 'pending',
55
  savedAt: order.savedAt || new Date().toISOString(),
56
+ source: 'avatar2',
57
+ // ownership for privacy filtering
58
+ cid: order.cid || '',
59
+ ma_kh: order.ma_kh || ''
60
  };
61
  }
62
 
 
103
  grandTotal: so.grandTotal || so.total || 0,
104
  status: so.status || 'pending',
105
  savedAt: so.savedAt || '',
106
+ source: so.source || 'vaistudio',
107
+ cid: so.cid || '',
108
+ ma_kh: so.ma_kh || ''
109
  };
110
  }
111
 
112
  // ---- merge server orders into local malloca_orders (V.AISTUDIO-style) ----
113
+ // PRIVACY: a non-admin visitor must only ever see/sync their OWN orders.
114
+ // Admin (cid 479b1cfad6a83ff666b9) syncs the full list. Filter server orders
115
+ // by the current visitor's cid / ma_kh before merging, so other customers'
116
+ // orders never land in this browser's list.
117
+ function currentIdentity() {
118
+ var cid = '', kh = '';
119
+ try {
120
+ if (window.currentCid) cid = window.currentCid() || '';
121
+ var sel = JSON.parse(localStorage.getItem('vas_selected_customer') || 'null');
122
+ if (sel && sel.ma_kh) kh = String(sel.ma_kh);
123
+ else { var p = new URLSearchParams(location.search); if (p.get('kh')) kh = p.get('kh'); }
124
+ } catch (e) {}
125
+ return { cid: cid, kh: kh };
126
+ }
127
+ function orderBelongsToMe(o, strict) {
128
+ try { if (window.isAdmin && window.isAdmin()) return true; } catch (e) {}
129
+ var me = currentIdentity();
130
+ if (o.cid && me.cid && String(o.cid) === String(me.cid)) return true;
131
+ if (o.ma_kh && me.kh && String(o.ma_kh).toUpperCase() === String(me.kh).toUpperCase()) return true;
132
+ // Ownerless orders: allowed only for browser-local display (strict=false),
133
+ // never pulled from the server for a non-admin (strict=true) — those can't
134
+ // be attributed and would leak other people's data.
135
+ if (!strict && !o.cid && !o.ma_kh) return true;
136
+ return false;
137
+ }
138
  function syncFromServer() {
139
  try {
140
  var ac = new AbortController();
 
152
  serverOrders.forEach(function (so) {
153
  var code = so.ma_don || so.code || '';
154
  if (!code) return;
155
+ // Privacy: for non-admin only pull orders that match THIS visitor's
156
+ // identity (strict). Ownerless server orders are never synced to a
157
+ // non-admin browser.
158
+ var oId = { cid: so.cid || so.zalo_id || '', ma_kh: so.ma_kh || '' };
159
+ if (!orderBelongsToMe(oId, true)) return;
160
  var existingIdx = local.findIndex(function (o) { return String(o.code) === String(code); });
161
  if (existingIdx >= 0 && !so.overwrite && !so.savedAt && so.source !== 'avatar2') return;
162
  var order = toLocalOrder(so);