1015 lines
47 KiB
JavaScript
1015 lines
47 KiB
JavaScript
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))}<small>万元</small>`;
|
|
$("#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))}<small>万元</small>`;
|
|
$("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]) => `<tr data-subject="${subject}"><td>${date}</td><td>${direction}</td><td>${subject}</td><td>${ownAccount}</td><td>${counterparty}</td><td>${summary}</td><td><span class="status ${match === "双边匹配" ? "success" : "warning"}">${match}</span></td><td class="number">${format(amount)}</td></tr>`).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 = `<span class="status ${status.className}">${status.label}</span>`;
|
|
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 = '<span class="status success">正常</span>';
|
|
const actionCell = document.createElement("td");
|
|
actionCell.innerHTML = '<button class="text-button">管理</button>';
|
|
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 = '<span class="status warning">待复核</span>'; 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 = '<span class="notification-icon warning"><svg><use href="icons.svg#bell"/></svg></span>';
|
|
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 = `<td><strong>IMP-260806-019</strong></td><td>${$("#accountSelect").value}</td><td>${state.parseResult?.period_start || "待确认"}—${state.parseResult?.period_end || "待确认"}</td><td>${state.parseResult?.transactions ?? "—"}</td><td><span class="status warning">待计算</span></td><td><span class="status neutral">已解析</span></td><td>刚刚</td>`;
|
|
$("#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();
|
|
}
|
|
}
|