Spaces:
Running
Running
Sửa quản lý đơn hàng: (1) click checkbox/label không còn mở chi tiết đơn; (2) bulkbar thêm nút 'Hợp nhất đã chọn' gộp N đơn; (3) hiển thị ngày tạo đơn (savedAt) trên mỗi đơn; (4) xóa vĩnh viễn — tombstone vaix_deleted_orders chặn order-sync merge lại đơn đã xóa
Browse files- src/cart-quote.js +72 -6
- src/order-sync.js +13 -1
src/cart-quote.js
CHANGED
|
@@ -1030,9 +1030,26 @@ function mergeTwoOrders(a, b) {
|
|
| 1030 |
}
|
| 1031 |
window.mergeTwoOrders = mergeTwoOrders;
|
| 1032 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1033 |
window.deleteOrder = function(code) {
|
| 1034 |
_lss(ORDER_KEY, getAllOrders().filter(function(o){ return o.code !== code; }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
};
|
|
|
|
|
|
|
| 1036 |
|
| 1037 |
window.openOrderModal = function() {
|
| 1038 |
var old = document.getElementById('vai-order-modal');
|
|
@@ -1065,10 +1082,11 @@ window.openOrderModal = function() {
|
|
| 1065 |
ov.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:720px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column">'
|
| 1066 |
+ '<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 (<span id="vai-ord-count">' + orders.length + '</span>' + (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>'
|
| 1067 |
+ '<div style="padding:10px 18px 0;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>'
|
| 1068 |
-
+ '<div id="vai-ord-bulkbar" style="display:none;padding:8px 18px;background:#
|
| 1069 |
-
+ '<b style="font-size:12px;color:#
|
| 1070 |
-
+ '<span style="font-size:12px;color:#
|
| 1071 |
-
+ '<button id="vai-ord-bulk-
|
|
|
|
| 1072 |
+ '<button id="vai-ord-bulk-cancel" style="padding:8px 12px;background:#e2e8f0;color:#334155;border:none;border-radius:8px;font-size:12px;cursor:pointer">Hủy</button>'
|
| 1073 |
+ '</div>'
|
| 1074 |
+ '<div id="vai-ord-tabs" style="padding:8px 18px 0;display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid #e2e8f0"></div>'
|
|
@@ -1100,6 +1118,24 @@ window.openOrderModal = function() {
|
|
| 1100 |
b.onclick = function () { curTab = b.getAttribute('data-tab'); renderTabs(); filter(); };
|
| 1101 |
});
|
| 1102 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
function render(list) {
|
| 1104 |
renderTabs();
|
| 1105 |
if (!list.length) {
|
|
@@ -1123,7 +1159,7 @@ window.openOrderModal = function() {
|
|
| 1123 |
+ '<button class="voi-merge" data-code="' + esc(o.code) + '" title="Hợp nhất đơn này với đơn khác" style="background:#eff6ff;border:none;color:#1d4ed8;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">🔀 Hợp nhất</button>'
|
| 1124 |
+ '<button class="voi-del" data-code="' + esc(o.code) + '" style="background:#fee2e2;border:none;color:#dc2626;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">🗑</button></div>'
|
| 1125 |
+ '<div style="font-size:11px;color:#64748b;margin-top:3px">' + esc(o.customer) + ' • ' + (o.phone || '') + ' • ' + total.toLocaleString('vi-VN') + '₫</div>'
|
| 1126 |
-
+ '<div style="font-size:11px;color:#94a3b8;margin-top:2px">' + (o.items || []).length + ' SP • ' + esc(o.date) + '</div>'
|
| 1127 |
+ '</div>';
|
| 1128 |
}).join('');
|
| 1129 |
}
|
|
@@ -1157,6 +1193,27 @@ window.openOrderModal = function() {
|
|
| 1157 |
ov.addEventListener('change', function (e) {
|
| 1158 |
if (e.target && e.target.classList && e.target.classList.contains('voi-bulk-chk')) updateBulkBar();
|
| 1159 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1160 |
var bulkDel = document.getElementById('vai-ord-bulk-del');
|
| 1161 |
if (bulkDel) bulkDel.onclick = function () {
|
| 1162 |
var chosen = selectedOrderCodes();
|
|
@@ -1164,6 +1221,11 @@ window.openOrderModal = function() {
|
|
| 1164 |
if (!confirm('Xóa ' + chosen.length + ' đơn hàng loạt?')) return;
|
| 1165 |
var all2 = getAllOrders();
|
| 1166 |
_lss(ORDER_KEY, all2.filter(function (o) { return chosen.indexOf(String(o.code)) === -1; }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1167 |
orders = getAllOrders().filter(ownsOrder);
|
| 1168 |
var cnt = document.getElementById('vai-ord-count');
|
| 1169 |
if (cnt) cnt.textContent = orders.length;
|
|
@@ -1217,8 +1279,12 @@ window.openOrderModal = function() {
|
|
| 1217 |
filter(); updateBulkBar();
|
| 1218 |
}
|
| 1219 |
re.onclick = function(e) {
|
|
|
|
|
|
|
| 1220 |
var chk = e.target.closest('.voi-bulk-chk');
|
| 1221 |
-
if (chk
|
|
|
|
|
|
|
| 1222 |
var rn = e.target.closest('.voi-rename');
|
| 1223 |
if (rn) { e.stopPropagation(); renameOrder(rn.getAttribute('data-code')); return; }
|
| 1224 |
var cp = e.target.closest('.voi-copy');
|
|
|
|
| 1030 |
}
|
| 1031 |
window.mergeTwoOrders = mergeTwoOrders;
|
| 1032 |
|
| 1033 |
+
// Xóa đơn VĨNH VIỄN: ngoài xóa khỏi malloca_orders, ghi vào tombstone
|
| 1034 |
+
// vaix_deleted_orders để order-sync (order-sync.js GET /orders từ server
|
| 1035 |
+
// V.AISTUDIO) không merge đơn đã xóa quay lại localStorage.
|
| 1036 |
+
function tombstoneOrders(codes) {
|
| 1037 |
+
try {
|
| 1038 |
+
var arr = _ls('vaix_deleted_orders', '[]');
|
| 1039 |
+
(codes || []).forEach(function (c) { if (c && arr.indexOf(String(c)) === -1) arr.push(String(c)); });
|
| 1040 |
+
_lss('vaix_deleted_orders', arr);
|
| 1041 |
+
} catch (e) {}
|
| 1042 |
+
}
|
| 1043 |
window.deleteOrder = function(code) {
|
| 1044 |
_lss(ORDER_KEY, getAllOrders().filter(function(o){ return o.code !== code; }));
|
| 1045 |
+
tombstoneOrders([code]);
|
| 1046 |
+
try {
|
| 1047 |
+
var syn = window.vaiOrdersSync && window.vaiOrdersSync.deleteRemoteOrder;
|
| 1048 |
+
if (syn) syn(code); else if (window.vaiOrderDeleteRemote) window.vaiOrderDeleteRemote(code);
|
| 1049 |
+
} catch (e) {}
|
| 1050 |
};
|
| 1051 |
+
window.tombstoneOrders = tombstoneOrders;
|
| 1052 |
+
window.getDeletedOrderCodes = function () { try { return _ls('vaix_deleted_orders', '[]'); } catch (e) { return []; } };
|
| 1053 |
|
| 1054 |
window.openOrderModal = function() {
|
| 1055 |
var old = document.getElementById('vai-order-modal');
|
|
|
|
| 1082 |
ov.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:720px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column">'
|
| 1083 |
+ '<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 (<span id="vai-ord-count">' + orders.length + '</span>' + (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>'
|
| 1084 |
+ '<div style="padding:10px 18px 0;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>'
|
| 1085 |
+
+ '<div id="vai-ord-bulkbar" style="display:none;padding:8px 18px;background:#eef2ff;border-bottom:1px solid #c7d2fe;align-items:center;gap:8px;flex-wrap:wrap">'
|
| 1086 |
+
+ '<b style="font-size:12px;color:#3730a3">⚡ Hàng loạt:</b>'
|
| 1087 |
+
+ '<span style="font-size:12px;color:#4338ca">đã chọn <b id="vai-ord-bulk-count">0</b> đơn</span>'
|
| 1088 |
+
+ '<button id="vai-ord-bulk-merge" style="padding:8px 14px;background:#4f46e5;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer">🔀 Hợp nhất đã chọn</button>'
|
| 1089 |
+
+ '<button id="vai-ord-bulk-del" style="padding:8px 14px;background:#dc2626;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer">🗑 Xóa đã chọn</button>'
|
| 1090 |
+ '<button id="vai-ord-bulk-cancel" style="padding:8px 12px;background:#e2e8f0;color:#334155;border:none;border-radius:8px;font-size:12px;cursor:pointer">Hủy</button>'
|
| 1091 |
+ '</div>'
|
| 1092 |
+ '<div id="vai-ord-tabs" style="padding:8px 18px 0;display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid #e2e8f0"></div>'
|
|
|
|
| 1118 |
b.onclick = function () { curTab = b.getAttribute('data-tab'); renderTabs(); filter(); };
|
| 1119 |
});
|
| 1120 |
}
|
| 1121 |
+
function fmtCreated(o) {
|
| 1122 |
+
// Ưu tiên savedAt (ISO) — ngày tạo đơn thực; fallback o.date (ngày báo giá).
|
| 1123 |
+
var src = o.savedAt || o.createdAt || '';
|
| 1124 |
+
if (src) {
|
| 1125 |
+
try {
|
| 1126 |
+
var dt = new Date(src);
|
| 1127 |
+
if (!isNaN(dt.getTime())) {
|
| 1128 |
+
var d2 = String(dt.getDate()).padStart(2, '0');
|
| 1129 |
+
var m2 = String(dt.getMonth() + 1).padStart(2, '0');
|
| 1130 |
+
var y2 = dt.getFullYear();
|
| 1131 |
+
var h2 = String(dt.getHours()).padStart(2, '0');
|
| 1132 |
+
var mi2 = String(dt.getMinutes()).padStart(2, '0');
|
| 1133 |
+
return d2 + '/' + m2 + '/' + y2 + ' ' + h2 + ':' + mi2;
|
| 1134 |
+
}
|
| 1135 |
+
} catch (e) {}
|
| 1136 |
+
}
|
| 1137 |
+
return o.date || '';
|
| 1138 |
+
}
|
| 1139 |
function render(list) {
|
| 1140 |
renderTabs();
|
| 1141 |
if (!list.length) {
|
|
|
|
| 1159 |
+ '<button class="voi-merge" data-code="' + esc(o.code) + '" title="Hợp nhất đơn này với đơn khác" style="background:#eff6ff;border:none;color:#1d4ed8;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">🔀 Hợp nhất</button>'
|
| 1160 |
+ '<button class="voi-del" data-code="' + esc(o.code) + '" style="background:#fee2e2;border:none;color:#dc2626;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px">🗑</button></div>'
|
| 1161 |
+ '<div style="font-size:11px;color:#64748b;margin-top:3px">' + esc(o.customer) + ' • ' + (o.phone || '') + ' • ' + total.toLocaleString('vi-VN') + '₫</div>'
|
| 1162 |
+
+ '<div style="font-size:11px;color:#94a3b8;margin-top:2px">' + (o.items || []).length + ' SP • 📅 Tạo: ' + esc(fmtCreated(o)) + (o.date && o.date !== fmtCreated(o) ? ' • Báo giá: ' + esc(o.date) : '') + '</div>'
|
| 1163 |
+ '</div>';
|
| 1164 |
}).join('');
|
| 1165 |
}
|
|
|
|
| 1193 |
ov.addEventListener('change', function (e) {
|
| 1194 |
if (e.target && e.target.classList && e.target.classList.contains('voi-bulk-chk')) updateBulkBar();
|
| 1195 |
});
|
| 1196 |
+
var bulkMerge = document.getElementById('vai-ord-bulk-merge');
|
| 1197 |
+
if (bulkMerge) bulkMerge.onclick = function () {
|
| 1198 |
+
var chosen = selectedOrderCodes();
|
| 1199 |
+
if (chosen.length < 2) { window.showToast && window.showToast('Chọn ít nhất 2 đơn để hợp nhất'); return; }
|
| 1200 |
+
if (!confirm('Hợp nhất ' + chosen.length + ' đơn đã chọn thành 1 đơn?')) return;
|
| 1201 |
+
var allM = getAllOrders();
|
| 1202 |
+
var picked = chosen.map(function (c) { return allM.find(function (o) { return String(o.code) === String(c); }); }).filter(Boolean);
|
| 1203 |
+
if (picked.length < 2) { window.showToast && window.showToast('Không đủ đơn hợp lệ để hợp nhất'); return; }
|
| 1204 |
+
// Hợp nhất tuần tự: picked[0] làm gốc, lần lượt gộp các đơn còn lại vào.
|
| 1205 |
+
var merged = mergeTwoOrders(picked[0], picked[1]);
|
| 1206 |
+
for (var mi = 2; mi < picked.length; mi++) {
|
| 1207 |
+
var cur = getAllOrders().find(function (o) { return String(o.code) === String(merged.code); });
|
| 1208 |
+
if (cur) merged = mergeTwoOrders(cur, picked[mi]);
|
| 1209 |
+
}
|
| 1210 |
+
window.showToast && window.showToast('✅ Đã hợp nhất ' + picked.length + ' đơn → ' + merged.code);
|
| 1211 |
+
ov.querySelectorAll('.voi-bulk-chk:checked').forEach(function (chk) { chk.checked = false; });
|
| 1212 |
+
orders = getAllOrders().filter(ownsOrder);
|
| 1213 |
+
var cntM = document.getElementById('vai-ord-count');
|
| 1214 |
+
if (cntM) cntM.textContent = orders.length;
|
| 1215 |
+
filter(); updateBulkBar();
|
| 1216 |
+
};
|
| 1217 |
var bulkDel = document.getElementById('vai-ord-bulk-del');
|
| 1218 |
if (bulkDel) bulkDel.onclick = function () {
|
| 1219 |
var chosen = selectedOrderCodes();
|
|
|
|
| 1221 |
if (!confirm('Xóa ' + chosen.length + ' đơn hàng loạt?')) return;
|
| 1222 |
var all2 = getAllOrders();
|
| 1223 |
_lss(ORDER_KEY, all2.filter(function (o) { return chosen.indexOf(String(o.code)) === -1; }));
|
| 1224 |
+
tombstoneOrders(chosen);
|
| 1225 |
+
try {
|
| 1226 |
+
var syn2 = window.vaiOrdersSync && window.vaiOrdersSync.deleteRemoteOrder;
|
| 1227 |
+
if (syn2) chosen.forEach(function (c) { syn2(c); });
|
| 1228 |
+
} catch (e) {}
|
| 1229 |
orders = getAllOrders().filter(ownsOrder);
|
| 1230 |
var cnt = document.getElementById('vai-ord-count');
|
| 1231 |
if (cnt) cnt.textContent = orders.length;
|
|
|
|
| 1279 |
filter(); updateBulkBar();
|
| 1280 |
}
|
| 1281 |
re.onclick = function(e) {
|
| 1282 |
+
// FIX: click trên checkbox HOẶC label của nó (.voi-chk) chỉ chọn hàng loạt,
|
| 1283 |
+
// không được mở chi tiết đơn. Trước đây click vào label rơi xuống openOrderDetail.
|
| 1284 |
var chk = e.target.closest('.voi-bulk-chk');
|
| 1285 |
+
if (chk) { e.stopPropagation(); if (e.target.tagName !== 'input') { chk.checked = !chk.checked; updateBulkBar(); } return; }
|
| 1286 |
+
var chkLabel = e.target.closest('.voi-chk');
|
| 1287 |
+
if (chkLabel) { e.stopPropagation(); return; }
|
| 1288 |
var rn = e.target.closest('.voi-rename');
|
| 1289 |
if (rn) { e.stopPropagation(); renameOrder(rn.getAttribute('data-code')); return; }
|
| 1290 |
var cp = e.target.closest('.voi-copy');
|
src/order-sync.js
CHANGED
|
@@ -141,10 +141,14 @@
|
|
| 141 |
var serverOrders = (d && d.data) || [];
|
| 142 |
if (!serverOrders.length) return;
|
| 143 |
var local = _lsr(ORDER_KEY, []);
|
|
|
|
|
|
|
|
|
|
| 144 |
var changed = false;
|
| 145 |
serverOrders.forEach(function (so) {
|
| 146 |
var code = so.ma_don || so.code || '';
|
| 147 |
if (!code) return;
|
|
|
|
| 148 |
var oId = { cid: so.cid || so.zalo_id || '', ma_kh: so.ma_kh || '' };
|
| 149 |
if (!orderBelongsToMe(oId, true)) return;
|
| 150 |
var existingIdx = local.findIndex(function (o) { return String(o.code) === String(code); });
|
|
@@ -165,6 +169,13 @@
|
|
| 165 |
try { var orig = window[name]; if (typeof orig !== 'function') { window[name] = wrapper(function () {}); return; } if (orig.__synced) return; window[name] = function () { var ret = orig.apply(this, arguments); wrapper.apply(this, arguments); return ret; }; window[name].__synced = true; } catch (e) {}
|
| 166 |
}
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
function initHooks() {
|
| 169 |
hook('saveOrder', function () { var orders = _lsr(ORDER_KEY, []); if (orders.length) { syncOrderToServer(orders[0]); setTimeout(syncFromServer, 800); } });
|
| 170 |
hook('saveOrders', function (list) { var arr = list || []; if (arr.length) syncOrderToServer(arr[0]); setTimeout(syncFromServer, 800); });
|
|
@@ -181,8 +192,9 @@
|
|
| 181 |
initHooks();
|
| 182 |
wireOpenRefresh();
|
| 183 |
setTimeout(syncFromServer, 1500);
|
| 184 |
-
window.vaiOrdersSync = { syncFromServer: syncFromServer, syncOrderToServer: syncOrderToServer, toServerPayload: toServerPayload };
|
| 185 |
window.vaiOrderToServerPayload = toServerPayload;
|
|
|
|
| 186 |
window.__vaiOrderToServerPayload = toServerPayload;
|
| 187 |
}
|
| 188 |
|
|
|
|
| 141 |
var serverOrders = (d && d.data) || [];
|
| 142 |
if (!serverOrders.length) return;
|
| 143 |
var local = _lsr(ORDER_KEY, []);
|
| 144 |
+
// Tombstone: các đơn người dùng đã XÓA VĨNH VIỄN — không được merge lại.
|
| 145 |
+
var deleted = [];
|
| 146 |
+
try { deleted = JSON.parse(localStorage.getItem('vaix_deleted_orders') || '[]'); } catch (e) { deleted = []; }
|
| 147 |
var changed = false;
|
| 148 |
serverOrders.forEach(function (so) {
|
| 149 |
var code = so.ma_don || so.code || '';
|
| 150 |
if (!code) return;
|
| 151 |
+
if (deleted.indexOf(String(code)) !== -1) return; // đã xóa vĩnh viễn — bỏ qua
|
| 152 |
var oId = { cid: so.cid || so.zalo_id || '', ma_kh: so.ma_kh || '' };
|
| 153 |
if (!orderBelongsToMe(oId, true)) return;
|
| 154 |
var existingIdx = local.findIndex(function (o) { return String(o.code) === String(code); });
|
|
|
|
| 169 |
try { var orig = window[name]; if (typeof orig !== 'function') { window[name] = wrapper(function () {}); return; } if (orig.__synced) return; window[name] = function () { var ret = orig.apply(this, arguments); wrapper.apply(this, arguments); return ret; }; window[name].__synced = true; } catch (e) {}
|
| 170 |
}
|
| 171 |
|
| 172 |
+
// Xóa đơn trên server V.AISTUDIO (ketoan) để không còn tồn tại ở nguồn.
|
| 173 |
+
function deleteRemoteOrder(code) {
|
| 174 |
+
if (!code) return;
|
| 175 |
+
try {
|
| 176 |
+
fetch(KETOAN_API + '/orders/' + encodeURIComponent(code), { method: 'DELETE', signal: AbortSignal.timeout(8000) }).catch(function () {});
|
| 177 |
+
} catch (e) {}
|
| 178 |
+
}
|
| 179 |
function initHooks() {
|
| 180 |
hook('saveOrder', function () { var orders = _lsr(ORDER_KEY, []); if (orders.length) { syncOrderToServer(orders[0]); setTimeout(syncFromServer, 800); } });
|
| 181 |
hook('saveOrders', function (list) { var arr = list || []; if (arr.length) syncOrderToServer(arr[0]); setTimeout(syncFromServer, 800); });
|
|
|
|
| 192 |
initHooks();
|
| 193 |
wireOpenRefresh();
|
| 194 |
setTimeout(syncFromServer, 1500);
|
| 195 |
+
window.vaiOrdersSync = { syncFromServer: syncFromServer, syncOrderToServer: syncOrderToServer, toServerPayload: toServerPayload, deleteRemoteOrder: deleteRemoteOrder };
|
| 196 |
window.vaiOrderToServerPayload = toServerPayload;
|
| 197 |
+
window.vaiOrderDeleteRemote = deleteRemoteOrder;
|
| 198 |
window.__vaiOrderToServerPayload = toServerPayload;
|
| 199 |
}
|
| 200 |
|