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:
co-authored by
Cursor
multica-agent
parent
96309eb2cc
commit
b27016d71d
+565
-2
@@ -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, """)}">
|
||||
<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 () => {
|
||||
|
||||
Reference in New Issue
Block a user