HEL-177: 公司端按方案 A 实现转账往来四层页面

工作台概览小卡 + 侧栏入口;总览/对方汇总/明细/原始流水抽屉接入 HEL-175/176 接口;无期初时展示期间净变动;模块内用 info 品蓝。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-08-27 13:45:19 +00:00
co-authored by Cursor multica-agent
parent 96309eb2cc
commit b27016d71d
7 changed files with 1352 additions and 18 deletions
+565 -2
View File
@@ -4,7 +4,7 @@ const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)]
const portal = document.body.dataset.portal || "entry";
const viewNames = portal === "admin"
? { dashboard: "管理总览", pair: "往来查询", audit: "审核中心", flows: "流水管理", companies: "公司与账号", settings: "结账与期初", reminders: "提醒管理" }
: { workspace: "工作台", upload: "流水导入", manual: "手工记录", flows: "流水管理", reconcile: "往来确认", accounts: "银行账户", notifications: "通知" };
: { workspace: "工作台", upload: "流水导入", manual: "手工记录", flows: "流水管理", transfers: "转账往来", reconcile: "往来确认", accounts: "银行账户", notifications: "通知" };
const storageKeys = {
manual: "ledger-demo-manual-records",
@@ -177,11 +177,27 @@ function showView(view) {
else item.removeAttribute("aria-current");
});
const title = $("#currentViewName");
if (title) title.textContent = viewNames[view];
if (title) {
if (view === "transfers" && state.transfersDetail?.company_name) {
title.textContent = `转账往来 / ${state.transfersDetail.company_name}`;
} else {
title.textContent = viewNames[view];
}
}
closeNavigation({ restoreFocus: navigationWasOpen });
const activeView = $(`.app-view[data-page="${view}"]`);
requestAnimationFrame(() => animateView(activeView));
window.scrollTo({ top: 0, behavior: motionQuery.matches ? "auto" : "smooth" });
if (portal === "company" && view === "transfers") {
if (state.transfersKeepDetail && state.transfersDetail?.company_id) {
showTransfersDetailLayer();
} else {
state.transfersDetail = null;
showTransfersOverviewLayer();
loadTransfersSummary();
}
}
state.transfersKeepDetail = false;
}
// 原型占位,非本司真待办/真断档数据:detailContent 仅用于演示总览/工作台事项
@@ -2934,6 +2950,550 @@ function openAccountDetail(account) {
$("#modal-account-detail")?.classList.add("open");
}
function yuanToWan(value) {
const n = Number(value);
if (!Number.isFinite(n)) return null;
return n / 10000;
}
function formatWan(value, { signed = false } = {}) {
const wan = yuanToWan(value);
if (wan === null) return "—";
const abs = Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
if (!signed) return `${abs}<span class="unit">万元</span>`;
const sign = wan > 0 ? "+" : wan < 0 ? "" : "";
return `${sign}${abs}<span class="unit">万元</span>`;
}
function formatWanText(value, { signed = false } = {}) {
const wan = yuanToWan(value);
if (wan === null) return "—";
const abs = Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
if (!signed) return `${abs} 万元`;
const sign = wan > 0 ? "+" : wan < 0 ? "" : "";
return `${sign}${abs} 万元`;
}
function netDirectionMeta(netValue, netDirection) {
const wan = yuanToWan(netValue);
if (wan === null || wan === 0 || !netDirection) {
return { label: "持平", className: "flat", signedClass: "" };
}
if (netDirection === "receivable" || wan > 0) {
return { label: "应收", className: "recv", signedClass: "pos" };
}
return { label: "应付", className: "pay", signedClass: "neg" };
}
function netLabelForWindow(windowInfo) {
return windowInfo?.has_opening ? "期末净往来" : "期间净变动";
}
function showTransfersOverviewLayer() {
const overview = $("#transfersOverviewLayer");
const detail = $("#transfersDetailLayer");
if (overview) overview.hidden = false;
if (detail) detail.hidden = true;
}
function showTransfersDetailLayer() {
const overview = $("#transfersOverviewLayer");
const detail = $("#transfersDetailLayer");
if (overview) overview.hidden = true;
if (detail) detail.hidden = false;
}
function setTransfersUiState(mode) {
["transfersLoading", "transfersError", "transfersEmpty", "transfersData"].forEach((id) => {
const el = $(`#${id}`);
if (el) el.hidden = id !== mode;
});
}
function applyTransfersNavBadge(pendingCount) {
const badge = $("#transfersNavBadge") || $('.side-nav a[data-view="transfers"] .nav-badge');
if (!badge) return;
const n = Number(pendingCount) || 0;
badge.textContent = String(n);
badge.hidden = n <= 0;
if (n <= 0) badge.setAttribute("hidden", "");
else badge.removeAttribute("hidden");
}
function renderWorkspaceTransfersCard(summary) {
const sub = $("#workspaceTransfersSub");
if (!sub || !summary) return;
const win = summary.window || {};
const confirmed = summary.confirmed || {};
const pending = summary.pending || {};
const netMeta = netDirectionMeta(confirmed.net_change, confirmed.net_direction);
sub.textContent = `${win.start || "—"}${win.end || "—"} · 集团内公司间 · 单位:万元`;
const inflow = $("#wsTfIn");
const outflow = $("#wsTfOut");
const net = $("#wsTfNet");
const pendingEl = $("#wsTfPending");
const netLabel = $("#wsTfNetLabel");
if (inflow) inflow.innerHTML = formatWan(confirmed.inflow_total, { signed: true });
if (outflow) {
outflow.innerHTML = formatWan(
confirmed.outflow_total != null ? -Math.abs(Number(confirmed.outflow_total)) : null,
{ signed: true },
);
// formatWan with negative value already adds ; ensure unit
if (confirmed.outflow_total != null) {
const wan = yuanToWan(confirmed.outflow_total);
const abs = Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
outflow.innerHTML = `${abs}<span class="unit">万元</span>`;
}
}
if (netLabel) netLabel.textContent = `${netLabelForWindow(win)}${netMeta.label !== "持平" ? ` · ${netMeta.label}` : ""}`;
if (net) {
net.className = `ms-value ${netMeta.signedClass}`.trim();
net.innerHTML = formatWan(confirmed.net_change, { signed: true });
}
if (pendingEl) {
const pCount = Number(pending.count) || 0;
pendingEl.innerHTML = pCount
? `${formatWan(pending.amount_total)}<span class="unit"> · ${pCount} 笔</span>`
: `0.00<span class="unit">万元 · 0 笔</span>`;
}
}
async function loadTransfersSummary({ asOf } = {}) {
if (!$("#transfersOverviewLayer")) return null;
setTransfersUiState("transfersLoading");
const params = new URLSearchParams();
if (asOf) params.set("as_of", asOf);
const qs = params.toString();
const response = await fetch(`/api/company/intercompany/summary${qs ? `?${qs}` : ""}`).catch(() => null);
if (response?.status === 401 || response?.status === 403) {
window.location.href = "index.html";
return null;
}
const result = await response?.json().catch(() => null);
if (!response?.ok || !result || result.status !== "ok") {
setTransfersUiState("transfersError");
const body = $("#transfersErrorBody");
if (body) body.textContent = result?.message || "服务器连接异常。为避免误读,本页不展示任何金额。";
return null;
}
state.transfersSummary = result;
applyTransfersNavBadge(result.pending?.count);
renderWorkspaceTransfersCard(result);
renderTransfersOverview(result);
return result;
}
function renderTransfersOverview(summary) {
const win = summary.window || {};
const confirmed = summary.confirmed || {};
const pending = summary.pending || {};
const cps = Array.isArray(summary.counterparties) ? summary.counterparties : [];
const ownName = summary.own_company?.name || "本公司";
const pageSub = $("#transfersPageSub");
if (pageSub) {
pageSub.textContent = `${ownName} · 统计区间 ${win.start || "—"}${win.end || "—"},仅含集团内公司间转账(HEL-169 口径)。单位:万元。`;
}
const goConfirm = $("#transfersGoConfirm");
const pCount = Number(pending.count) || 0;
if (goConfirm) {
goConfirm.hidden = pCount <= 0;
goConfirm.textContent = pCount ? `去确认待确认 ${pCount}` : "去确认待确认";
}
const hasAny = cps.length > 0 || Number(confirmed.outflow_count || 0) > 0 || Number(confirmed.inflow_count || 0) > 0 || pCount > 0;
if (!hasAny) {
setTransfersUiState("transfersEmpty");
const emptyStats = $("#transfersEmptyStats");
if (emptyStats) {
emptyStats.innerHTML = `
<div class="card stat-card"><div class="stat-label">往来公司数</div><div class="stat-value">0<span class="unit">家</span></div><div class="stat-foot">${win.start || "—"} ~ ${win.end || "—"}</div></div>
<div class="card stat-card"><div class="stat-label">本期转入 · 流入</div><div class="stat-value">0.00<span class="unit">万元</span></div></div>
<div class="card stat-card"><div class="stat-label">本期转出 · 流出</div><div class="stat-value">0.00<span class="unit">万元</span></div></div>
<div class="card stat-card"><div class="stat-label">${netLabelForWindow(win)}</div><div class="stat-value">0.00<span class="unit">万元</span></div></div>`;
}
return;
}
setTransfersUiState("transfersData");
const companies = $("#tfStatCompanies");
const inflow = $("#tfStatIn");
const outflow = $("#tfStatOut");
const net = $("#tfStatNet");
const netTitle = $("#tfStatNetTitle");
const netFoot = $("#tfStatNetFoot");
const netMeta = netDirectionMeta(confirmed.net_change, confirmed.net_direction);
if (companies) companies.innerHTML = `${cps.length}<span class="unit">家</span>`;
if (inflow) inflow.innerHTML = formatWan(confirmed.inflow_total, { signed: true });
if (outflow) {
const wan = yuanToWan(confirmed.outflow_total);
const abs = wan === null ? "—" : Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
outflow.innerHTML = wan === null ? "—" : `${abs}<span class="unit">万元</span>`;
}
if (netTitle) netTitle.textContent = netLabelForWindow(win);
if (net) {
const tag = netMeta.label !== "持平"
? ` <span class="xfer-dir-tag ${netMeta.className}">${netMeta.label}</span>`
: "";
net.innerHTML = `${formatWan(confirmed.net_change, { signed: true })}${tag}`;
}
if (netFoot) {
netFoot.textContent = win.has_opening
? "期末 = 期初 + 已确认转出 − 已确认转入"
: "起算日未就绪 · 展示期间净变动(转出 − 转入),不得当作期末余额";
}
const confirmedCount = (Number(confirmed.outflow_count) || 0) + (Number(confirmed.inflow_count) || 0);
const tfConfirmedCount = $("#tfConfirmedCount");
const tfConfirmedNet = $("#tfConfirmedNet");
const tfPendingCount = $("#tfPendingCount");
const tfPendingAmount = $("#tfPendingAmount");
if (tfConfirmedCount) tfConfirmedCount.textContent = `${confirmedCount}`;
if (tfConfirmedNet) tfConfirmedNet.innerHTML = formatWan(confirmed.net_change, { signed: true });
if (tfPendingCount) tfPendingCount.textContent = `${pCount}`;
if (tfPendingAmount) tfPendingAmount.innerHTML = formatWan(pending.amount_total || 0);
const tbody = $("#transfersCpBody");
const tfoot = $("#transfersCpFoot");
if (!tbody) return;
if (!cps.length) {
tbody.innerHTML = `<tr><td colspan="7"><div class="empty" style="border:0;padding:24px 0;"><div class="e-title">暂无对方公司汇总</div></div></td></tr>`;
if (tfoot) tfoot.innerHTML = "";
return;
}
tbody.innerHTML = cps.map((cp) => {
const meta = netDirectionMeta(cp.net, Number(cp.net) > 0 ? "receivable" : Number(cp.net) < 0 ? "payable" : null);
const pendingN = Number(cp.pending_count) || 0;
const pendingPill = pendingN
? `<span class="pill pill-warn">${pendingN} 笔待确认</span>`
: `<span class="pill pill-success">全部已确认</span>`;
const last = cp.last_effective_at ? String(cp.last_effective_at).slice(0, 10) : "—";
const inWan = yuanToWan(cp.confirmed_inflow);
const outWan = yuanToWan(cp.confirmed_outflow);
const netWan = yuanToWan(cp.net);
const fmtAbs = (wan) => wan === null ? "—" : Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const inText = inWan === null ? "—" : `+${fmtAbs(inWan)}`;
const outText = outWan === null ? "—" : `${fmtAbs(outWan)}`;
const netText = netWan === null ? "—" : `${netWan > 0 ? "+" : netWan < 0 ? "" : ""}${fmtAbs(netWan)}`;
return `<tr class="clickable" data-cp-id="${cp.company_id}" data-cp-name="${String(cp.company_name || "").replace(/"/g, "&quot;")}">
<td class="cell-main">${cp.company_name || "—"}</td>
<td class="num-col amt-in">${inText}</td>
<td class="num-col amt-out">${outText}</td>
<td class="num-col">${netText}</td>
<td><span class="xfer-dir-tag ${meta.className}">${meta.label}</span></td>
<td>${pendingPill}</td>
<td class="num">${last}</td>
</tr>`;
}).join("");
if (tfoot) {
const totalPending = cps.reduce((s, cp) => s + (Number(cp.pending_count) || 0), 0);
const inWan = yuanToWan(confirmed.inflow_total);
const outWan = yuanToWan(confirmed.outflow_total);
const netWan = yuanToWan(confirmed.net_change);
const fmtAbs = (wan) => wan === null ? "—" : Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
tfoot.innerHTML = `<tr>
<td>合计</td>
<td class="num-col amt-in">${inWan === null ? "—" : `+${fmtAbs(inWan)}`}</td>
<td class="num-col amt-out">${outWan === null ? "—" : `${fmtAbs(outWan)}`}</td>
<td class="num-col">${netWan === null ? "—" : `${netWan > 0 ? "+" : netWan < 0 ? "" : ""}${fmtAbs(netWan)}`}</td>
<td><span class="xfer-dir-tag ${netMeta.className}">${netMeta.label}</span></td>
<td>${totalPending ? `<span class="pill pill-warn">${totalPending} 笔待确认</span>` : `<span class="pill pill-success">全部已确认</span>`}</td>
<td></td>
</tr>`;
}
}
function transfersEventQueryParams({ includeCursor = false } = {}) {
const detail = state.transfersDetail || {};
const filters = detail.filters || {};
const params = new URLSearchParams();
if (detail.company_id) params.set("counterparty_id", String(detail.company_id));
if (filters.from) params.set("from", filters.from);
if (filters.to) params.set("to", filters.to);
if (filters.direction) params.set("direction", filters.direction);
if (filters.state) params.set("state", filters.state);
params.set("limit", "50");
if (includeCursor && detail.nextCursor) params.set("cursor", detail.nextCursor);
return params;
}
async function openTransfersDetail(companyId, companyName, { keepFilters = false } = {}) {
const summary = state.transfersSummary;
const win = summary?.window || {};
const existing = state.transfersDetail;
const filters = keepFilters && existing?.filters
? { ...existing.filters }
: {
from: win.start || "",
to: win.end || "",
direction: "",
state: "",
};
state.transfersDetail = {
company_id: Number(companyId),
company_name: companyName || "对方公司",
filters,
nextCursor: null,
events: [],
};
state.transfersKeepDetail = true;
showTransfersDetailLayer();
const title = $("#currentViewName");
if (title) title.textContent = `转账往来 / ${state.transfersDetail.company_name}`;
$("#transfersDetailTitle").textContent = state.transfersDetail.company_name;
$("#tfFilterFrom").value = filters.from || "";
$("#tfFilterTo").value = filters.to || "";
$("#tfFilterDirection").value = filters.direction || "";
$("#tfFilterState").value = filters.state || "";
const cp = (summary?.counterparties || []).find((c) => Number(c.company_id) === Number(companyId));
const pendingN = Number(cp?.pending_count) || 0;
$("#tfDetailCount").innerHTML = ``;
$("#tfDetailCountFoot").textContent = pendingN ? `其中待确认 ${pendingN} 笔(汇总)` : "按筛选加载明细";
if (cp) {
$("#tfDetailIn").innerHTML = formatWan(cp.confirmed_inflow, { signed: true });
const wan = yuanToWan(cp.confirmed_outflow);
$("#tfDetailOut").innerHTML = wan === null
? "—"
: `${Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}<span class="unit">万元</span>`;
}
if (state.currentView !== "transfers") {
state.transfersKeepDetail = true;
showView("transfers");
}
await loadTransfersEvents({ reset: true });
}
async function loadTransfersEvents({ reset = false } = {}) {
if (!state.transfersDetail?.company_id) return;
const tbody = $("#transfersEventBody");
const empty = $("#transfersEventEmpty");
const foot = $("#transfersEventFoot");
const moreBtn = $("#transfersLoadMore");
if (reset) {
state.transfersDetail.nextCursor = null;
state.transfersDetail.events = [];
if (tbody) showTableLoading(tbody, 6);
if (empty) empty.hidden = true;
if (moreBtn) moreBtn.hidden = true;
}
const params = transfersEventQueryParams({ includeCursor: !reset && !!state.transfersDetail.nextCursor });
// Always need a distinguishing filter so HEL-176 path is used (counterparty_id is enough)
const response = await fetch(`/api/company/intercompany/events?${params}`).catch(() => null);
if (response?.status === 401 || response?.status === 403) {
window.location.href = "index.html";
return;
}
const result = await response?.json().catch(() => null);
if (!response?.ok || !result || result.status !== "ok") {
if (tbody) showTableError(tbody, 6);
showToast("明细加载失败", result?.message || "请稍后重试", "danger");
return;
}
const events = Array.isArray(result.events) ? result.events : [];
if (reset) state.transfersDetail.events = events;
else state.transfersDetail.events = [...(state.transfersDetail.events || []), ...events];
state.transfersDetail.nextCursor = result.next_cursor || null;
state.transfersDetail.hasMore = !!result.has_more;
renderTransfersEvents();
if (foot) foot.textContent = `已加载 ${state.transfersDetail.events.length}${result.has_more ? " · 还有更多" : ""}`;
if (moreBtn) moreBtn.hidden = !result.has_more;
$("#tfDetailCount").innerHTML = `${state.transfersDetail.events.length}${result.has_more ? "+" : ""}<span class="unit">笔</span>`;
}
function transfersStateLabel(event) {
if (event.state === "confirmed") {
if (event.pairing === "paired") return { pill: "pill-success", text: "已确认 · 双边" };
if (event.locked) return { pill: "pill-success", text: "已确认 · 单边锁定" };
return { pill: "pill-success", text: "已确认" };
}
return { pill: "pill-warn", text: "待确认" };
}
function renderTransfersEvents() {
const tbody = $("#transfersEventBody");
const empty = $("#transfersEventEmpty");
if (!tbody) return;
const events = state.transfersDetail?.events || [];
if (!events.length) {
tbody.innerHTML = "";
if (empty) empty.hidden = false;
return;
}
if (empty) empty.hidden = true;
tbody.innerHTML = events.map((ev) => {
const dir = ev.direction === "out" ? "转出" : ev.direction === "in" ? "转入" : "—";
const dirClass = ev.direction === "out" ? "amt-out" : ev.direction === "in" ? "amt-in" : "";
const wan = yuanToWan(ev.amount);
const amt = wan === null
? "—"
: `${ev.direction === "out" ? "" : "+"}${Math.abs(wan).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const st = transfersStateLabel(ev);
const date = String(ev.effective_at || "").slice(0, 10) || "—";
const pendingClass = ev.state === "pending" ? " is-pending" : "";
return `<tr class="${pendingClass.trim()}" data-event-id="${ev.event_id}">
<td class="num">${date}</td>
<td class="wrap">${ev.summary || "—"}</td>
<td>${dir}${ev.direction === "in" ? " ↓" : ev.direction === "out" ? " ↑" : ""}</td>
<td class="num-col ${dirClass}">${amt}</td>
<td><span class="pill ${st.pill}">${st.text}</span></td>
<td><button type="button" class="btn btn-sm" data-transfer-evidence="${ev.event_id}">原始流水</button></td>
</tr>`;
}).join("");
}
async function openTransferEvidence(eventId) {
const drawer = $("#transferEvidenceDrawer");
if (!drawer) return;
$("#tfEvTitle").textContent = "加载中…";
$("#tfEvDesc").textContent = "";
$("#tfEvFields").replaceChildren();
drawer.classList.add("is-open");
drawer.setAttribute("aria-hidden", "false");
const response = await fetch(`/api/company/transfer-events/${eventId}`).catch(() => null);
if (response?.status === 401 || response?.status === 403) {
window.location.href = "index.html";
return;
}
const result = await response?.json().catch(() => null);
if (!response?.ok || !result || result.status !== "ok") {
$("#tfEvTitle").textContent = "无法打开原始流水";
$("#tfEvDesc").textContent = result?.message || "事件不存在或无权查看";
return;
}
const event = result.event || {};
const obs = Array.isArray(event.observations) ? event.observations[0] : null;
const cp = event.counterparty || {};
$("#tfEvTag").className = "pill pill-info";
$("#tfEvTag").textContent = "银行原始流水 · 只读";
$("#tfEvTitle").textContent = obs?.reference || `事件 #${event.event_id}`;
$("#tfEvDesc").textContent = obs?.import_batch_id
? `导入批次 IMP-${String(obs.import_batch_id).padStart(6, "0")}${obs.original_filename ? ` · ${obs.original_filename}` : ""}`
: "本方银行流水原文";
const income = obs?.income != null && Number(obs.income) !== 0;
const amountText = obs
? `${Number(income ? obs.income : obs.expense).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} 元(${income ? "收 / 转入" : "付 / 转出"}`
: `${event.amount || "—"} ${event.currency || "CNY"}`;
const fields = [
["本方账户", obs?.own_account_masked || "—"],
["交易时间", obs?.transaction_at || event.effective_at || "—"],
["方向", income ? "收入(转入)" : "支出(转出)"],
["金额", amountText],
["对方户名", obs?.counterparty_name || cp.company_name || "—"],
["对方账号", obs?.counterparty_account_masked || cp.account_number_masked || "—"],
["工作表", obs?.sheet_name || "—"],
["原始文件行号", obs?.source_row != null ? String(obs.source_row) : "—"],
["摘要", obs?.summary || event.reason || "—"],
["确认状态", event.pairing === "paired" ? "已确认 · 双边一致" : event.status || "—"],
];
const dl = $("#tfEvFields");
dl.replaceChildren(...fields.flatMap(([label, value]) => {
const dt = document.createElement("dt");
dt.textContent = label;
const dd = document.createElement("dd");
dd.className = "num";
dd.textContent = value;
return [dt, dd];
}));
$("[data-close-transfer-evidence]", drawer)?.focus();
}
function closeTransferEvidence({ restoreFocus = true } = {}) {
const drawer = $("#transferEvidenceDrawer");
if (!drawer) return;
drawer.classList.remove("is-open");
drawer.setAttribute("aria-hidden", "true");
}
function buildTransfersExportUrl() {
const detail = state.transfersDetail;
const summary = state.transfersSummary;
const params = new URLSearchParams();
if (detail?.company_id) {
params.set("counterparty_id", String(detail.company_id));
const f = detail.filters || {};
if (f.from) params.set("from", f.from);
if (f.to) params.set("to", f.to);
if (f.direction) params.set("direction", f.direction);
} else if (summary?.window) {
if (summary.window.start) params.set("from", summary.window.start);
if (summary.window.end) params.set("to", summary.window.end);
}
const qs = params.toString();
return `/api/company/intercompany/export.csv${qs ? `?${qs}` : ""}`;
}
function initTransfers() {
if (!$("[data-page='transfers']")) return;
$("#transfersRetryBtn")?.addEventListener("click", () => loadTransfersSummary());
$("#transfersExportBtn")?.addEventListener("click", () => {
window.location.href = buildTransfersExportUrl();
});
$("#transfersDetailExportBtn")?.addEventListener("click", () => {
window.location.href = buildTransfersExportUrl();
});
$("#transfersBackBtn")?.addEventListener("click", () => {
state.transfersDetail = null;
showTransfersOverviewLayer();
const title = $("#currentViewName");
if (title) title.textContent = "转账往来";
if (!state.transfersSummary) loadTransfersSummary();
else renderTransfersOverview(state.transfersSummary);
});
$("#transfersCpBody")?.addEventListener("click", (event) => {
const row = event.target.closest("tr[data-cp-id]");
if (!row) return;
openTransfersDetail(row.dataset.cpId, row.dataset.cpName);
});
$("#transfersFilterForm")?.addEventListener("submit", (event) => {
event.preventDefault();
if (!state.transfersDetail) return;
state.transfersDetail.filters = {
from: $("#tfFilterFrom")?.value || "",
to: $("#tfFilterTo")?.value || "",
direction: $("#tfFilterDirection")?.value || "",
state: $("#tfFilterState")?.value || "",
};
loadTransfersEvents({ reset: true });
});
const clearFilters = () => {
if (!state.transfersDetail) return;
const win = state.transfersSummary?.window || {};
state.transfersDetail.filters = {
from: win.start || "",
to: win.end || "",
direction: "",
state: "",
};
$("#tfFilterFrom").value = state.transfersDetail.filters.from;
$("#tfFilterTo").value = state.transfersDetail.filters.to;
$("#tfFilterDirection").value = "";
$("#tfFilterState").value = "";
loadTransfersEvents({ reset: true });
};
$("#tfFilterReset")?.addEventListener("click", clearFilters);
$("#tfEmptyClear")?.addEventListener("click", clearFilters);
$("#transfersLoadMore")?.addEventListener("click", () => loadTransfersEvents({ reset: false }));
$("#transfersEventBody")?.addEventListener("click", (event) => {
const btn = event.target.closest("[data-transfer-evidence]");
if (!btn) return;
openTransferEvidence(btn.dataset.transferEvidence);
});
$$("[data-close-transfer-evidence]").forEach((btn) => btn.addEventListener("click", () => closeTransferEvidence()));
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && $("#transferEvidenceDrawer")?.classList.contains("is-open")) {
event.stopPropagation();
closeTransferEvidence();
}
});
// 工作台概览与角标:与页面共用 summary
loadTransfersSummary();
}
function initCompany() {
renderCompanyManualRecords();
loadCompanyAccounts();
@@ -3071,6 +3631,9 @@ function initCompany() {
showToast("手工记录已撤回", "该记录已从审核队列中移除,需重新登记提交", "success");
});
// ── 转账往来(方案 A)──
initTransfers();
// ── 往来确认 + 工作台权威待办 ──
initReconcile();
(async () => {