/** * 财务管理模块组件 * 包含:收支管理、客户对账、发票管理、供应商对账 */ window.Finance = (() => { // 当前子模块 let currentSubModule = 'income-expense'; // 渲染主框架 const render = () => { return `
收支管理
客户对账
供应商对账
发票管理
统计报表
${renderSubModule(currentSubModule)}
`; }; // 渲染子模块内容 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 `
本月收入
¥0.00
本月支出
¥0.00
本月结余
¥0.00
待确认
0
日期 类型 类别 金额 关联方 关联单号 支付方式 状态 备注
加载中...
`; }; // 客户对账 const renderCustomerReconciliation = () => { return `
本期客户数
0
总应收金额
¥0.00
总已收金额
¥0.00
总未收余额
¥0.00
显示全部历史数据
客户 / 合同号 订单数 物料明细 累计应收 累计已收 未收余额 回款率 最后交易
暂无数据
`; }; // 供应商对账 const renderSupplierReconciliation = () => { return `
供应商名称 对账期间 期初应付 本期应付 本期已付 期末应付 状态 操作
暂无数据
`; }; // 发票管理 const renderInvoice = () => { return `
本月进项发票
0 张
进项金额
¥0.00
本月销项发票
0 张
销项金额
¥0.00
发票类型 发票号码 开票日期 金额 税额 价税合计 关联方 关联单号 状态
暂无数据
`; }; // 统计报表 const renderStats = () => { return `
总收入
¥0.00
总支出
¥0.00
净利润
¥0.00
毛利率
0%

收支趋势

客户应收TOP10

客户应收金额

供应商应付TOP10

供应商应付金额
`; }; // 初始化模块 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 = '暂无数据'; } else { tbody.innerHTML = res.data.map(row => ` ${row.date} ${row.type === 'income' ? '收入' : '支出'} ${row.category || '-'} ¥${(row.amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ${row.related_party || '-'} ${row.order_no || '-'} ${row.payment_method || '-'} ${row.status === 'confirmed' ? '已确认' : '待确认'} ${row.description || '-'} `).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 = '暂无数据'; } 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 = ``; if(isMergeMode) { displayName = `${row.customer_name}${editIcon}`; } else { // 分开显示模式:同时显示客户名称和合同号,可点击编辑 if(row.customer_name && row.customer_name !== row.contract_no) { displayName = `${row.customer_name}${editIcon}
${row.contract_no}`; } else { displayName = `${row.contract_no}${editIcon}`; } } return ` ${displayName} ${row.order_count || 0} 单 ${materials} ¥${(row.total_income || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ¥${(row.total_paid || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ¥${(row.closing_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ${payRate} ${lastDate} `}).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 = '暂无数据'; } else { tbody.innerHTML = res.data.map(row => ` ${row.supplier_name} ${row.period_start} ~ ${row.period_end} ¥${(row.opening_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ¥${(row.total_expense || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ¥${(row.total_paid || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ¥${(row.closing_balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ${row.statement_status === 'confirmed' ? '已确认' : '待确认'} `).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 = '暂无数据'; } else { tbody.innerHTML = res.data.map(row => ` ${row.invoice_type === 'input' ? '进项' : '销项'} ${row.invoice_no} ${row.issue_date} ¥${(row.amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ¥${(row.tax_amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ¥${(row.total_amount || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} ${row.related_party || '-'} ${row.order_no || '-'} ${row.status === 'normal' ? '正常' : '作废'} `).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 => ` ${r.related_party}¥${(r.balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} `).join(''); } else { recTbody.innerHTML = '暂无数据'; } // 供应商应付 const payTbody = document.getElementById('payables-tbody'); if(res.stats.supplier_payables?.length > 0) { payTbody.innerHTML = res.stats.supplier_payables.map(r => ` ${r.related_party}¥${(r.balance || 0).toLocaleString('zh-CN', {minimumFractionDigits: 2})} `).join(''); } else { payTbody.innerHTML = '暂无数据'; } } } catch(e) { console.error('加载统计失败:', e); } }; // ========== 模态框函数 ========== const showAddTransactionModal = () => { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.id = 'transaction-modal'; modal.innerHTML = ` `; 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 = ` `; 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 = ` `; 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; });