vai-avatar / src /order-sync.js
bep40's picture
restore to c1013cc7ebfdc93cbb55645441528a84d871a052: 100% restore of target commit tree
9b44afa verified
Raw
History Blame Contribute Delete
8.02 kB
/**
* 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();
})();