/**
* 财务管理模块组件
* 包含:收支管理、客户对账、发票管理、供应商对账
*/
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 `
`;
};
// 客户对账
const renderCustomerReconciliation = () => {
return `
`;
};
// 供应商对账
const renderSupplierReconciliation = () => {
return `
`;
};
// 发票管理
const renderInvoice = () => {
return `
`;
};
// 统计报表
const renderStats = () => {
return `
`;
};
// 初始化模块
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;
});