/** * V.AI AVATAR2 — Order Sync with V.AISTUDIO backend * ================================================= * Keeps the order list ("Đơn hàng") always in sync with bep40/V.AISTUDIO's * shared order backend (bep40-vaistudio-ketoan-bot.hf.space). * * - On save order (avatar2): POST to /order-confirmed so it appears in * V.AISTUDIO's server-synced order list too. * - On load + order modal open: GET /orders and merge server orders into * local malloca_orders (same conversion as V.AISTUDIO's order-store.js). * * Does NOT replace avatar2's own localStorage logic — it layers sync on top. */ (function () { 'use strict'; var KETOAN_API = 'https://bep40-vaistudio-ketoan-bot.hf.space'; var ORDER_KEY = 'malloca_orders'; function _lsr(key, def) { try { var v = JSON.parse(localStorage.getItem(key)); return v == null ? def : v; } catch (e) { return def; } } function _lsw(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {} } function _toast(m) { try { (window.showToast || window.toast || alert)(m); } catch (e) {} } function norm(s) { return String(s == null ? '' : s); } // ---- server payload (matches V.AISTUDIO sendToKetoanBot) ---- function toServerPayload(order) { var items = (order && order.items) || []; return { ma_don: order.code || '', khach: order.customer || '', dien_thoai: order.phone || '', dia_chi: order.addr || '', email: order.email || '', date: order.date || '', items: items.map(function (it) { return { ma: it.model || '', model: it.model || '', ten: it.name || '', name: it.name || '', brand: it.brand || '', image: it.image || it.img || '', img: it.image || it.img || '', specs: it.specs || it.info || '', info: it.specs || it.info || '', sl: it.qty || 1, qty: it.qty || 1, gia_ban: it.discPrice || it.price || 0, price: it.price || 0, discPrice: it.discPrice || it.price || 0, note: it.note || '' }; }), fees: (order && order.fees) || [], grandTotal: order.grandTotal || order.total || 0, deposit: order.deposit || 0, remaining: order.remaining || 0, status: order.status || 'pending', savedAt: order.savedAt || new Date().toISOString(), source: 'avatar2', // ownership for privacy filtering cid: order.cid || '', ma_kh: order.ma_kh || '' }; } // ---- push a single order to the shared backend ---- function syncOrderToServer(order) { if (!order || !order.code) return; try { fetch(KETOAN_API + '/order-confirmed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(toServerPayload(order)) }).then(function (r) { return r.ok ? r.json() : Promise.reject(); }) .then(function (d) { if (d && (d.ok !== false)) { /* ok */ } }) .catch(function () { /* backend asleep — will be handled on next sync */ }); } catch (e) {} } // ---- convert a server order into avatar2 local format ---- function toLocalOrder(so) { var code = so.ma_don || so.code || ''; return { code: code, customer: so.khach || so.customer || '', phone: so.dien_thoai || so.phone || '', email: so.email || '', addr: so.dia_chi || so.addr || '', date: so.date || so.ngay || new Date().toLocaleDateString('vi-VN'), items: ((so.items || [])).map(function (it) { return { name: it.ten || it.name || '', model: it.ma || it.model || '', brand: it.brand || '', image: it.image || it.img || '', specs: it.specs || it.info || '', qty: it.sl || it.qty || 1, price: it.price || it.gia_goc || it.listPrice || it.originalPrice || 0, discPrice: it.discPrice || it.gia_ban || it.price || 0, total: (it.gia_ban || it.price || 0) * (it.sl || it.qty || 1), note: it.note || '' }; }), fees: so.fees || [], deposit: so.deposit || 0, grandTotal: so.grandTotal || so.total || 0, status: so.status || 'pending', savedAt: so.savedAt || '', source: so.source || 'vaistudio', cid: so.cid || '', ma_kh: so.ma_kh || '' }; } // ---- merge server orders into local malloca_orders (V.AISTUDIO-style) ---- // PRIVACY: a non-admin visitor must only ever see/sync their OWN orders. // Admin (cid 479b1cfad6a83ff666b9) syncs the full list. Filter server orders // by the current visitor's cid / ma_kh before merging, so other customers' // orders never land in this browser's list. function currentIdentity() { var cid = '', kh = ''; try { if (window.currentCid) cid = window.currentCid() || ''; var sel = JSON.parse(localStorage.getItem('vas_selected_customer') || 'null'); if (sel && sel.ma_kh) kh = String(sel.ma_kh); else { var p = new URLSearchParams(location.search); if (p.get('kh')) kh = p.get('kh'); } } catch (e) {} return { cid: cid, kh: kh }; } function orderBelongsToMe(o, strict) { try { if (window.isAdmin && window.isAdmin()) return true; } catch (e) {} var me = currentIdentity(); if (o.cid && me.cid && String(o.cid) === String(me.cid)) return true; if (o.ma_kh && me.kh && String(o.ma_kh).toUpperCase() === String(me.kh).toUpperCase()) return true; // Ownerless orders: allowed only for browser-local display (strict=false), // never pulled from the server for a non-admin (strict=true) — those can't // be attributed and would leak other people's data. if (!strict && !o.cid && !o.ma_kh) return true; return false; } function syncFromServer() { try { var ac = new AbortController(); var t = setTimeout(function () { try { ac.abort(); } catch (e) {} }, 8000); fetch(KETOAN_API + '/orders', { method: 'GET', signal: ac.signal }) .then(function (r) { return r.ok ? r.json() : Promise.reject(); }) .then(function (d) { clearTimeout(t); var serverOrders = (d && d.data) || []; if (!serverOrders.length) return; var local = _lsr(ORDER_KEY, []); var codes = {}; local.forEach(function (o) { if (o.code) codes[String(o.code)] = true; }); var changed = false; serverOrders.forEach(function (so) { var code = so.ma_don || so.code || ''; if (!code) return; // Privacy: for non-admin only pull orders that match THIS visitor's // identity (strict). Ownerless server orders are never synced to a // non-admin browser. var oId = { cid: so.cid || so.zalo_id || '', ma_kh: so.ma_kh || '' }; if (!orderBelongsToMe(oId, true)) return; var existingIdx = local.findIndex(function (o) { return String(o.code) === String(code); }); if (existingIdx >= 0 && !so.overwrite && !so.savedAt && so.source !== 'avatar2') return; var order = toLocalOrder(so); if (existingIdx >= 0) { if (so.source === 'avatar2' && !so.overwrite) return; local[existingIdx] = order; } else { local.unshift(order); } changed = true; }); if (changed) _lsw(ORDER_KEY, local); }) .catch(function () { /* backend unavailable */ }) .finally(function () { clearTimeout(t); }); } catch (e) {} } // ---- hook into avatar2 save functions (idempotent overrides) ---- function hook(name, wrapper) { try { var orig = window[name]; if (typeof orig !== 'function') { window[name] = wrapper(function () {}); return; } if (orig.__synced) return; window[name] = function () { var args = arguments; var ret = orig.apply(this, args); wrapper.apply(this, args); return ret; }; window[name].__synced = true; } catch (e) {} } function initHooks() { // saveOrder (cart-quote): reads cart data, no order arg — pull latest from storage hook('saveOrder', function () { var orders = _lsr(ORDER_KEY, []); if (orders.length) { var latest = orders[0]; // push to server; re-run sync shortly after so it round-trips syncOrderToServer(latest); setTimeout(syncFromServer, 800); } }); // saveOrders (greeting-news / other modules): takes the full list hook('saveOrders', function (list) { var arr = list || []; if (arr.length) syncOrderToServer(arr[0]); setTimeout(syncFromServer, 800); }); // deleteOrder (cart-quote): fire-and-forget, then re-sync hook('deleteOrder', function (code) { setTimeout(syncFromServer, 500); }); } // ---- wire order button / modal open to refresh from server ---- function wireOpenRefresh() { var btn = document.getElementById('order-btn') || document.getElementById('vaistudio-order-btn'); if (btn) { var orig = btn.onclick; var handler = function (e) { syncFromServer(); if (typeof orig === 'function') return orig.call(btn, e); }; btn.addEventListener('click', handler); } // also refresh whenever the order modal is opened via openOrderModal/openOrderPage ['openOrderModal', 'openOrderPage'].forEach(function (k) { hook(k, function () { syncFromServer(); }); }); } function boot() { initHooks(); wireOpenRefresh(); // initial pull after a short delay so the app's own load doesn't race setTimeout(syncFromServer, 1500); window.vaiOrdersSync = { syncFromServer: syncFromServer, syncOrderToServer: syncOrderToServer }; } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();