Commit fff9b2a6 authored by wenwen.tang's avatar wenwen.tang 😕

feat(web): 核对态可直接改数据并随「确认核对」一并存盘;录入加临时保存

核对:
- 核对模式下表格恢复可编辑,改数据不再影响核对状态(勾选仍由人工决定)
- 删除「保存修改」按钮与 saveVerifyEdits();改为 confirmVerify() 先 saveVerifyData()
  按批次分组逐张 PUT,再 POST /api/periods/verify,按钮文案「确认核对(存 N 处改动)」
- 脏基线由数组下标改为稳定行号 _rid(ekey/assignRowIds),修掉删行导致的基线漂移
- 退出核对若有未保存改动,提示「退出就丢了」

录入:
- 新增「临时保存」按钮:saveElecDraft/clearElecDraft/restoreElecDraft/elecBootFill
- 有草稿时不再自动带入上期(以草稿为准),生成成功后自动清除草稿
- td 取值由 children[N] 改为按 data-col 定位

文案:admin.html 同步说明临时保存与「改数据不影响核对状态、确认时一并保存」
parent 9f0dd9ea
......@@ -138,10 +138,13 @@
维护的名册里选,留空则按台账取值。
<br>下面的<b>楼层总表(3F / 6F / 7F)</b>与本期电费一并录入,按「公共 / 套内 / 空调总」分行,
<b>只记账:不出通知书、不发送</b>,随批次留档并可导出。
<br>录入过程中可以随时点「<b>临时保存</b>」把当前明细存到服务器(含还没抄完的行),
下次进本页会<b>自动恢复</b>、直接接着编辑;点「生成通知书」后草稿自动清除。
<br>已生成的批次在下面「批次台账」里<b>按账期(月)汇总</b>,点汇总行可展开该账期的批次;
<b>核对</b>与<b>重新编辑</b>统一在<b>批次详情</b>里进行。
核对按<b>账期</b>统一做,可逐行勾选(照明 / 空调分别核对,也允许只核一部分),
<b>确认核对后才能发送账单</b>。
核对时<b>表里的数据可以直接改</b>(改数据不影响核对状态),
点「确认核对」会把改动<b>一并保存</b>;<b>确认核对后才能发送账单</b>。
</div>
<div class="row">
......@@ -156,6 +159,8 @@
<div style="flex:0 0 auto"><label>&nbsp;</label><button id="btn-e-row">+ 添加一行</button></div>
<div style="flex:0 0 auto"><label>&nbsp;</label><button id="btn-e-copy" title="重新从最近一期批次带入上期抄表数等信息">重新带入上期</button></div>
<div style="flex:0 0 auto"><label>&nbsp;</label><button id="btn-e-clear">清空</button></div>
<div style="flex:0 0 auto" id="eDraftWrap"><label>&nbsp;</label><button id="btn-e-draft"
title="把当前录入(含还没抄完的行)存到服务器,下次进电费页自动恢复,可直接接着编辑">临时保存</button></div>
<div style="flex:0 0 auto"><label>&nbsp;</label><button class="primary" id="btn-e-submit">生成通知书</button></div>
<div style="flex:0 0 auto" class="muted" id="eSum"></div>
</div>
......
......@@ -166,7 +166,7 @@ function applyNav(hash) {
closeDrawer();
if (item.page === 'overview') loadOverview();
if (item.page === 'elec') { loadBatches('elec'); autoFillFromPrev(); }
if (item.page === 'elec') { loadBatches('elec'); elecBootFill(); }
if (item.page === 'settings') loadSettings();
if (item.page === 'companies') loadDirectory();
if (item.page === 'table') {
......@@ -337,12 +337,14 @@ let ELEC = []; // 待提交的明细行
let EDEF = { price: 1.2, ac: 12.25, room: 1.0 };
/* 录入页的三种模式(2026-10-13):
new 新建一期(默认)
new 新建一期(默认)—— 可随时「临时保存」草稿,下次进页面自动恢复
reingest 重新编辑某个已存在的电费批次 —— 保存后覆盖原批次
verify **按账期核对** —— 该账期下所有批次的明细一起列出(只读),逐室勾选 ✔,
确认后该账期每张批次都变成「已核对」,才能发送
verify **按账期核对** —— 该账期下所有批次的明细一起列出,逐**行**勾选 ✔
(「单元|类目」;照明 / 空调可分别核对,也允许只核一部分)。
表里数据**可直接改**,改数据不影响核对状态;
点「确认核对」时先存改动、再整期写为「已核对」,之后才能发送。
EMODE 是当前模式,EBATCH 是重编目标批次,EBATCHES 是核对账期里的全部批次,
EPERIOD 是核对中的账期名,EVERIFY 是勾过的「室」。 */
EPERIOD 是核对中的账期名,EVERIFY 是勾过的「行」(单元|类目)。 */
let EMODE = 'new';
let EBATCH = null;
let EBATCHES = [];
......@@ -592,7 +594,8 @@ function rowChecked(k) { return EVERIFY.has(String(k || '').trim()); }
function renderElecRows() {
const tb = $('#eBody');
const verify = EMODE === 'verify';
const dis = verify ? ' disabled' : '';
// 核对模式下**允许直接改数据**:改哪行就重新核哪行,不用退出核对重来
const dis = '';
if (!ELEC.length) {
tb.innerHTML = '<tr><td colspan="13" class="empty">还没有明细行,点「+ 添加一行」,或等待自动带入上一期</td></tr>';
updateElecSum();
......@@ -626,11 +629,11 @@ function renderElecRows() {
<td data-col="owner"><input list="eOwners" data-i="${i}" data-k="owner" value="${esc(r.owner || '')}" placeholder="留空取台账"${dis}></td>
<td class="e-note" data-col="note">
<input data-i="${i}" data-k="note" value="${esc(r.note)}" placeholder="备注"${dis}>
${verify ? (r.images || []).length ? imgStripHtml(r.images) : ''
: imgStripHtml(r.images, i)}
${(r.images || []).length ? imgStripHtml(r.images) : ''}
${!verify ? imgStripHtml(r.images, i) : ''}
</td>
<td class="num mono-sm" data-col="amt">${c.amount === null ? '—' : '¥' + fmt(c.amount)}</td>
<td class="act" data-col="act">${verify ? '' : `<button class="sm ghost" data-delrow="${i}" title="删除本行">×</button>`}</td>
<td class="act" data-col="act"><button class="sm ghost" data-delrow="${i}" title="删除本行">×</button></td>
<td class="vcol" data-col="vcol">${verify && u
? `<label class="chk vchk" title="确认这一行的抄表数据正确(照明、空调可分别核对)">
<input type="checkbox" data-vrow="${esc(rk)}"${rowChecked(rk) ? ' checked' : ''}> ✔</label>`
......@@ -820,12 +823,15 @@ function bindElecInputs() {
renderElecRows(); return;
}
}
// 按 data-col 定位,不写死第 N 列(核对模式下的行带 data-col,位置会变)
const tr = inp.closest('tr');
const c = calcRow(ELEC[i]);
tr.children[4].innerHTML = c.pending ? '<span class="tag warn">未抄表</span>'
const q = tr.querySelector('[data-col="qty"]'), a = tr.querySelector('[data-col="amt"]');
if (q) q.innerHTML = c.pending ? '<span class="tag warn">未抄表</span>'
: (c.qty === null ? '—' : fmt0(c.qty));
tr.children[10].textContent = c.amount === null ? '—' : '¥' + fmt(c.amount);
if (a) a.textContent = c.amount === null ? '—' : '¥' + fmt(c.amount);
updateElecSum();
if (EMODE === 'verify') markVerifyDirty(); // 刷新「N 处未保存」提示
});
inp.addEventListener('blur', () => {
const i = +inp.dataset.i, k = inp.dataset.k;
......@@ -839,8 +845,16 @@ function bindElecInputs() {
});
});
$$('#eBody [data-delrow]').forEach(b => b.addEventListener('click', () => {
ELEC.splice(+b.dataset.delrow, 1);
const i = +b.dataset.delrow;
const r = ELEC[i];
if (EMODE === 'verify' && r) {
const nm = `${unitKey(r.unit) || '(空行)'} ${String(r.meter || '照明')}`;
if (!confirm(`从核对表里删掉这一行?\n${nm}\n保存后该行所在批次会按新明细重出通知书。`)) return;
}
ELEC.splice(i, 1);
ESNAP.delete(ekey(r)); // 只删这一行的基线,其余行按 _rid 精确对应
renderElecRows();
markVerifyDirty();
}));
$$('#eBody [data-imgadd]').forEach(b => b.addEventListener('click', e => {
e.preventDefault();
......@@ -1074,6 +1088,7 @@ function renderEModeBar() {
if (EMODE === 'new') {
box.innerHTML = '';
$('#eActions').style.display = '';
const dw = $('#eDraftWrap'); if (dw) dw.style.display = ''; // 临时保存只在录入模式
if ($('#btn-e-submit')) $('#btn-e-submit').textContent = E_MODE_TITLE.new;
return;
}
......@@ -1086,12 +1101,14 @@ function renderEModeBar() {
<button class="sm" id="e-mode-cancel">取消重编</button>
</div>`;
$('#eActions').style.display = '';
const dw2 = $('#eDraftWrap'); if (dw2) dw2.style.display = 'none'; // 重编的是已有批次,不存草稿
if ($('#btn-e-submit')) $('#btn-e-submit').textContent = E_MODE_TITLE.reingest;
} else {
const rows = verifyRows(), done = rows.filter(rowChecked).length;
const all = rows.length && done >= rows.length;
const canConfirm = done > 0; // 部分核对也允许确认(至少勾一行)
const bs = EBATCHES || [];
const ed = editSummary();
box.innerHTML = `<div class="banner${all ? ' ok' : ''}">
<b>账期核对中</b>:<b>${esc(EPERIOD || '—')}</b> | ${bs.length} 个批次
<span class="mono-sm">${bs.map(x => esc(x.batch_id)).join('、')}</span>
......@@ -1100,15 +1117,20 @@ function renderEModeBar() {
: canConfirm ? '<span class="tag wait">部分核对</span>'
: '<span class="tag wait">待核对</span>'}
<br>核对列<b>按行勾选</b>:可以只核一部分房间,也可以只核对照明或空调;
<b>同一房间在多个批次里共用一次核对</b>。数据只读 —— 要改数据请先「退出核对」,
到批次详情里点「重新编辑」。<b>确认核对后该账期每张批次都能发送账单。</b>
<b>同一房间在多个批次里共用一次核对</b>。
<br>表里的数据<b>可以直接改</b>,改数据<b>不影响核对状态</b>(勾了哪些行就算哪些行);
点「确认核对」时会把改动<b>一并保存</b>并重出单据。改完不确认就退出,改动会丢失。
<b>确认核对后该账期每张批次都能发送账单。</b>
<span id="e-vf-dirty"${ed.any ? '' : ' style="display:none"'}>${ed.any ? dirtyTip(ed) : ''}</span>
<div class="spacer"></div>
<div class="row" style="align-items:center;gap:8px;flex-wrap:wrap">
<div style="flex:0 0 auto">${vcolsMenuHtml()}</div>
<div style="flex:0 0 auto">
<button class="sm" id="e-vf-add" title="在这一账期里补录一行(保存后归到最后一张批次)">+ 添加一行</button>
<button class="sm" id="e-vf-all">全部核对</button>
<button class="sm" id="e-vf-none">全部取消</button>
<button class="sm primary" id="e-vf-done"${canConfirm ? '' : ' disabled'}>确认核对</button>
<button class="sm primary" id="e-vf-done"${canConfirm ? '' : ' disabled'}
title="先保存表里的改动(若有),再把这个账期的勾选写为「已核对」">${doneLabel(ed)}</button>
<button class="sm" id="e-mode-cancel">退出核对</button>
</div>
</div>
......@@ -1126,6 +1148,13 @@ function renderEModeBar() {
if (n2) n2.addEventListener('click', () => {
EVERIFY.clear(); renderElecRows(); saveVerifyProgress();
});
const add = $('#e-vf-add');
if (add) add.addEventListener('click', () => {
// 新行归到本账期的**最后一张批次**(保存时随它一起重编)
const last = (EBATCHES || [])[(EBATCHES || []).length - 1];
elecAddRow({ _bid: last ? last.batch_id : '', _autoCompany: true, _autoOwner: true });
const tb = $('#eWrap'); if (tb) tb.scrollTop = tb.scrollHeight;
});
const dn = $('#e-vf-done');
if (dn) dn.addEventListener('click', confirmVerify);
}
......@@ -1135,8 +1164,12 @@ function cancelElecMode(silent) {
const was = EMODE;
if (was === 'new') return;
if (!silent) {
const msg = was === 'verify' ? '退出核对?已勾选的核对进度会保存。'
let msg = was === 'verify' ? '退出核对?已勾选的核对进度会保存。'
: '取消重新编辑?未保存的改动会丢失。';
if (was === 'verify' && editSummary().any) {
msg = '退出核对?\n\n核对里改过的数据**还没保存**,退出就丢了(只在点「确认核对」时才会存盘)。\n'
+ '已勾选的核对进度会保留。\n\n确定退出?';
}
if (!confirm(msg)) return;
}
if (was === 'verify') saveVerifyProgress(true); // 进度落盘
......@@ -1166,7 +1199,8 @@ async function enterReingest(id) {
} catch (e) { toast(e.message, 'err'); }
}
/** 进入「账期核对」:把该账期**所有电费批次**的明细一起载入,数据只读,逐室勾选 */
/** 进入「账期核对」:把该账期**所有电费批次**的明细一起载入,逐行勾选(数据可直接改,
* 改动只在点「确认核对」时一并存盘) */
async function enterPeriodVerify(period) {
const p = String(period || '').trim();
if (!p) return toast('该批次没有账期信息,无法核对', 'err');
......@@ -1188,6 +1222,7 @@ async function enterPeriodVerify(period) {
setVColsOpen(false); // 每次进核对都是收起的
EVERIFY = new Set((ps.checked || []).map(String)); // 已核对的「行」(单元|类目)
PREV_DONE = true;
takeVerifySnapshot(); // 改动的「原始值」基线
renderElecRows();
navTo('elec');
$('#elecCard').scrollIntoView({ behavior: 'smooth', block: 'start' });
......@@ -1224,14 +1259,218 @@ function fillElecFromBatch(b) {
$('#in-due').value = (b.due_date || '').slice(0, 10);
}
/* ============ 核对中就地改数据(保存修改) ============ */
/** 一行当前内容 vs 载入时内容(字段名与 detailToRow 对应) */
const ELEC_CMP_FIELDS = ['unit', 'meter', 'prev', 'cur', 'qty', 'price',
'factor', 'company', 'owner', 'note'];
let ESNAP = new Map(); // 行标识 → 载入时的内容
let ERID_SEQ = 0;
/** 给每行一个**稳定标识**:删行 / 改房间号都不会让其它行的基线错位 */
function assignRowIds() {
ELEC.forEach((r, i) => { if (!r._rid) { ERID_SEQ += 1; r._rid = 'R' + ERID_SEQ + '_' + i; } });
}
function ekey(r) { return r._rid || (r._bid || '') + '@' + (r.unit || '') + '|' + (r.meter || ''); }
/** 记下当前表格内容作为「原始值」基线(进入核对 / 保存成功后调用) */
function takeVerifySnapshot() {
assignRowIds();
ESNAP = new Map();
ELEC.forEach(r => ESNAP.set(ekey(r), ELEC_CMP_FIELDS.map(f => r[f])));
}
/** 与基线有差异的行**对象**(新增的行也算:它没进过基线,但确实要存) */
function dirtyRows() {
if (EMODE !== 'verify') return [];
const out = [];
ELEC.forEach(r => {
const base = ESNAP.get(ekey(r));
if (!base) {
// 新增的行:有内容才算改动(空行不计)
if (String(r.unit || '').trim() || String(r.prev || '') || String(r.cur || '')) out.push(r);
return;
}
const now = ELEC_CMP_FIELDS.map(f => r[f]);
if (base.some((v, k) => String(v == null ? '' : v) !== String(now[k] == null ? '' : now[k]))) {
out.push(r);
}
});
return out;
}
/** 改动概览:{ n: 改了几行, structural: 有没有增删行, any: 要不要存盘 } */
function editSummary() {
if (EMODE !== 'verify') return { n: 0, structural: false, any: false };
const n = dirtyRows().length;
// 基线一行一条(键是 _rid);行数对不上就说明增删过行
const structural = ELEC.length !== ESNAP.size;
return { n, structural, any: n > 0 || structural };
}
/** 「有改动」提示文案 */
function dirtyTip(ed) {
const bits = [];
if (ed.n) bits.push(`${ed.n} 行改过内容`);
if (ed.structural) bits.push('有行被增删');
return `<br><span style="color:#92400e"><b>待保存</b>:${esc(bits.join('、'))}
—— 点「确认核对」时一并存入,改数据不影响核对状态。</span>`;
}
/** 「确认核对」按钮文案:带不带改动都要说清楚 */
function doneLabel(ed) {
return ed.any ? `确认核对(存 ${ed.n || '增删'} 处改动)` : '确认核对';
}
/** 兼容旧名(「有 N 处改动未保存」的行名列表) */
function verifyDirty() {
return dirtyRows().map(r => `${unitKey(r.unit) || '(空行)'} ${String(r.meter || '照明')}`);
}
let DIRTY_T = null;
/** 刷新「待保存」提示与确认按钮文案 —— **只改文字,不重渲染模式条**(否则输入框失焦) */
function markVerifyDirty() {
clearTimeout(DIRTY_T);
DIRTY_T = setTimeout(() => {
const box = $('#eModeBar');
if (!box || EMODE !== 'verify') return;
const ed = editSummary();
const btn = $('#e-vf-done');
if (btn) btn.textContent = doneLabel(ed);
const tip = $('#e-vf-dirty');
if (!tip) return;
tip.style.display = ed.any ? '' : 'none';
tip.innerHTML = ed.any ? dirtyTip(ed) : '';
}, 300);
}
/** 把一批录入行转成提交体(与「生成通知书」同一套字段) */
function elecRowsPayload(list) {
return list.map(r => {
const o = {};
ELEC_ROW_FIELDS.forEach(f => { o[f] = r[f]; });
o.images = r.images || [];
return o;
});
}
/** 核对中改的数据:按批次整张重编(**PUT 是全量覆盖**,所以每张批次都要带上它的全部行)。
*
* 只在「确有改动」时才发请求:没改动就一个包都不发,不白白重出单据。
* ⚠️ 重编会把该批次的服务端核对状态清空 —— 所以调用方必须在**紧接着**的
* 账期级确认里把勾选写回去(见 confirmVerify),这样「改数据」才不影响核对状态。
*/
async function saveVerifyData() {
if (EMODE !== 'verify' || !EPERIOD) return 0;
const ed = editSummary();
if (!ed.any) return 0;
const lastBid = ((EBATCHES || [])[(EBATCHES || []).length - 1] || {}).batch_id || '';
const groups = new Map();
ELEC.forEach(r => {
const b = r._bid || lastBid; // 理论上每行都带 _bid;兜底归到最后一张批次
if (!b) return;
if (!groups.has(b)) groups.set(b, []);
groups.get(b).push(r);
});
let n = 0;
for (const [bid, list] of groups) {
if (!list.some(r => String(r.unit || '').trim())) continue; // 整组空行 → 跳过
await api('/api/batches/' + bid, {
method: 'PUT',
body: {
rows: elecRowsPayload(list),
period_label: $('#in-period').value.trim() || EPERIOD,
meter_date: $('#in-meter').value,
notice_date: $('#in-notice').value,
due_date: $('#in-due').value,
},
});
n += 1;
}
return n;
}
$('#btn-e-row').addEventListener('click', () => elecAddRow());
const _draftBtn = $('#btn-e-draft');
if (_draftBtn) _draftBtn.addEventListener('click', () => {
if (EMODE !== 'new') return toast('重编 / 核对中不需要临时保存', 'err');
saveElecDraft();
});
$('#btn-e-clear').addEventListener('click', () => {
if (ELEC.length && !confirm('清空当前录入的全部明细行?')) return;
if (ELEC.length && !confirm('清空当前录入的全部明细行?(已「临时保存」的草稿也会一并清掉)')) return;
ELEC = []; renderElecRows();
clearElecDraft(); // 清空即视为放弃草稿
});
/* ---------------- 临时保存:录入草稿落盘(按账号一份,下次自动恢复) ---------------- */
/** 把当前录入(含没抄完的行)整份存到服务端 */
async function saveElecDraft() {
const btn = $('#btn-e-draft');
const rows = elecRowsPayload(ELEC);
if (!rows.length) return toast('还没有可保存的录入行', 'err');
if (btn) { btn.disabled = true; btn.textContent = '保存中…'; }
try {
const d = await api('/api/drafts/elec', {
method: 'PUT',
body: {
rows,
period_label: $('#in-period').value.trim(),
meter_date: $('#in-meter').value,
notice_date: $('#in-notice').value,
due_date: $('#in-due').value,
},
});
toast(`已临时保存 ${rows.length} 行(${(d.draft || {}).updated_at || ''}),下次打开本页会自动恢复`, 'ok');
} catch (e) { toast(e.message, 'err'); }
finally { if (btn) { btn.disabled = false; btn.textContent = '临时保存'; } }
}
/** 清掉草稿(生成成功 / 清空录入 / 手动丢弃时调用;失败不打扰用户) */
async function clearElecDraft() {
try { await api('/api/drafts/elec', { method: 'DELETE' }); } catch (e) { /* 忽略 */ }
}
/** 进电费页时恢复草稿。返回 true 表示已恢复(此时不再自动「带入上期」)。 */
async function restoreElecDraft() {
if (EMODE !== 'new' || ELEC.length) return false;
try {
const d = await api('/api/drafts/elec');
const dr = d.draft;
if (!dr || !(dr.rows || []).length) return false;
ELEC = dr.rows.map(x => detailToRow(x));
if (dr.period_label) $('#in-period').value = dr.period_label;
if (dr.meter_date) $('#in-meter').value = String(dr.meter_date).slice(0, 10);
if (dr.notice_date) $('#in-notice').value = String(dr.notice_date).slice(0, 10);
if (dr.due_date) $('#in-due').value = String(dr.due_date).slice(0, 10);
PREV_DONE = true; // 有草稿就不自动带上一期,免得覆盖
renderElecRows();
$('#uploadResult').innerHTML = `<div class="banner">
已恢复 <b>${esc(dr.updated_at || '上次')}</b> 临时保存的录入:<b>${ELEC.length}</b> 行
<span class="muted">(${esc(dr.updated_by || '')})</span> —— 可直接接着编辑,
点「生成通知书」后这份草稿会自动清除。
<div class="spacer"></div>
<button class="sm" id="e-draft-drop">丢弃这份草稿</button>
</div>`;
const drop = $('#e-draft-drop');
if (drop) drop.addEventListener('click', async () => {
if (!confirm('丢弃临时保存的草稿并清空当前录入?')) return;
await clearElecDraft();
ELEC = []; PREV_DONE = false; $('#uploadResult').innerHTML = '';
renderElecRows(); toast('草稿已丢弃', 'ok');
});
return true;
} catch (e) { return false; }
}
/** 进电费页:优先恢复草稿,没有草稿才自动带入上一期 */
async function elecBootFill() {
if (await restoreElecDraft()) return;
autoFillFromPrev();
}
/* ---------------- 自动带入上一期的「已知信息」 ---------------- */
let PREV_DONE = false; // 本轮进入页面是否已自动带入过
......@@ -1402,12 +1641,13 @@ $('#btn-e-submit').addEventListener('click', async () => {
通知书 <b>${b.documents.length}</b> 份,合计 <b>¥${fmt(b.totals.total_amount)}</b>${imgs ? `,图片附件 <b>${imgs}</b> 张` : ''}。
${mt ? `<br>楼层总表已存 <b>${mt}</b> 行,合计 <b>¥${fmt(b.totals.master_amount)}</b>
<span class="muted">(只记账:不出通知书、不发送,可在批次详情与导出里查看)</span>` : ''}
${editing ? '<br><span style="color:#92400e">核对状态已重置:请在批次台账点「核对」逐室确认后再发送。</span>' : ''}
${editing ? '<br><span style="color:#92400e">核对状态已重置:请在批次台账里点「核对」,逐行确认后再发送。</span>' : ''}
${pend ? `<br><span style="color:#92400e">其中 ${pend} 行「本期抄表数」为空,已按未抄表处理、未参与出单。</span>` : ''}
${parks ? `<br><span style="color:#92400e">已忽略 ${parks} 行车位(电费只算房间):
${esc((b.skipped_park || []).slice(0, 6).join('、'))}${parks > 6 ? '…' : ''}</span>` : ''}
</div>`;
toast(`已${editing ? '重编' : '生成'} ${b.documents.length} 份通知书`, 'ok');
if (!editing) await clearElecDraft(); // 落成批次了,草稿使命结束
EMODE = 'new'; EBATCH = null; EVERIFY = new Set(); ELEC = [];
clearMasters();
renderElecRows(); renderEModeBar();
......@@ -1419,7 +1659,12 @@ $('#btn-e-submit').addEventListener('click', async () => {
} finally { btn.disabled = false; btn.textContent = label || '生成通知书'; }
});
/** 核对模式:确认核对(账期级)—— 勾了任意一行就能确认,支持部分核对 */
/** 核对模式:确认核对(账期级)—— 勾了任意一行就能确认,支持部分核对。
*
* 「核对时直接改数据」的落点:确认时**先按批次把改动存盘(重编、重出单据),
* 再把整期的勾选写成「已核对」**。所以改数据不影响核对状态 —— 用户勾了哪些行,
* 最终就是哪些行已核对,跟改没改过这行无关。
*/
async function confirmVerify() {
if (EMODE !== 'verify' || !EPERIOD) return;
const rows = verifyRows(), done = rows.filter(rowChecked).length;
......@@ -1429,6 +1674,7 @@ async function confirmVerify() {
const n = (EBATCHES || []).length;
const miss = rows.filter(k => !rowChecked(k));
const partial = miss.length > 0;
const ed = editSummary();
const rows_txt = `${done}/${rows.length} 行`;
const ok = await confirmBox({
title: partial ? '确认核对(部分核对)' : '确认核对(按账期)',
......@@ -1438,23 +1684,33 @@ async function confirmVerify() {
{ k: '核对范围', v: partial
? `已逐行勾选 <b>${rows_txt}</b>(还有 <b>${miss.length}</b> 行未核对)`
: `该账期全部 <b>${rows_txt}</b> 已逐行勾选确认` },
{ k: '数据改动', v: ed.any
? `<b>一并保存</b>${ed.n ? ` ${ed.n} 处改过的内容` : ''}`
+ `${ed.structural ? `${ed.n ? ',' : ' '}并包含行的增删` : ''}`
+ ';各批次按新明细重出通知书(已发送状态按文件名沿用)'
: '没有改动,只写核对状态' },
{ k: '影响', v: '确认后该账期下每一张批次都变为「已核对」,才允许发送账单' },
],
warn: partial
? `这是<b>部分核对</b>:还有 ${miss.length} 行没核对(${esc(miss.slice(0, 6).join('、'))}`
+ `${miss.length > 6 ? ' …' : ''})。确认后这些行也会按已核对处理、可以发送。`
+ '若之后取消勾选,确认会自动撤销。'
: '确认即表示该账期电费数据已人工核对无误;若之后取消勾选或「重新编辑」改动数据,核对状态会自动重置。',
okText: partial ? '确认(部分核对)' : '确认核对',
: '确认即表示该账期电费数据已人工核对无误;若之后取消勾选,核对状态会自动撤销。',
okText: partial ? '确认并保存(部分核对)' : '确认并保存',
});
if (!ok) return;
const btn = $('#e-vf-done');
const label = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = ed.any ? '保存中…' : '确认中…'; }
try {
const first = (EBATCHES[0] || {}).batch_id;
const d = await api('/api/periods/verify', {
await saveVerifyData(); // ① 改动先落盘(没改动则跳过)
const d = await api('/api/periods/verify', { // ② 再把勾选写为「已核对」
method: 'POST', body: { period: EPERIOD, rows: Array.from(EVERIFY), done: true },
});
applyPeriodSummary(d.period);
toast(`账期 ${EPERIOD} 已完成核对(${(d.batches || []).length} 个批次`
+ (ed.any ? `,存下 ${ed.n || '增删'} 处改动` : '')
+ (partial ? `,部分核对 ${rows_txt}` : '') + '),现在可以发送账单了', 'ok');
EMODE = 'new'; EBATCH = null; EBATCHES = []; EPERIOD = null; EVERIFY = new Set();
ELEC = [];
......@@ -1462,7 +1718,10 @@ async function confirmVerify() {
renderElecRows(); renderEModeBar();
await loadBatches('elec');
if (first) openBatch(first, 'elec');
} catch (e) { toast(e.message, 'err'); }
} catch (e) {
toast(e.message, 'err');
if (btn) { btn.disabled = false; btn.textContent = label || '确认核对'; }
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment