const $ = (selector, scope = document) => scope.querySelector(selector); 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: "通知" }; const storageKeys = { accounts: "ledger-demo-account-submissions", manual: "ledger-demo-manual-records", }; const state = { currentView: portal === "admin" ? "dashboard" : "workspace", selectedFile: null, parseResult: null, }; const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); function animateView(view, { initial = false } = {}) { if (!view || motionQuery.matches || typeof view.animate !== "function") return; if (!initial) { view.getAnimations().forEach((animation) => animation.cancel()); view.animate( [{ opacity: 0.84, transform: "translateY(5px)" }, { opacity: 1, transform: "translateY(0)" }], { duration: 180, easing: "cubic-bezier(.22,1,.36,1)" }, ); return; } const selectors = [ ".page-heading > *", ".metric-card", ".period-ribbon", ".company-alert", ".admin-dashboard-grid > *", ".company-dashboard-grid > *", ".company-ledger-panel", ".query-band", ".filter-bar", ".filter-grid", ".pair-report", ".panel", ".work-progress", ".reconcile-summary", ".account-directory > article", ]; const elements = [...new Set(selectors.flatMap((selector) => [...view.querySelectorAll(selector)]))] .filter((element) => !element.closest(".panel") || element.matches(".panel")); elements.forEach((element, index) => { element.getAnimations().forEach((animation) => animation.cancel()); element.animate( [ { opacity: 0, transform: `translateY(${initial ? 16 : 10}px)` }, { opacity: 1, transform: "translateY(0)" }, ], { duration: 440, delay: Math.min(index * 38, 260), easing: "cubic-bezier(.22,1,.36,1)", fill: "both", }, ); }); } function initMotion() { if (motionQuery.matches) return; animateView($(".app-view.is-active"), { initial: true }); $$(".nav-item", $("#sidebar") || document).forEach((item, index) => { item.animate( [{ opacity: 0, transform: "translateX(-8px)" }, { opacity: 1, transform: "translateX(0)" }], { duration: 360, delay: 90 + index * 28, easing: "cubic-bezier(.22,1,.36,1)", fill: "both" }, ); }); $$(".company-ledger").forEach((ledger) => { ledger.addEventListener("toggle", () => { if (!ledger.open) return; $(".ledger-breakdown", ledger)?.animate( [{ opacity: 0, transform: "translateY(-8px)" }, { opacity: 1, transform: "translateY(0)" }], { duration: 260, easing: "cubic-bezier(.22,1,.36,1)" }, ); }); }); } function readStoredRecords(key) { try { const value = JSON.parse(localStorage.getItem(key) || "[]"); return Array.isArray(value) ? value : []; } catch { return []; } } function writeStoredRecords(key, records) { try { localStorage.setItem(key, JSON.stringify(records)); return true; } catch { showToast("本机演示数据保存失败", "请检查浏览器是否允许本地存储"); return false; } } function recordStatus(status) { if (["已启用", "已确认"].includes(status)) return { className: "success", label: status }; if (status === "已退回") return { className: "danger", label: status }; if (status === "异常待处理") return { className: "neutral", label: status }; return { className: "warning", label: status || "待复核" }; } function formatCurrency(value) { return Number(value).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function showToast(title, detail = "") { const region = $("#toastRegion"); if (!region) return; const toast = document.createElement("div"); toast.className = "toast"; const heading = document.createElement("strong"); heading.textContent = title; toast.append(heading); if (detail) { const description = document.createElement("small"); description.textContent = detail; toast.append(description); } region.append(toast); window.setTimeout(() => toast.remove(), 3400); } function closeNavigation({ restoreFocus = false } = {}) { const sidebar = $("#sidebar"); const button = $("#menuButton"); if (!sidebar || !button) return; const wasOpen = sidebar.classList.contains("is-open"); sidebar.classList.remove("is-open"); button.setAttribute("aria-expanded", "false"); button.setAttribute("aria-label", "打开导航"); if (restoreFocus && wasOpen) button.focus(); } function showView(view) { if (!viewNames[view]) return; const navigationWasOpen = $("#sidebar")?.classList.contains("is-open"); state.currentView = view; $$(".app-view").forEach((page) => page.classList.toggle("is-active", page.dataset.page === view)); $$(".nav-item[data-view]").forEach((item) => { const active = item.dataset.view === view; item.classList.toggle("is-active", active); if (active) item.setAttribute("aria-current", "page"); else item.removeAttribute("aria-current"); }); const title = $("#currentViewName"); if (title) 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" }); } function initEntry() { const form = $("#loginForm"); if (!form) return; if (!motionQuery.matches) { [$(".entry-brand"), $(".entry-statement"), ...$$(".entry-facts > div"), form].filter(Boolean).forEach((element, index) => { element.animate( [{ opacity: 0, transform: "translateY(16px)" }, { opacity: 1, transform: "translateY(0)" }], { duration: 520, delay: index * 65, easing: "cubic-bezier(.22,1,.36,1)", fill: "both" }, ); }); } const roleInputs = $$('input[name="role"]', form); const username = $('input[name="username"]', form); const action = $("#loginAction"); function updateRole() { const role = $('input[name="role"]:checked', form).value; action.textContent = role === "admin" ? "进入总账管理端" : "进入公司业务端"; username.value = role === "admin" ? "group-admin" : "a-cashier"; } roleInputs.forEach((input) => input.addEventListener("change", updateRole)); $("#togglePassword").addEventListener("click", (event) => { const password = $('input[name="password"]', form); const visible = password.type === "text"; password.type = visible ? "password" : "text"; event.currentTarget.setAttribute("aria-label", visible ? "显示密码" : "隐藏密码"); event.currentTarget.title = visible ? "显示密码" : "隐藏密码"; }); form.addEventListener("submit", (event) => { event.preventDefault(); const role = $('input[name="role"]:checked', form).value; window.location.href = role === "admin" ? "admin.html" : "company.html"; }); } function initShell() { const menuButton = $("#menuButton"); if (menuButton) { menuButton.addEventListener("click", () => { const open = $("#sidebar").classList.toggle("is-open"); menuButton.setAttribute("aria-expanded", String(open)); menuButton.setAttribute("aria-label", open ? "关闭导航" : "打开导航"); }); } $$(".nav-item").forEach((item) => { const label = $("span", item)?.textContent.trim(); if (label) { item.setAttribute("aria-label", label); item.title = label; } }); $$("[data-view]").forEach((button) => button.addEventListener("click", () => showView(button.dataset.view))); $$("[data-view-link]").forEach((button) => button.addEventListener("click", () => showView(button.dataset.viewLink))); $$("[data-metric-link]").forEach((card) => { const activate = () => showView(card.dataset.metricLink); card.addEventListener("click", activate); card.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(); } }); }); $$("[data-metric-action=\"upload\"]").forEach((card) => { const activate = () => $("[data-open-upload]")?.click(); card.addEventListener("click", activate); card.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); activate(); } }); }); $$("[data-toast]").forEach((button) => button.addEventListener("click", () => showToast(button.dataset.toast))); $(".nav-item[data-view].is-active")?.setAttribute("aria-current", "page"); $("#globalSearch")?.addEventListener("input", (event) => { const view = $(`.app-view[data-page="${state.currentView}"]`); const query = event.target.value.trim().toLowerCase(); $$(".data-table tbody tr, .company-ledger, .notification-list article, .account-directory article", view).forEach((item) => { item.hidden = query ? !item.textContent.toLowerCase().includes(query) : false; }); }); document.addEventListener("click", (event) => { if ($("#sidebar")?.classList.contains("is-open") && !event.target.closest("#sidebar") && !event.target.closest("#menuButton")) closeNavigation({ restoreFocus: true }); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeNavigation({ restoreFocus: true }); }); initMotion(); } function pairData(from, to, endDate) { const companies = ["A公司", "B公司", "C公司", "D公司", "E公司", "F公司"]; const fromIndex = companies.indexOf(from) + 1; const toIndex = companies.indexOf(to) + 1; const low = Math.min(fromIndex, toIndex); const high = Math.max(fromIndex, toIndex); const seed = low * 13 + high * 7; const canonical = { receivable: 280 + seed * 18, otherReceivable: 120 + seed * 9, payable: 160 + ((seed * 11) % 720), otherPayable: 80 + ((seed * 5) % 360), }; const reversed = fromIndex > toIndex; const receivable = reversed ? canonical.payable : canonical.receivable; const otherReceivable = reversed ? canonical.otherPayable : canonical.otherReceivable; const payable = reversed ? canonical.receivable : canonical.payable; const otherPayable = reversed ? canonical.otherReceivable : canonical.otherPayable; const debit = receivable + otherReceivable; const credit = payable + otherPayable; const opening = ((fromIndex + toIndex) % 3) * 60 * (reversed ? -1 : 1); const final = opening + debit - credit; const end = new Date(`${endDate}T12:00:00`); const dateBefore = (days) => { const date = new Date(end); date.setDate(date.getDate() - days); return date.toISOString().slice(0, 10); }; return { opening, debit, credit, final, totals: { 应收: receivable, 其他应收: otherReceivable, 应付: payable, 其他应付: otherPayable }, rows: [ [dateBefore(2), "转出", "应收", "中信 · 5316", `${to} · 9481`, "往来款", "双边匹配", receivable], [dateBefore(9), "转出", "其他应收", "建行 · 0845", `${to} · 2046`, "资金调拨", "双边匹配", otherReceivable], [dateBefore(17), "转入", "应付", "中信 · 5316", `${to} · 9481`, "归还往来款", "双边匹配", payable], [dateBefore(25), "转入", "其他应付", "农行 · 3650", `${to} · 6120`, "临时往来", "单边待核", otherPayable], ], }; } function setPair(from, to, endDate = "2026-07-31") { $$('[data-pair-from]').forEach((item) => { item.textContent = from; }); $$('[data-pair-to]').forEach((item) => { item.textContent = to; }); $$("[data-pair-form]").forEach((form) => { const fromSelect = $('[name="from"]', form); const toSelect = $('[name="to"]', form); if ([...fromSelect.options].some((option) => option.value === from)) fromSelect.value = from; if ([...toSelect.options].some((option) => option.value === to)) toSelect.value = to; const endInput = $('[name="end"]', form); if (endInput) endInput.value = endDate; }); if (!$("#pairReport")) return; const data = pairData(from, to, endDate); const format = (value) => value.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); $("#pairPeriod").textContent = `统计口径 2026.01.01—${endDate}`; $("#pairOpening").textContent = format(data.opening); $("#pairDebit").textContent = format(data.debit); $("#pairCredit").textContent = format(data.credit); $("#pairFinal").innerHTML = `${data.final >= 0 ? "应收" : "应付"} ${format(Math.abs(data.final))}万元`; $("#pairReviewStatus").textContent = "含 1 笔待审核"; const quickResult = $(".quick-result"); if (quickResult) { $("span", quickResult).textContent = `${from}对${to}`; $("strong", quickResult).innerHTML = `${data.final >= 0 ? "应收" : "应付"} ${format(Math.abs(data.final))}万元`; $("p", quickResult).textContent = `${data.rows.length - 1} 笔已确认 · 1 笔待审核`; } $('[data-subject-total="all"]').textContent = `${data.rows.length} 笔`; Object.entries(data.totals).forEach(([subject, value]) => { $(`[data-subject-total="${subject}"]`).textContent = format(value); }); $("#pairTransactions tbody").innerHTML = data.rows.map(([date, direction, subject, ownAccount, counterparty, summary, match, amount]) => `${date}${direction}${subject}${ownAccount}${counterparty}${summary}${match}${format(amount)}`).join(""); } function initPairQueries() { $$("[data-pair-form]").forEach((form) => { $("[data-swap]", form)?.addEventListener("click", () => { const from = $('[name="from"]', form); const to = $('[name="to"]', form); const previous = from.value; from.value = to.value; to.value = previous; }); form.addEventListener("submit", (event) => { event.preventDefault(); const from = $('[name="from"]', form).value; const to = $('[name="to"]', form).value; const endDate = $('[name="end"]', form)?.value || "2026-07-31"; if (from === to) { showToast("请选择两个不同的公司", "同公司账户调拨不进入公司间往来查询"); return; } setPair(from, to, endDate); showView("pair"); showToast("查询结果已更新", `${from} 与 ${to} · 截至 ${endDate}`); }); }); $$("[data-pair-link]").forEach((button) => button.addEventListener("click", () => { const [from, to] = button.dataset.pairLink.split("|"); setPair(from, to); showView("pair"); })); $$("[data-subject-filter]").forEach((button) => button.addEventListener("click", () => { const subject = button.dataset.subjectFilter; $$("[data-subject-filter]").forEach((item) => { const active = item === button; item.classList.toggle("is-active", active); item.setAttribute("aria-pressed", String(active)); }); $$("#pairTransactions tbody tr").forEach((row) => { row.hidden = subject !== "all" && row.dataset.subject !== subject; }); })); } function renderCompanyManualRecords() { const tbody = $("#manualRecordRows"); if (!tbody) return; $$('[data-stored-record]', tbody).forEach((row) => row.remove()); const records = readStoredRecords(storageKeys.manual).filter((record) => record.company === "A公司"); [...records].reverse().forEach((record) => { const row = document.createElement("tr"); row.dataset.storedRecord = record.id; const identity = document.createElement("td"); const id = document.createElement("strong"); id.textContent = record.id; const date = document.createElement("small"); date.textContent = record.transactionDate; identity.append(id, date); const direction = document.createElement("td"); const directionName = document.createElement("strong"); directionName.textContent = record.direction; const subject = document.createElement("small"); subject.textContent = record.subject; direction.append(directionName, subject); const source = document.createElement("td"); source.textContent = record.sourceAccount; const counterparty = document.createElement("td"); const counterpartyName = document.createElement("strong"); counterpartyName.textContent = record.counterparty; const counterpartyType = document.createElement("small"); counterpartyType.textContent = record.counterpartyType; counterparty.append(counterpartyName, counterpartyType); const summary = document.createElement("td"); summary.textContent = record.summary; const amount = document.createElement("td"); amount.className = "number"; amount.textContent = formatCurrency(record.amount); const statusCell = document.createElement("td"); const status = recordStatus(record.status); const badge = document.createElement("span"); badge.className = `status ${status.className}`; badge.textContent = status.label; statusCell.append(badge); row.append(identity, direction, source, counterparty, summary, amount, statusCell); tbody.append(row); }); const pending = records.filter((record) => record.status === "待总账复核").length + 1; if ($("#manualPendingStatus")) $("#manualPendingStatus").textContent = `${pending} 笔待总账复核`; } function addApprovedAccountOption(record) { if (record.status !== "已启用") return; const value = `${record.bank} · ${record.accountNumber.slice(-4)}`; [$("#accountSelect"), $('#manualEntryForm [name="sourceAccount"]')].forEach((select) => { if (!select || [...select.options].some((option) => option.value === value)) return; const option = new Option(value, value); option.dataset.submittedAccount = record.id; select.add(option); }); } function renderCompanyAccountSubmissions() { const directory = $("#accountDirectory"); if (!directory) return; $$('[data-submitted-account]', directory).forEach((item) => item.remove()); const records = readStoredRecords(storageKeys.accounts).filter((record) => record.company === "A公司"); records.forEach((record) => { const article = document.createElement("article"); article.dataset.submittedAccount = record.id; const header = document.createElement("header"); const mark = document.createElement("span"); mark.className = "bank-mark"; mark.textContent = record.bank.slice(0, 1); const identity = document.createElement("div"); const name = document.createElement("strong"); name.textContent = record.bank; const meta = document.createElement("small"); meta.textContent = `${record.type} · 尾号 ${record.accountNumber.slice(-4)}`; identity.append(name, meta); const status = recordStatus(record.status); const badge = document.createElement("em"); badge.className = `status ${status.className}`; badge.textContent = status.label; header.append(mark, identity, badge); const details = document.createElement("dl"); [["申请启用", record.startDate], ["流水覆盖", record.status === "已启用" ? "尚未上传" : "不参与计算"], ["提交时间", record.createdAt]].forEach(([term, value]) => { const wrapper = document.createElement("div"); const dt = document.createElement("dt"); dt.textContent = term; const dd = document.createElement("dd"); dd.textContent = value; wrapper.append(dt, dd); details.append(wrapper); }); article.append(header, details); directory.append(article); addApprovedAccountOption(record); }); } function appendAdminReviewRow(record, kind) { const tbody = $("#auditRows"); if (!tbody) return; const row = document.createElement("tr"); row.dataset.storedReview = record.id; row.dataset.recordId = record.id; row.dataset.recordKind = kind; row.dataset.auditType = kind === "account" ? "账户" : "手工"; row.dataset.company = record.company; row.dataset.evidence = kind === "account" ? "公司提交资料、开户行、账号、账户类型与启用日期" : "公司手工记录、关联银行流水号、证明附件与提交说明"; if (record.status !== "待复核" && record.status !== "待总账复核") row.dataset.resolved = "true"; const riskCell = document.createElement("td"); const risk = document.createElement("span"); risk.className = "task-level warning"; risk.textContent = "中"; riskCell.append(risk); const identityCell = document.createElement("td"); const identity = document.createElement("strong"); const detail = document.createElement("small"); const typeCell = document.createElement("td"); const periodCell = document.createElement("td"); const impactCell = document.createElement("td"); if (kind === "account") { identity.textContent = `${record.company} · ${record.bank} ${record.accountNumber.slice(-4)}`; detail.textContent = `${record.type} · 完整账号 ${record.accountNumber}`; typeCell.textContent = "账户登记"; periodCell.textContent = record.startDate; impactCell.textContent = "账户识别与流水上传"; } else { identity.textContent = `${record.company} · ${record.id}`; detail.textContent = `${record.direction} ${formatCurrency(record.amount)} 元 · ${record.counterparty} · ${record.subject}`; typeCell.textContent = "手工记录"; periodCell.textContent = record.transactionDate; impactCell.textContent = `待确认往来 ${(Number(record.amount) / 10000).toFixed(2)} 万元`; } identityCell.append(identity, detail); const statusCell = document.createElement("td"); const status = recordStatus(record.status); const badge = document.createElement("span"); badge.className = `status ${status.className}`; badge.textContent = status.label; statusCell.append(badge); const actionCell = document.createElement("td"); const action = document.createElement("button"); action.className = "text-button"; action.dataset.auditAction = ""; action.textContent = row.dataset.resolved ? "查看记录" : "复核"; if (row.dataset.resolved) { action.dataset.record = `${record.decision || record.status} · ${record.reviewReason || "已留痕"}`; action.dataset.decision = record.decision || record.status; action.dataset.reason = record.reviewReason || "已留痕"; action.dataset.processedAt = record.reviewedAt || "时间未记录"; } actionCell.append(action); row.append(riskCell, identityCell, typeCell, periodCell, impactCell, statusCell, actionCell); tbody.append(row); } function renderStoredAdminReviews() { if (!$("#auditRows")) return; $$('[data-stored-review]', $("#auditRows")).forEach((row) => row.remove()); readStoredRecords(storageKeys.accounts).forEach((record) => appendAdminReviewRow(record, "account")); readStoredRecords(storageKeys.manual).forEach((record) => appendAdminReviewRow(record, "manual")); } function updateStoredReview(kind, id, status, decision, reviewReason, reviewedAt) { if (!kind || !id) return; const key = kind === "account" ? storageKeys.accounts : storageKeys.manual; const records = readStoredRecords(key); const record = records.find((item) => item.id === id); if (!record) return; Object.assign(record, { status, decision, reviewReason, reviewedAt }); writeStoredRecords(key, records); } function updateAuditCounts() { const rows = $$("#auditRows tr"); const unresolved = rows.filter((row) => row.dataset.resolved !== "true"); $$('[data-audit-filter]').forEach((button) => { const type = button.dataset.auditFilter; const count = unresolved.filter((row) => type === "all" || row.dataset.auditType === type).length; button.textContent = `${button.dataset.label} ${count}`; }); const badge = $('.nav-item[data-view="audit"] b'); if (badge) badge.textContent = unresolved.length; } function initAdmin() { renderStoredAdminReviews(); updateAuditCounts(); const companySearch = $('[data-filter-target="companyLedgers"]'); companySearch?.addEventListener("input", () => { const query = companySearch.value.trim().toLowerCase(); $$("#companyLedgers .company-ledger").forEach((item) => { item.hidden = query ? !item.textContent.toLowerCase().includes(query) : false; }); }); let activeAuditType = "all"; function filterAuditRows() { const company = $("#auditCompany")?.value || "全部公司"; $$(".audit-table tbody tr").forEach((row) => { const typeMatches = activeAuditType === "all" || row.dataset.auditType === activeAuditType; const companyMatches = company === "全部公司" || row.dataset.company === company; row.hidden = !(typeMatches && companyMatches); }); } $$("[data-audit-filter]").forEach((button) => button.addEventListener("click", () => { activeAuditType = button.dataset.auditFilter; $$("[data-audit-filter]").forEach((item) => { const active = item === button; item.classList.toggle("is-active", active); item.setAttribute("aria-pressed", String(active)); }); filterAuditRows(); })); $("#auditCompany")?.addEventListener("change", filterAuditRows); const auditDialog = $("#auditDialog"); $$('[data-close-audit]').forEach((button) => button.addEventListener("click", () => auditDialog.close())); $$("[data-audit-action]").forEach((button) => button.addEventListener("click", () => { const row = button.closest("tr"); state.auditRow = row; const cells = $$('td', row); const form = $("#auditForm"); const decision = $('[name="decision"]', form); const reason = $('[name="reason"]', form); const submit = $('button[type="submit"]', form); form.reset(); const decisions = row.dataset.recordKind === "account" ? ["复核通过并启用账户", "退回公司修改", "停用并驳回"] : ["确认并纳入计算", "退回公司补充材料", "转为异常待后续处理"]; decision.replaceChildren(new Option("请选择", ""), ...decisions.map((item) => new Option(item, item))); decision.disabled = false; reason.disabled = false; submit.hidden = false; $("#auditDialogTitle").textContent = `${cells[2].innerText.trim()} · ${cells[1].querySelector("strong").textContent}`; $("#auditDialogMeta").textContent = `${cells[3].innerText.trim()} · 影响 ${cells[4].innerText.trim()}`; const evidence = $("#auditEvidence"); evidence.replaceChildren(); const heading = document.createElement("strong"); heading.textContent = cells[1].querySelector("strong").textContent; const detail = document.createElement("small"); detail.textContent = cells[1].querySelector("small").textContent; const source = document.createElement("small"); source.textContent = `证据:${row.dataset.evidence || "银行原始行、导入批次、账户覆盖区间与公司说明"}`; evidence.append(heading, detail, source); if (button.dataset.record) { decision.value = button.dataset.decision; reason.value = button.dataset.reason; decision.disabled = true; reason.disabled = true; submit.hidden = true; $("#auditDialogMeta").textContent = `已处理 · ${button.dataset.processedAt} · 系统管理员`; const record = document.createElement("small"); record.textContent = `处理记录:${button.dataset.record}`; evidence.append(record); } auditDialog.showModal(); })); $("#auditForm")?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); const row = state.auditRow; const decision = String(data.get("decision")); const approved = decision.includes("通过") || decision.includes("确认并纳入"); const returned = decision.includes("退回"); const storedStatus = approved ? (row.dataset.recordKind === "account" ? "已启用" : "已确认") : (returned ? "已退回" : "异常待处理"); const status = recordStatus(storedStatus); const statusCell = row.children[5]; statusCell.innerHTML = `${status.label}`; row.dataset.resolved = "true"; const action = $("[data-audit-action]", row); action.textContent = "查看记录"; action.dataset.record = `${decision} · ${data.get("reason")}`; action.dataset.decision = decision; action.dataset.reason = data.get("reason"); action.dataset.processedAt = new Date().toLocaleString("zh-CN", { hour12: false }); updateStoredReview(row.dataset.recordKind, row.dataset.recordId, storedStatus, decision, String(data.get("reason")), action.dataset.processedAt); updateAuditCounts(); auditDialog.close(); event.currentTarget.reset(); showToast("审核结果已记录", approved ? "已通过的数据取得相应使用或核算资格" : "当前记录不参与自动核算"); }); const dialog = $("#companyDialog"); $("#openCompanyDialog")?.addEventListener("click", () => dialog.showModal()); $("#companyForm")?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); const row = document.createElement("tr"); const values = [data.get("companyName"), data.get("creditCode") || "待补充", "0 个", data.get("loginName"), data.get("cashier")]; values.forEach((value, index) => { const cell = document.createElement("td"); if (index === 0) { const strong = document.createElement("strong"); strong.textContent = value; cell.append(strong); } else cell.textContent = value; row.append(cell); }); const statusCell = document.createElement("td"); statusCell.innerHTML = '正常'; const actionCell = document.createElement("td"); actionCell.innerHTML = ''; row.append(statusCell, actionCell); $("#companyTable tbody").append(row); dialog.close(); event.currentTarget.reset(); showToast("公司与账号已创建", "初始密码:ChangeMe2026(演示)"); }); $("#systemSettings")?.addEventListener("submit", (event) => { event.preventDefault(); showToast("系统计算口径已保存", "正式系统将记录修改前后值与操作人"); }); $("#runClosingCheck")?.addEventListener("click", () => { const unresolved = $$(".audit-table tbody tr").filter((row) => row.dataset.resolved !== "true" && !$(".status.success", row)).length; if (unresolved) { showToast("结账检查未通过", `仍有 ${unresolved} 项审核事项,已打开审核中心`); showView("audit"); } else { $$("#closingPanel .is-blocked").forEach((item) => { item.classList.remove("is-blocked"); $("use", item).setAttribute("href", "icons.svg#circle-check"); $("small", item).textContent = "检查已通过"; }); $("#closingDescription").textContent = "2026 年 7 月 · 全部前置检查已通过"; $("#closingStatus").className = "status success"; $("#closingStatus").textContent = "可结账"; $("#executeClosing").disabled = false; $("#executeClosing").removeAttribute("title"); showToast("结账检查通过", "执行结账按钮已解锁"); } }); const closingDialog = $("#closingDialog"); $("#executeClosing")?.addEventListener("click", () => closingDialog.showModal()); $$('[data-close-closing]').forEach((button) => button.addEventListener("click", () => closingDialog.close())); $("#closingForm")?.addEventListener("submit", (event) => { event.preventDefault(); closingDialog.close(); $("#closingDescription").textContent = "2026 年 7 月 · 已完成集团结账"; $("#closingStatus").className = "status success"; $("#closingStatus").textContent = "已结账"; $("#closingHistory").textContent = `${new Date().toLocaleString("zh-CN", { hour12: false })} · 系统管理员执行 2026 年 7 月结账 · 已写入审计记录`; $("#runClosingCheck").disabled = true; $("#executeClosing").disabled = true; $("#executeClosing").textContent = "7 月已结账"; showToast("2026 年 7 月已完成结账", "本期结果已锁定,后续补录将进入重开流程"); }); const openingDialog = $("#openingDialog"); $("#openOpeningDialog")?.addEventListener("click", () => openingDialog.showModal()); $$('[data-close-opening]').forEach((button) => button.addEventListener("click", () => openingDialog.close())); $("#openingForm")?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); if (data.get("from") === data.get("to")) { showToast("本方与对方不能相同", "同公司账户余额不属于公司间期初"); return; } const row = document.createElement("tr"); [data.get("from"), data.get("to"), data.get("subject"), data.get("direction")].forEach((value) => { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); }); const amount = document.createElement("td"); amount.className = "number"; amount.textContent = Number(data.get("amount")).toLocaleString("zh-CN", {minimumFractionDigits:2}); row.append(amount); const date = document.createElement("td"); date.textContent = data.get("effectiveDate"); row.append(date); const status = document.createElement("td"); status.innerHTML = '待复核'; row.append(status); $("#openingRows").append(row); openingDialog.close(); event.currentTarget.reset(); showToast("期初余额已提交复核", "正式系统将保留录入依据与操作人"); }); $("#reminderForm")?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); const article = document.createElement("article"); article.innerHTML = ''; const content = document.createElement("span"); const heading = document.createElement("strong"); heading.textContent = `${data.get("company")} · ${data.get("message")}`; const meta = document.createElement("small"); meta.textContent = `手动提醒 · 截止 ${data.get("dueDate")} · 刚刚`; content.append(heading, meta); const status = document.createElement("em"); status.className = "status warning"; status.textContent = "未读"; article.append(content, status); $("#adminReminderList").prepend(article); event.currentTarget.reset(); showToast("提醒已发送", "对方将在公司业务端收到站内通知"); }); } function visibleRows(table) { return $$('tbody tr', table).filter((row) => !row.hidden); } function filterFlows() { const table = $("#flowTable"); if (!table) return; const company = $("#flowCompany")?.value || "全部公司"; const bank = $("#flowBank")?.value || "全部银行"; const account = $("#flowAccount")?.value || "全部账户"; const startDate = $("#flowStart")?.value || "0000-01-01"; const endDate = $("#flowEnd")?.value || "9999-12-31"; const keyword = $("#flowKeyword")?.value.trim().toLowerCase() || ""; if (startDate > endDate) { showToast("日期范围无效", "开始日期不能晚于结束日期"); return; } let count = 0; $$("tbody tr", table).forEach((row) => { const rowDate = $("td", row).textContent.trim().replaceAll(".", "-"); const matchesCompany = company === "全部公司" || row.dataset.company === company; const matchesBank = bank === "全部银行" || row.dataset.bank === bank; const matchesAccount = account === "全部账户" || row.textContent.includes(account); const matchesDate = rowDate >= startDate && rowDate <= endDate; const matchesKeyword = !keyword || row.textContent.toLowerCase().includes(keyword); row.hidden = !(matchesCompany && matchesBank && matchesAccount && matchesDate && matchesKeyword); if (!row.hidden) count += 1; }); $("#flowCount").textContent = count; showToast("查询完成", `当前显示 ${count} 笔流水`); } function exportFlows() { const table = $("#flowTable"); const rows = visibleRows(table); const headers = ["交易日期", "公司", "银行及账号", "收付方向", "对方户名及账号", "摘要", "银行流水号", "归集状态", "金额", "导入批次", "源行定位"]; const records = rows.map((row, index) => { const cells = $$('td', row).map((cell) => cell.innerText.replace(/\n/g, " ").trim()); return [cells[0], portal === "company" ? "A公司" : cells[1].split(" ")[0], portal === "company" ? cells[1] : cells[1], ...cells.slice(2), `IMP-DEMO-${String(index + 1).padStart(3, "0")}`, `Sheet1!R${index + 8}`]; }); const csv = [headers, ...records].map((record) => record.map((value) => `"${String(value ?? "").replace(/"/g, '""')}"`).join(",")).join("\r\n"); const link = document.createElement("a"); link.href = URL.createObjectURL(new Blob(["\ufeff", csv], { type: "text/csv;charset=utf-8" })); link.download = `${portal === "admin" ? "集团" : "A公司"}银行流水_202607.csv`; link.click(); URL.revokeObjectURL(link.href); showToast("导出已生成", `共 ${rows.length} 笔,已保留银行标识与源行定位`); } function initFlowTools() { $("#applyFlowFilters")?.addEventListener("click", filterFlows); $("#exportFlows")?.addEventListener("click", exportFlows); } function resetUpload() { state.selectedFile = null; state.parseResult = null; $("#uploadForm")?.reset(); if ($("#filePreview")) $("#filePreview").hidden = true; if ($("#parseResult")) $("#parseResult").hidden = true; if ($("#dropzone")) $("#dropzone").hidden = false; if ($("#parseButton")) { $("#parseButton").disabled = true; $("#parseButton span").textContent = "开始解析"; delete $("#parseButton").dataset.stage; } } function updateParseButton() { const button = $("#parseButton"); if (button) button.disabled = !(state.selectedFile && $("#accountSelect").value); } function acceptFile(file) { if (!file) return; const extension = file.name.split(".").pop().toLowerCase(); if (!["xls", "xlsx"].includes(extension)) { showToast("文件格式不支持", "请选择银行导出的 .xls 或 .xlsx 文件"); return; } state.selectedFile = file; state.parseResult = null; $("#fileName").textContent = file.name; $("#fileMeta").textContent = `${(file.size / 1024).toFixed(1)} KB · 等待表头识别`; $("#filePreview").hidden = false; $("#dropzone").hidden = true; $("#parseResult").hidden = true; delete $("#parseButton").dataset.stage; updateParseButton(); } async function parseFile() { const formData = new FormData(); formData.append("file", state.selectedFile); let result; let parsed = false; try { const response = await fetch("/api/parse", { method: "POST", body: formData }); result = await response.json(); parsed = response.ok && result.status === "parsed"; } catch { result = { status: "error", message: "解析服务暂时不可用,请稍后重试。" }; } state.parseResult = result; const panel = $("#parseResult"); panel.classList.toggle("is-exception", !parsed); $("use", panel).setAttribute("href", parsed ? "icons.svg#circle-check" : "icons.svg#circle-alert"); $("strong", panel).textContent = parsed ? "文件解析完成" : "未识别到银行模板"; $("#parseSummary").textContent = parsed ? `${result.bank} · 表头第 ${result.header_row} 行 · ${result.transactions} 条明细 · ${result.warnings.length ? `${result.warnings.length} 项提示` : "校验通过"}` : `${result.message} 系统不会猜测模板或自动入账。`; panel.hidden = false; $("#parseButton span").textContent = parsed ? "确认导入" : "提交异常"; $("#parseButton").disabled = false; $("#parseButton").dataset.stage = "confirm"; } function confirmImport() { const row = document.createElement("tr"); row.innerHTML = `IMP-260806-019${$("#accountSelect").value}${state.parseResult?.period_start || "待确认"}—${state.parseResult?.period_end || "待确认"}${state.parseResult?.transactions ?? "—"}待计算已解析刚刚`; $("#importRows")?.prepend(row); $("#uploadDialog").close(); showView("upload"); showToast("流水已进入归集队列", "原始文件和解析结果已保留"); } function submitImportException() { $("#uploadDialog").close(); showView("upload"); showToast("解析异常已提交", `${state.selectedFile?.name || "该文件"} 未入账,等待总账或模板维护人员处理`); } function initCompany() { renderCompanyManualRecords(); renderCompanyAccountSubmissions(); const uploadDialog = $("#uploadDialog"); $$('[data-open-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.showModal())); $$('[data-close-upload]').forEach((button) => button.addEventListener("click", () => uploadDialog.close())); uploadDialog?.addEventListener("close", resetUpload); $("#accountSelect")?.addEventListener("change", updateParseButton); $("#fileInput")?.addEventListener("change", (event) => acceptFile(event.target.files[0])); $("#removeFile")?.addEventListener("click", () => { state.selectedFile = null; $("#fileInput").value = ""; $("#filePreview").hidden = true; $("#dropzone").hidden = false; $("#parseResult").hidden = true; updateParseButton(); }); const dropzone = $("#dropzone"); if (dropzone) { ["dragenter", "dragover"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.add("is-dragging"); })); ["dragleave", "drop"].forEach((type) => dropzone.addEventListener(type, (event) => { event.preventDefault(); dropzone.classList.remove("is-dragging"); })); dropzone.addEventListener("drop", (event) => acceptFile(event.dataTransfer.files[0])); } $("#uploadForm")?.addEventListener("submit", async (event) => { event.preventDefault(); if ($("#parseButton").dataset.stage === "confirm") { if (state.parseResult?.status === "parsed") confirmImport(); else submitImportException(); return; } $("#parseButton").disabled = true; $("#parseButton span").textContent = "正在识别表头..."; await parseFile(); }); function finishReview(button, disposition) { const item = button.closest("article"); const type = item.dataset.reviewType; const selectedMatch = $('input[name="matchCandidate"]:checked', item)?.value; const selectedSubject = $("select", item)?.value; const detail = type === "match" ? (selectedMatch || "转为匹配异常") : selectedSubject; const history = $("#reviewHistory"); const record = document.createElement("p"); record.textContent = `${new Date().toLocaleString("zh-CN", { hour12: false })} · 牛女士 · ${disposition} · ${detail}`; history.append(record); history.hidden = false; item.remove(); if (type === "match") $("#matchPendingCount").textContent = "0 笔"; else $("#subjectPendingCount").textContent = "0 笔"; const remaining = $$("#reviewList > article").length; const badge = $('.nav-item[data-view="reconcile"] b'); if (badge) badge.textContent = remaining; $(`.cashier-tasks [data-task-type="${type}"]`)?.remove(); const workspaceRemaining = $$(".cashier-tasks > article").length; $("#workspacePendingStatus").textContent = `${workspaceRemaining} 项待处理`; const workspaceBadge = $('.nav-item[data-view="workspace"] b'); if (workspaceBadge) workspaceBadge.textContent = workspaceRemaining; showToast("处理结果已记录", "待办状态、操作人、时间和依据已同步更新"); } $$('[data-resolve]').forEach((button) => button.addEventListener("click", () => finishReview(button, "确认"))); $$('[data-reject]').forEach((button) => button.addEventListener("click", () => finishReview(button, "转异常"))); $("#markAllRead")?.addEventListener("click", () => { $$("#companyNotifications .is-unread").forEach((item) => { item.classList.remove("is-unread"); const status = $(".status", item); status.className = "status neutral"; status.textContent = "已读"; }); showToast("通知已全部标为已读"); }); $("#manualEntryForm")?.addEventListener("submit", (event) => { event.preventDefault(); const form = event.currentTarget; const data = new FormData(form); const evidence = data.get("evidence"); if (evidence instanceof File && evidence.size > 20 * 1024 * 1024) { showToast("证明附件超过限制", "请选择不超过 20 MB 的文件"); return; } const records = readStoredRecords(storageKeys.manual); const record = { id: `MR-${Date.now().toString().slice(-10)}`, company: "A公司", transactionDate: String(data.get("transactionDate")), direction: String(data.get("direction")), amount: Number(data.get("amount")), sourceAccount: String(data.get("sourceAccount")), counterpartyType: String(data.get("counterpartyType")), counterparty: String(data.get("counterparty")).trim(), counterpartyAccount: String(data.get("counterpartyAccount") || "").trim(), subject: String(data.get("subject")), summary: String(data.get("summary")).trim(), remark: String(data.get("remark")).trim(), bankReference: String(data.get("bankReference") || "").trim(), evidenceName: evidence instanceof File ? evidence.name : "", status: "待总账复核", createdAt: new Date().toLocaleString("zh-CN", { hour12: false }), }; records.push(record); if (!writeStoredRecords(storageKeys.manual, records)) return; renderCompanyManualRecords(); form.reset(); showToast("手工记录已提交", "总账复核前不会纳入公司间往来计算"); }); const accountDialog = $("#accountDialog"); $("#openAccountDialog")?.addEventListener("click", () => accountDialog.showModal()); $$('[data-close-account]').forEach((button) => button.addEventListener("click", () => accountDialog.close())); $("#accountForm")?.addEventListener("submit", (event) => { event.preventDefault(); const data = new FormData(event.currentTarget); const accountNumber = String(data.get("accountNumber")).replace(/[\s-]/g, ""); const records = readStoredRecords(storageKeys.accounts); if (records.some((record) => record.accountNumber === accountNumber && record.status !== "已退回")) { showToast("该银行账号已登记", "请等待现有申请处理,或联系总账管理员核对"); return; } records.push({ id: `ACC-${Date.now().toString().slice(-10)}`, company: "A公司", bank: String(data.get("bank")).trim(), type: String(data.get("type")), accountNumber, startDate: String(data.get("startDate")), status: "待复核", createdAt: new Date().toLocaleString("zh-CN", { hour12: false }), }); if (!writeStoredRecords(storageKeys.accounts, records)) return; renderCompanyAccountSubmissions(); accountDialog.close(); event.currentTarget.reset(); showToast("银行账户已提交登记", "复核通过前不能上传流水,也不参与账户识别和覆盖计算"); }); } if (portal === "entry") { initEntry(); } else { initShell(); initFlowTools(); if (portal === "admin") { initPairQueries(); initAdmin(); } else { initCompany(); } }