1285 lines
48 KiB
JavaScript
1285 lines
48 KiB
JavaScript
/**
|
||
* 财务管理模块组件
|
||
* 包含:收支管理、客户对账、发票管理、供应商对账
|
||
*/
|
||
window.Finance = (() => {
|
||
|
||
// 当前子模块
|
||
let currentSubModule = 'income-expense';
|
||
|
||
// 渲染主框架
|
||
const render = () => {
|
||
return `
|
||
<div class="finance-container">
|
||
<!-- 子模块标签页 -->
|
||
<div class="finance-tabs">
|
||
<div class="tab-item ${currentSubModule === 'income-expense' ? 'active' : ''}" data-tab="income-expense">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||
</svg>
|
||
收支管理
|
||
</div>
|
||
<div class="tab-item ${currentSubModule === 'customer-rec' ? 'active' : ''}" data-tab="customer-rec">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M17 21v-2a4 4 0 0 0-3-3.87M9 21v-2a4 4 0 0 1 3-3.87M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/>
|
||
</svg>
|
||
客户对账
|
||
</div>
|
||
<div class="tab-item ${currentSubModule === 'supplier-rec' ? 'active' : ''}" data-tab="supplier-rec">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||
</svg>
|
||
供应商对账
|
||
</div>
|
||
<div class="tab-item ${currentSubModule === 'invoice' ? 'active' : ''}" data-tab="invoice">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<path d="M9 9h6M9 13h6M9 17h4"/>
|
||
</svg>
|
||
发票管理
|
||
</div>
|
||
<div class="tab-item ${currentSubModule === 'stats' ? 'active' : ''}" data-tab="stats">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M3 3v18h18M18 17V9M13 17V5M8 17v-5"/>
|
||
</svg>
|
||
统计报表
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容区域 -->
|
||
<div class="finance-content" id="finance-content">
|
||
${renderSubModule(currentSubModule)}
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.finance-container {
|
||
padding: 20px;
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
.finance-tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 20px;
|
||
border-bottom: 1px solid var(--border);
|
||
padding-bottom: 12px;
|
||
}
|
||
.tab-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 10px 16px;
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
transition: all 0.2s;
|
||
font-size: 14px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.tab-item svg {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
.tab-item:hover {
|
||
background: var(--surface);
|
||
color: var(--text);
|
||
}
|
||
.tab-item.active {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
.finance-content {
|
||
background: var(--surface);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
min-height: 500px;
|
||
}
|
||
.stats-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.stat-card {
|
||
background: var(--bg);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
.stat-card .label {
|
||
font-size: 13px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 8px;
|
||
}
|
||
.stat-card .value {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: var(--text);
|
||
}
|
||
.stat-card .value.income { color: #10B981; }
|
||
.stat-card .value.expense { color: #EF4444; }
|
||
.stat-card .value.balance { color: #3B82F6; }
|
||
.filter-bar {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
.filter-bar input, .filter-bar select {
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-size: 13px;
|
||
}
|
||
.filter-bar button {
|
||
padding: 8px 16px;
|
||
background: var(--primary);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
.filter-bar button.secondary {
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
}
|
||
.data-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
.data-table th, .data-table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 13px;
|
||
}
|
||
.data-table th {
|
||
background: var(--bg);
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
}
|
||
.data-table tr:hover {
|
||
background: var(--bg);
|
||
}
|
||
.badge {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
.badge.income { background: #D1FAE5; color: #059669; }
|
||
.badge.expense { background: #FEE2E2; color: #DC2626; }
|
||
.badge.confirmed { background: #DBEAFE; color: #2563EB; }
|
||
.badge.pending { background: #FEF3C7; color: #D97706; }
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
.modal-content {
|
||
background: var(--surface);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
.form-group input, .form-group select, .form-group textarea {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
font-size: 14px;
|
||
}
|
||
.chart-container {
|
||
height: 300px;
|
||
margin: 20px 0;
|
||
}
|
||
</style>
|
||
`;
|
||
};
|
||
|
||
// 渲染子模块内容
|
||
const renderSubModule = (module) => {
|
||
switch(module) {
|
||
case 'income-expense':
|
||
return renderIncomeExpense();
|
||
case 'customer-rec':
|
||
return renderCustomerReconciliation();
|
||
case 'supplier-rec':
|
||
return renderSupplierReconciliation();
|
||
case 'invoice':
|
||
return renderInvoice();
|
||
case 'stats':
|
||
return renderStats();
|
||
default:
|
||
return renderIncomeExpense();
|
||
}
|
||
};
|
||
|
||
// 收支管理
|
||
const renderIncomeExpense = () => {
|
||
return `
|
||
<div class="income-expense-module">
|
||
<div class="stats-cards">
|
||
<div class="stat-card">
|
||
<div class="label">本月收入</div>
|
||
<div class="value income" id="month-income">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">本月支出</div>
|
||
<div class="value expense" id="month-expense">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">本月结余</div>
|
||
<div class="value balance" id="month-balance">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">待确认</div>
|
||
<div class="value" id="pending-count">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-bar">
|
||
<select id="trans-type">
|
||
<option value="">全部类型</option>
|
||
<option value="income">收入</option>
|
||
<option value="expense">支出</option>
|
||
</select>
|
||
<select id="trans-category">
|
||
<option value="">全部类别</option>
|
||
<option value="货款">货款</option>
|
||
<option value="运费">运费</option>
|
||
<option value="工资">工资</option>
|
||
<option value="租金">租金</option>
|
||
<option value="税费">税费</option>
|
||
<option value="其他">其他</option>
|
||
</select>
|
||
<input type="date" id="start-date" placeholder="开始日期">
|
||
<input type="date" id="end-date" placeholder="结束日期">
|
||
<input type="text" id="related-party" placeholder="客户/供应商">
|
||
<button onclick="Finance.loadTransactions()">查询</button>
|
||
<button class="secondary" onclick="Finance.showAddTransactionModal()">+ 新增</button>
|
||
</div>
|
||
|
||
<table class="data-table" id="transactions-table">
|
||
<thead>
|
||
<tr>
|
||
<th>日期</th>
|
||
<th>类型</th>
|
||
<th>类别</th>
|
||
<th>金额</th>
|
||
<th>关联方</th>
|
||
<th>关联单号</th>
|
||
<th>支付方式</th>
|
||
<th>状态</th>
|
||
<th>备注</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="transactions-tbody">
|
||
<tr><td colspan="9" style="text-align:center;color:var(--text-secondary)">加载中...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div id="transaction-pagination" style="margin-top:16px;display:flex;justify-content:center;gap:8px;"></div>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
// 客户对账
|
||
const renderCustomerReconciliation = () => {
|
||
return `
|
||
<div class="customer-rec-module">
|
||
<div class="stats-cards" style="margin-bottom:16px;">
|
||
<div class="stat-card">
|
||
<div class="label">本期客户数</div>
|
||
<div class="value" id="rec-customer-count">0</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">总应收金额</div>
|
||
<div class="value income" id="rec-total-receivable">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">总已收金额</div>
|
||
<div class="value" id="rec-total-paid">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">总未收余额</div>
|
||
<div class="value expense" id="rec-total-balance">¥0.00</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-bar">
|
||
<input type="text" id="customer-name" placeholder="合同号/客户名称">
|
||
<input type="month" id="rec-period" placeholder="选择月份筛选">
|
||
<button onclick="Finance.loadCustomerRec()">查询</button>
|
||
<button class="secondary" onclick="Finance.clearPeriodFilter()">显示全部</button>
|
||
<button id="merge-btn" class="secondary" onclick="Finance.toggleMerge()" style="background:#3B82F6;color:white;">整合显示</button>
|
||
<button class="secondary" onclick="Finance.showGenerateStatementModal()">生成对账单</button>
|
||
</div>
|
||
|
||
<div id="rec-period-label" style="margin-bottom:12px;font-size:13px;color:var(--text-secondary);">显示全部历史数据</div>
|
||
|
||
<table class="data-table" id="customer-rec-table">
|
||
<thead>
|
||
<tr>
|
||
<th id="rec-th-customer">客户 / 合同号</th>
|
||
<th>订单数</th>
|
||
<th>物料明细</th>
|
||
<th>累计应收</th>
|
||
<th>累计已收</th>
|
||
<th>未收余额</th>
|
||
<th>回款率</th>
|
||
<th>最后交易</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="customer-rec-tbody">
|
||
<tr><td colspan="8" style="text-align:center;color:var(--text-secondary)">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
// 供应商对账
|
||
const renderSupplierReconciliation = () => {
|
||
return `
|
||
<div class="supplier-rec-module">
|
||
<div class="filter-bar">
|
||
<input type="text" id="supplier-name" placeholder="供应商名称">
|
||
<input type="month" id="supplier-period" value="${new Date().toISOString().slice(0, 7)}">
|
||
<button onclick="Finance.loadSupplierRec()">查询</button>
|
||
<button class="secondary" onclick="Finance.showGenerateSupplierStatementModal()">生成对账单</button>
|
||
</div>
|
||
|
||
<table class="data-table" id="supplier-rec-table">
|
||
<thead>
|
||
<tr>
|
||
<th>供应商名称</th>
|
||
<th>对账期间</th>
|
||
<th>期初应付</th>
|
||
<th>本期应付</th>
|
||
<th>本期已付</th>
|
||
<th>期末应付</th>
|
||
<th>状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="supplier-rec-tbody">
|
||
<tr><td colspan="8" style="text-align:center;color:var(--text-secondary)">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
// 发票管理
|
||
const renderInvoice = () => {
|
||
return `
|
||
<div class="invoice-module">
|
||
<div class="stats-cards">
|
||
<div class="stat-card">
|
||
<div class="label">本月进项发票</div>
|
||
<div class="value" id="input-invoice-count">0 张</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">进项金额</div>
|
||
<div class="value" id="input-invoice-amount">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">本月销项发票</div>
|
||
<div class="value" id="output-invoice-count">0 张</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">销项金额</div>
|
||
<div class="value" id="output-invoice-amount">¥0.00</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-bar">
|
||
<select id="invoice-type">
|
||
<option value="">全部</option>
|
||
<option value="input">进项发票</option>
|
||
<option value="output">销项发票</option>
|
||
</select>
|
||
<select id="invoice-status">
|
||
<option value="">全部状态</option>
|
||
<option value="normal">正常</option>
|
||
<option value="voided">作废</option>
|
||
</select>
|
||
<input type="date" id="invoice-start" placeholder="开始日期">
|
||
<input type="date" id="invoice-end" placeholder="结束日期">
|
||
<button onclick="Finance.loadInvoices()">查询</button>
|
||
<button class="secondary" onclick="Finance.showAddInvoiceModal()">+ 新增发票</button>
|
||
</div>
|
||
|
||
<table class="data-table" id="invoices-table">
|
||
<thead>
|
||
<tr>
|
||
<th>发票类型</th>
|
||
<th>发票号码</th>
|
||
<th>开票日期</th>
|
||
<th>金额</th>
|
||
<th>税额</th>
|
||
<th>价税合计</th>
|
||
<th>关联方</th>
|
||
<th>关联单号</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="invoices-tbody">
|
||
<tr><td colspan="9" style="text-align:center;color:var(--text-secondary)">暂无数据</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
// 统计报表
|
||
const renderStats = () => {
|
||
return `
|
||
<div class="stats-module">
|
||
<div class="filter-bar">
|
||
<select id="stats-range">
|
||
<option value="7">近7天</option>
|
||
<option value="30" selected>近30天</option>
|
||
<option value="90">近90天</option>
|
||
<option value="365">近一年</option>
|
||
</select>
|
||
<button onclick="Finance.loadStats()">刷新</button>
|
||
</div>
|
||
|
||
<div class="stats-cards">
|
||
<div class="stat-card">
|
||
<div class="label">总收入</div>
|
||
<div class="value income" id="total-income">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">总支出</div>
|
||
<div class="value expense" id="total-expense">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">净利润</div>
|
||
<div class="value balance" id="net-profit">¥0.00</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">毛利率</div>
|
||
<div class="value" id="profit-margin">0%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px;">
|
||
<div class="chart-container">
|
||
<h4 style="margin-bottom:12px;">收支趋势</h4>
|
||
<canvas id="trend-chart" width="400" height="300"></canvas>
|
||
</div>
|
||
<div>
|
||
<h4 style="margin-bottom:12px;">客户应收TOP10</h4>
|
||
<table class="data-table" id="receivables-table">
|
||
<thead>
|
||
<tr><th>客户</th><th>应收金额</th></tr>
|
||
</thead>
|
||
<tbody id="receivables-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:20px;">
|
||
<h4 style="margin-bottom:12px;">供应商应付TOP10</h4>
|
||
<table class="data-table" id="payables-table">
|
||
<thead>
|
||
<tr><th>供应商</th><th>应付金额</th></tr>
|
||
</thead>
|
||
<tbody id="payables-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
// 初始化模块
|
||
const init = () => {
|
||
// 激活当前模块对应的标签
|
||
document.querySelectorAll('.finance-tabs .tab-item').forEach(t => t.classList.remove('active'));
|
||
const activeTab = document.querySelector(`[data-tab="${currentSubModule}"]`);
|
||
if(activeTab) activeTab.classList.add('active');
|
||
|
||
// 渲染当前模块内容
|
||
const contentEl = document.getElementById('finance-content');
|
||
if(contentEl) contentEl.innerHTML = renderSubModule(currentSubModule);
|
||
|
||
// 绑定标签页切换
|
||
document.querySelectorAll('.finance-tabs .tab-item').forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
currentSubModule = tab.dataset.tab;
|
||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById('finance-content').innerHTML = renderSubModule(currentSubModule);
|
||
|
||
// 加载对应数据
|
||
switch(currentSubModule) {
|
||
case 'income-expense':
|
||
setTimeout(() => {
|
||
loadTransactionStats();
|
||
loadTransactions();
|
||
}, 0);
|
||
break;
|
||
case 'customer-rec':
|
||
setTimeout(() => loadCustomerRec(), 0);
|
||
break;
|
||
case 'supplier-rec':
|
||
setTimeout(() => loadSupplierRec(), 0);
|
||
break;
|
||
case 'invoice':
|
||
setTimeout(() => {
|
||
loadInvoiceStats();
|
||
loadInvoices();
|
||
}, 0);
|
||
break;
|
||
case 'stats':
|
||
setTimeout(() => loadStats(), 0);
|
||
break;
|
||
}
|
||
});
|
||
});
|
||
|
||
// 加载初始数据(只在收支管理标签页加载)
|
||
if(currentSubModule === 'income-expense') {
|
||
setTimeout(() => {
|
||
loadTransactionStats();
|
||
loadTransactions();
|
||
}, 0);
|
||
}
|
||
};
|
||
|
||
// ========== 数据加载函数 ==========
|
||
|
||
// 加载收支统计
|
||
const loadTransactionStats = async () => {
|
||
try {
|
||
const res = await fetch('/api/finance/transactions/stats', {
|
||
credentials: 'include'
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
document.getElementById('month-income').textContent =
|
||
'¥' + (res.stats.month_income || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
document.getElementById('month-expense').textContent =
|
||
'¥' + (res.stats.month_expense || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
document.getElementById('month-balance').textContent =
|
||
'¥' + (res.stats.month_net || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
}
|
||
} catch(e) {
|
||
console.error('加载统计失败:', e);
|
||
}
|
||
};
|
||
|
||
// 加载收支流水
|
||
const loadTransactions = async (page = 1) => {
|
||
try {
|
||
const params = new URLSearchParams();
|
||
const type = document.getElementById('trans-type')?.value;
|
||
const category = document.getElementById('trans-category')?.value;
|
||
const startDate = document.getElementById('start-date')?.value;
|
||
const endDate = document.getElementById('end-date')?.value;
|
||
const party = document.getElementById('related-party')?.value;
|
||
|
||
if(type) params.append('type', type);
|
||
if(category) params.append('category', category);
|
||
if(startDate) params.append('start_date', startDate);
|
||
if(endDate) params.append('end_date', endDate);
|
||
if(party) params.append('related_party', party);
|
||
params.append('page', page);
|
||
|
||
const res = await fetch('/api/finance/transactions?' + params, {
|
||
credentials: 'include'
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
const tbody = document.getElementById('transactions-tbody');
|
||
if(res.data.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:var(--text-secondary)">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = res.data.map(row => `
|
||
<tr>
|
||
<td>${row.date}</td>
|
||
<td><span class="badge ${row.type}">${row.type === 'income' ? '收入' : '支出'}</span></td>
|
||
<td>${row.category || '-'}</td>
|
||
<td>¥${(row.amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>${row.related_party || '-'}</td>
|
||
<td>${row.order_no || '-'}</td>
|
||
<td>${row.payment_method || '-'}</td>
|
||
<td><span class="badge ${row.status}">${row.status === 'confirmed' ? '已确认' : '待确认'}</span></td>
|
||
<td>${row.description || '-'}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('加载流水失败:', e);
|
||
}
|
||
};
|
||
|
||
// 整合显示状态
|
||
let isMergeMode = false;
|
||
|
||
// 切换整合/分开显示
|
||
const toggleMerge = () => {
|
||
isMergeMode = !isMergeMode;
|
||
const btn = document.getElementById('merge-btn');
|
||
const th = document.getElementById('rec-th-customer');
|
||
if(btn) {
|
||
if(isMergeMode) {
|
||
btn.textContent = '分开显示';
|
||
btn.style.background = '#6B7280';
|
||
if(th) th.textContent = '客户名称';
|
||
} else {
|
||
btn.textContent = '整合显示';
|
||
btn.style.background = '#3B82F6';
|
||
if(th) th.textContent = '客户 / 合同号';
|
||
}
|
||
}
|
||
loadCustomerRec();
|
||
};
|
||
|
||
// 加载客户对账
|
||
const loadCustomerRec = async () => {
|
||
try {
|
||
const params = new URLSearchParams();
|
||
const customer = document.getElementById('customer-name')?.value;
|
||
const period = document.getElementById('rec-period')?.value;
|
||
|
||
console.log('DEBUG: period value =', period);
|
||
|
||
if(customer) params.append('customer', customer);
|
||
if(period) {
|
||
params.append('period', period);
|
||
console.log('DEBUG: adding period param =', period);
|
||
}
|
||
if(isMergeMode) params.append('merge', 'true');
|
||
|
||
console.log('DEBUG: final URL =', '/api/finance/customer-reconciliation?' + params.toString());
|
||
|
||
const res = await fetch('/api/finance/customer-reconciliation?' + params, {
|
||
credentials: 'include'
|
||
}).then(r => r.json());
|
||
|
||
console.log('DEBUG: response period_label =', res.period_label);
|
||
|
||
if(res.ok) {
|
||
// 更新周期标签
|
||
const periodLabel = document.getElementById('rec-period-label');
|
||
if(periodLabel && res.period_label) {
|
||
const modeText = isMergeMode ? '(整合显示)' : '(分开显示)';
|
||
periodLabel.textContent = (period ? `数据周期:${res.period_label}` : '显示全部历史数据') + modeText;
|
||
}
|
||
|
||
// 更新统计卡片
|
||
const totalReceivable = res.data.reduce((sum, r) => sum + (r.total_income || 0), 0);
|
||
const totalPaid = res.data.reduce((sum, r) => sum + (r.total_paid || 0), 0);
|
||
const totalBalance = res.data.reduce((sum, r) => sum + (r.closing_balance || 0), 0);
|
||
|
||
// 本期客户数:按客户名称去重统计(同一客户多个合同号只算1个)
|
||
const uniqueCustomers = new Set(res.data.map(r => r.customer_name)).size;
|
||
|
||
document.getElementById('rec-customer-count').textContent = uniqueCustomers;
|
||
document.getElementById('rec-total-receivable').textContent =
|
||
'¥' + totalReceivable.toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
document.getElementById('rec-total-paid').textContent =
|
||
'¥' + totalPaid.toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
document.getElementById('rec-total-balance').textContent =
|
||
'¥' + totalBalance.toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
|
||
const tbody = document.getElementById('customer-rec-tbody');
|
||
if(res.data.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-secondary)">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = res.data.map(row => {
|
||
// 计算回款率
|
||
const payRate = row.total_income > 0
|
||
? ((row.total_paid / row.total_income) * 100).toFixed(1) + '%'
|
||
: '0%';
|
||
|
||
// 物料明细截断显示
|
||
const materials = row.materials
|
||
? (row.materials.length > 30 ? row.materials.substring(0, 30) + '...' : row.materials)
|
||
: '-';
|
||
|
||
// 最后交易日期
|
||
const lastDate = row.last_order_date || '-';
|
||
|
||
// 显示名称:整合模式只显示客户名,分开模式显示客户名+合同号+编辑图标
|
||
let displayName;
|
||
const editIcon = `<svg onclick="Finance.editCustomerRec('${row.contract_no}', '${row.customer_name}')" style="cursor:pointer;margin-left:8px;color:var(--primary);" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`;
|
||
if(isMergeMode) {
|
||
displayName = `<strong style="cursor:pointer;" onclick="Finance.editCustomerRec('${row.contract_no}', '${row.customer_name}')">${row.customer_name}</strong>${editIcon}`;
|
||
} else {
|
||
// 分开显示模式:同时显示客户名称和合同号,可点击编辑
|
||
if(row.customer_name && row.customer_name !== row.contract_no) {
|
||
displayName = `<strong style="cursor:pointer;" onclick="Finance.editCustomerRec('${row.contract_no}', '${row.customer_name}')">${row.customer_name}</strong>${editIcon}<br><small style="color:var(--text-secondary)">${row.contract_no}</small>`;
|
||
} else {
|
||
displayName = `<strong style="cursor:pointer;" onclick="Finance.editCustomerRec('${row.contract_no}', '${row.customer_name}')">${row.contract_no}</strong>${editIcon}`;
|
||
}
|
||
}
|
||
|
||
return `
|
||
<tr>
|
||
<td>${displayName}</td>
|
||
<td>${row.order_count || 0} 单</td>
|
||
<td title="${row.materials || ''}">${materials}</td>
|
||
<td class="income">¥${(row.total_income || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>¥${(row.total_paid || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td class="${row.closing_balance > 0 ? 'expense' : ''}">¥${(row.closing_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>${payRate}</td>
|
||
<td>${lastDate}</td>
|
||
</tr>
|
||
`}).join('');
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('加载客户对账失败:', e);
|
||
}
|
||
};
|
||
|
||
// 加载供应商对账
|
||
const loadSupplierRec = async () => {
|
||
try {
|
||
const params = new URLSearchParams();
|
||
const supplier = document.getElementById('supplier-name')?.value;
|
||
const period = document.getElementById('supplier-period')?.value;
|
||
|
||
if(supplier) params.append('supplier', supplier);
|
||
if(period) params.append('period', period);
|
||
|
||
const res = await fetch('/api/finance/supplier-reconciliation?' + params, {
|
||
credentials: 'include'
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
const tbody = document.getElementById('supplier-rec-tbody');
|
||
if(res.data.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-secondary)">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = res.data.map(row => `
|
||
<tr>
|
||
<td>${row.supplier_name}</td>
|
||
<td>${row.period_start} ~ ${row.period_end}</td>
|
||
<td>¥${(row.opening_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>¥${(row.total_expense || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>¥${(row.total_paid || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>¥${(row.closing_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td><span class="badge ${row.statement_status}">${row.statement_status === 'confirmed' ? '已确认' : '待确认'}</span></td>
|
||
<td><button onclick="Finance.viewSupplierStatement(${row.id})">查看</button></td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('加载供应商对账失败:', e);
|
||
}
|
||
};
|
||
|
||
// 加载发票统计
|
||
const loadInvoiceStats = async () => {
|
||
try {
|
||
const res = await fetch('/api/finance/invoices/stats', {
|
||
credentials: 'include'
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
document.getElementById('input-invoice-count').textContent =
|
||
(res.stats.input.count || 0) + ' 张';
|
||
document.getElementById('input-invoice-amount').textContent =
|
||
'¥' + (res.stats.input.total || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
document.getElementById('output-invoice-count').textContent =
|
||
(res.stats.output.count || 0) + ' 张';
|
||
document.getElementById('output-invoice-amount').textContent =
|
||
'¥' + (res.stats.output.total || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
}
|
||
} catch(e) {
|
||
console.error('加载发票统计失败:', e);
|
||
}
|
||
};
|
||
|
||
// 加载发票列表
|
||
const loadInvoices = async () => {
|
||
try {
|
||
const params = new URLSearchParams();
|
||
const type = document.getElementById('invoice-type')?.value;
|
||
const status = document.getElementById('invoice-status')?.value;
|
||
const start = document.getElementById('invoice-start')?.value;
|
||
const end = document.getElementById('invoice-end')?.value;
|
||
|
||
if(type) params.append('type', type);
|
||
if(status) params.append('status', status);
|
||
if(start) params.append('start_date', start);
|
||
if(end) params.append('end_date', end);
|
||
|
||
const res = await fetch('/api/finance/invoices?' + params, {
|
||
credentials: 'include'
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
const tbody = document.getElementById('invoices-tbody');
|
||
if(res.data.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:var(--text-secondary)">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = res.data.map(row => `
|
||
<tr>
|
||
<td>${row.invoice_type === 'input' ? '进项' : '销项'}</td>
|
||
<td>${row.invoice_no}</td>
|
||
<td>${row.issue_date}</td>
|
||
<td>¥${(row.amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>¥${(row.tax_amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>¥${(row.total_amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td>
|
||
<td>${row.related_party || '-'}</td>
|
||
<td>${row.order_no || '-'}</td>
|
||
<td><span class="badge ${row.status}">${row.status === 'normal' ? '正常' : '作废'}</span></td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('加载发票失败:', e);
|
||
}
|
||
};
|
||
|
||
// 加载统计报表
|
||
const loadStats = async () => {
|
||
try {
|
||
const days = document.getElementById('stats-range')?.value || 30;
|
||
|
||
const res = await fetch('/api/finance/business-stats?days=' + days, {
|
||
credentials: 'include'
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
document.getElementById('total-income').textContent =
|
||
'¥' + (res.stats.summary.total_income || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
document.getElementById('total-expense').textContent =
|
||
'¥' + (res.stats.summary.total_expense || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
|
||
const net = (res.stats.summary.total_income || 0) - (res.stats.summary.total_expense || 0);
|
||
document.getElementById('net-profit').textContent =
|
||
'¥' + net.toLocaleString('zh-CN', {minimumFractionDigits: 2});
|
||
|
||
const margin = res.stats.summary.total_income > 0
|
||
? (net / res.stats.summary.total_income * 100).toFixed(1)
|
||
: 0;
|
||
document.getElementById('profit-margin').textContent = margin + '%';
|
||
|
||
// 客户应收
|
||
const recTbody = document.getElementById('receivables-tbody');
|
||
if(res.stats.customer_receivables?.length > 0) {
|
||
recTbody.innerHTML = res.stats.customer_receivables.map(r => `
|
||
<tr><td>${r.related_party}</td><td>¥${(r.balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td></tr>
|
||
`).join('');
|
||
} else {
|
||
recTbody.innerHTML = '<tr><td colspan="2" style="text-align:center">暂无数据</td></tr>';
|
||
}
|
||
|
||
// 供应商应付
|
||
const payTbody = document.getElementById('payables-tbody');
|
||
if(res.stats.supplier_payables?.length > 0) {
|
||
payTbody.innerHTML = res.stats.supplier_payables.map(r => `
|
||
<tr><td>${r.related_party}</td><td>¥${(r.balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})}</td></tr>
|
||
`).join('');
|
||
} else {
|
||
payTbody.innerHTML = '<tr><td colspan="2" style="text-align:center">暂无数据</td></tr>';
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.error('加载统计失败:', e);
|
||
}
|
||
};
|
||
|
||
// ========== 模态框函数 ==========
|
||
|
||
const showAddTransactionModal = () => {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.id = 'transaction-modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<h3 style="margin-bottom:20px;">新增收支记录</h3>
|
||
<div class="form-group">
|
||
<label>类型</label>
|
||
<select id="trans-type-input">
|
||
<option value="income">收入</option>
|
||
<option value="expense">支出</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>类别</label>
|
||
<select id="trans-category-input">
|
||
<option value="货款">货款</option>
|
||
<option value="运费">运费</option>
|
||
<option value="工资">工资</option>
|
||
<option value="租金">租金</option>
|
||
<option value="税费">税费</option>
|
||
<option value="其他">其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>金额</label>
|
||
<input type="number" id="trans-amount" step="0.01" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>日期</label>
|
||
<input type="date" id="trans-date" value="${new Date().toISOString().slice(0, 10)}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>关联方</label>
|
||
<input type="text" id="trans-party" placeholder="客户/供应商名称">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>关联单号</label>
|
||
<input type="text" id="trans-order" placeholder="订单号">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>支付方式</label>
|
||
<select id="trans-payment">
|
||
<option value="transfer">银行转账</option>
|
||
<option value="cash">现金</option>
|
||
<option value="alipay">支付宝</option>
|
||
<option value="wechat">微信支付</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>备注</label>
|
||
<textarea id="trans-desc" rows="3"></textarea>
|
||
</div>
|
||
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:20px;">
|
||
<button class="secondary" onclick="document.getElementById('transaction-modal').remove()">取消</button>
|
||
<button onclick="Finance.saveTransaction()">保存</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
};
|
||
|
||
const saveTransaction = async () => {
|
||
try {
|
||
const data = {
|
||
type: document.getElementById('trans-type-input').value,
|
||
category: document.getElementById('trans-category-input').value,
|
||
amount: parseFloat(document.getElementById('trans-amount').value) || 0,
|
||
date: document.getElementById('trans-date').value,
|
||
related_party: document.getElementById('trans-party').value,
|
||
related_party_type: 'customer',
|
||
order_no: document.getElementById('trans-order').value,
|
||
payment_method: document.getElementById('trans-payment').value,
|
||
description: document.getElementById('trans-desc').value
|
||
};
|
||
|
||
const res = await fetch('/api/finance/transactions', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
credentials: 'include',
|
||
body: JSON.stringify(data)
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
document.getElementById('transaction-modal').remove();
|
||
loadTransactions();
|
||
loadTransactionStats();
|
||
} else {
|
||
alert('保存失败: ' + (res.error || '未知错误'));
|
||
}
|
||
} catch(e) {
|
||
console.error('保存失败:', e);
|
||
alert('保存失败');
|
||
}
|
||
};
|
||
|
||
const showAddInvoiceModal = () => {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.id = 'invoice-modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<h3 style="margin-bottom:20px;">新增发票</h3>
|
||
<div class="form-group">
|
||
<label>发票类型</label>
|
||
<select id="inv-type-input">
|
||
<option value="input">进项发票</option>
|
||
<option value="output">销项发票</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>发票号码</label>
|
||
<input type="text" id="inv-no" placeholder="发票号码">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>发票代码</label>
|
||
<input type="text" id="inv-code" placeholder="发票代码">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>开票日期</label>
|
||
<input type="date" id="inv-date" value="${new Date().toISOString().slice(0, 10)}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>金额(不含税)</label>
|
||
<input type="number" id="inv-amount" step="0.01" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>税额</label>
|
||
<input type="number" id="inv-tax" step="0.01" placeholder="0.00">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>关联方</label>
|
||
<input type="text" id="inv-party" placeholder="开票方/收票方">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>关联订单号</label>
|
||
<input type="text" id="inv-order" placeholder="订单号">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>备注</label>
|
||
<textarea id="inv-notes" rows="2"></textarea>
|
||
</div>
|
||
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:20px;">
|
||
<button class="secondary" onclick="document.getElementById('invoice-modal').remove()">取消</button>
|
||
<button onclick="Finance.saveInvoice()">保存</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
};
|
||
|
||
const saveInvoice = async () => {
|
||
try {
|
||
const amount = parseFloat(document.getElementById('inv-amount').value) || 0;
|
||
const tax = parseFloat(document.getElementById('inv-tax').value) || 0;
|
||
|
||
const data = {
|
||
invoice_type: document.getElementById('inv-type-input').value,
|
||
invoice_no: document.getElementById('inv-no').value,
|
||
invoice_code: document.getElementById('inv-code').value,
|
||
issue_date: document.getElementById('inv-date').value,
|
||
amount: amount,
|
||
tax_amount: tax,
|
||
total_amount: amount + tax,
|
||
related_party: document.getElementById('inv-party').value,
|
||
order_no: document.getElementById('inv-order').value,
|
||
notes: document.getElementById('inv-notes').value
|
||
};
|
||
|
||
const res = await fetch('/api/finance/invoices', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
credentials: 'include',
|
||
body: JSON.stringify(data)
|
||
}).then(r => r.json());
|
||
|
||
if(res.ok) {
|
||
document.getElementById('invoice-modal').remove();
|
||
loadInvoices();
|
||
loadInvoiceStats();
|
||
} else {
|
||
alert('保存失败: ' + (res.error || '未知错误'));
|
||
}
|
||
} catch(e) {
|
||
console.error('保存失败:', e);
|
||
alert('保存失败');
|
||
}
|
||
};
|
||
|
||
// 其他占位函数
|
||
const showGenerateStatementModal = () => alert('生成客户对账单功能开发中');
|
||
const showGenerateSupplierStatementModal = () => alert('生成供应商对账单功能开发中');
|
||
const viewStatement = (id) => alert('查看对账单 ' + id);
|
||
const viewSupplierStatement = (id) => alert('查看供应商对账单 ' + id);
|
||
|
||
return {
|
||
render,
|
||
init,
|
||
renderCustomerReconciliation,
|
||
renderSupplierReconciliation,
|
||
renderInvoice,
|
||
renderStats,
|
||
loadTransactions,
|
||
loadCustomerRec,
|
||
loadSupplierRec,
|
||
loadInvoices,
|
||
loadStats,
|
||
loadTransactionStats,
|
||
loadInvoiceStats,
|
||
// 重置模块状态
|
||
resetModule: () => {
|
||
currentSubModule = 'income-expense';
|
||
isMergeMode = false;
|
||
},
|
||
showAddTransactionModal,
|
||
saveTransaction,
|
||
showAddInvoiceModal,
|
||
saveInvoice,
|
||
showGenerateStatementModal,
|
||
showGenerateSupplierStatementModal,
|
||
viewStatement,
|
||
viewSupplierStatement,
|
||
toggleMerge,
|
||
editCustomerRec: (contractNo, customerName) => {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-overlay';
|
||
modal.id = 'edit-rec-modal';
|
||
modal.innerHTML = `
|
||
<div class="modal-content">
|
||
<h3 style="margin-bottom:20px;">编辑客户信息</h3>
|
||
<div class="form-group">
|
||
<label>合同号</label>
|
||
<input type="text" id="edit-contract-no" value="${contractNo}" disabled style="background:var(--bg);">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>客户名称</label>
|
||
<input type="text" id="edit-customer-name" value="${customerName}" placeholder="请输入客户名称">
|
||
</div>
|
||
<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:20px;">
|
||
<button class="secondary" onclick="document.getElementById('edit-rec-modal').remove()">取消</button>
|
||
<button onclick="Finance.saveCustomerRecEdit()">保存</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
},
|
||
saveCustomerRecEdit: async () => {
|
||
const contractNo = document.getElementById('edit-contract-no').value;
|
||
const customerName = document.getElementById('edit-customer-name').value;
|
||
|
||
if(!customerName.trim()) {
|
||
alert('请输入客户名称');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 调用后端API保存
|
||
const res = await fetch('/api/finance/customer-reconciliation/update', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
order_no: contractNo,
|
||
customer_name: customerName.trim()
|
||
})
|
||
}).then(r => r.json());
|
||
|
||
document.getElementById('edit-rec-modal').remove();
|
||
|
||
if(res.ok) {
|
||
// 刷新列表
|
||
if(window.Finance && window.Finance.loadCustomerRec) {
|
||
window.Finance.loadCustomerRec();
|
||
}
|
||
if(window.API && window.API.toast) {
|
||
window.API.toast('客户名称已更新');
|
||
}
|
||
} else {
|
||
alert('保存失败:' + (res.error || '未知错误'));
|
||
}
|
||
} catch(e) {
|
||
console.error('保存失败:', e);
|
||
alert('保存失败,请检查网络连接');
|
||
}
|
||
},
|
||
clearPeriodFilter: () => {
|
||
document.getElementById('rec-period').value = '';
|
||
if(window.Finance && window.Finance.loadCustomerRec) {
|
||
window.Finance.loadCustomerRec();
|
||
}
|
||
}
|
||
};
|
||
})();
|
||
|
||
// 注册路由
|
||
Router.register('/finance', async () => {
|
||
// 重置为收支管理标签
|
||
if(window.Finance && window.Finance.resetModule) {
|
||
window.Finance.resetModule();
|
||
}
|
||
const html = window.Finance.render();
|
||
setTimeout(() => window.Finance.init(), 0);
|
||
return html;
|
||
});
|
||
|
||
Router.register('/finance/customer-rec', async () => {
|
||
// 设置当前模块为客户对账,避免init加载错误的数据
|
||
isMergeMode = false;
|
||
const html = window.Finance.render();
|
||
setTimeout(() => {
|
||
window.Finance.init();
|
||
// 手动切换到客户对账标签并加载数据
|
||
currentSubModule = 'customer-rec';
|
||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||
document.querySelector('[data-tab="customer-rec"]')?.classList.add('active');
|
||
document.getElementById('finance-content').innerHTML = window.Finance.renderCustomerReconciliation();
|
||
setTimeout(() => window.Finance.loadCustomerRec(), 0);
|
||
}, 0);
|
||
return html;
|
||
});
|
||
|
||
Router.register('/finance/supplier-rec', async () => {
|
||
isMergeMode = false;
|
||
const html = window.Finance.render();
|
||
setTimeout(() => {
|
||
window.Finance.init();
|
||
currentSubModule = 'supplier-rec';
|
||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||
document.querySelector('[data-tab="supplier-rec"]')?.classList.add('active');
|
||
document.getElementById('finance-content').innerHTML = window.Finance.renderSupplierReconciliation();
|
||
setTimeout(() => window.Finance.loadSupplierRec(), 0);
|
||
}, 0);
|
||
return html;
|
||
});
|
||
|
||
Router.register('/finance/invoice', async () => {
|
||
isMergeMode = false;
|
||
const html = window.Finance.render();
|
||
setTimeout(() => {
|
||
window.Finance.init();
|
||
currentSubModule = 'invoice';
|
||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||
document.querySelector('[data-tab="invoice"]')?.classList.add('active');
|
||
document.getElementById('finance-content').innerHTML = window.Finance.renderInvoice();
|
||
setTimeout(() => {
|
||
window.Finance.loadInvoiceStats();
|
||
window.Finance.loadInvoices();
|
||
}, 0);
|
||
}, 0);
|
||
return html;
|
||
});
|
||
|
||
Router.register('/finance/stats', async () => {
|
||
isMergeMode = false;
|
||
const html = window.Finance.render();
|
||
setTimeout(() => {
|
||
window.Finance.init();
|
||
currentSubModule = 'stats';
|
||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||
document.querySelector('[data-tab="stats"]')?.classList.add('active');
|
||
document.getElementById('finance-content').innerHTML = window.Finance.renderStats();
|
||
setTimeout(() => window.Finance.loadStats(), 0);
|
||
}, 0);
|
||
return html;
|
||
});
|