bep40 commited on
Commit
e9865a5
·
1 Parent(s): eba4f63

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
Files changed (2) hide show
  1. src/cart-quote.js +72 -6
  2. 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:#fee2e2;border-bottom:1px solid #fecaca;align-items:center;gap:8px;flex-wrap:wrap">'
1069
- + '<b style="font-size:12px;color:#dc2626">🗑 Xóa hàng loạt:</b>'
1070
- + '<span style="font-size:12px;color:#b91c1c">đã chọn <b id="vai-ord-bulk-count">0</b> đơn</span>'
1071
- + '<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>'
 
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 && e.target.tagName === 'input') { e.stopPropagation(); return; }
 
 
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