Spaces:
Running
Running
restore to c1013cc7ebfdc93cbb55645441528a84d871a052: 100% restore of target commit tree
9b44afa verified | /** | |
| * 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' | |
| }; | |
| } | |
| // ---- 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' | |
| }; | |
| } | |
| // ---- merge server orders into local malloca_orders (V.AISTUDIO-style) ---- | |
| 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; | |
| 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(); | |
| })(); | |