好友
阅读权限10
听众
最后登录1970-1-1
|
楼主|
DARONG
发表于 2026-3-17 11:15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开票独立扣费 · 项目管理 (元)</title>
<style>
* { box-sizing: border-box; font-family: system-ui, 'Segoe UI', Roboto, sans-serif; }
body { background: #ecf3f8; margin: 0; padding: 20px; display: flex; justify-content: center; }
.container { max-width: 1400px; width: 100%; }
h1 { color: #0b2b4f; border-bottom: 3px solid #2c7da0; padding-bottom: 10px; display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
h1 small { font-size: 0.9rem; font-weight: 400; color: #2f5c77; margin-left: auto; }
/* 全局统计卡片 */
.global-stats { background: #e2f0f9; border-radius: 40px; padding: 15px 30px; margin: 0 0 25px 0; display: flex; gap: 50px; align-items: center; border: 2px solid #b3d3e8; }
.stat-item { display: flex; align-items: baseline; gap: 8px; }
.stat-label { font-size: 1rem; font-weight: 600; color: #12455f; }
.stat-value { font-size: 1.8rem; font-weight: 700; color: #0b2b4f; }
.toolbar { background: white; border-radius: 30px; padding: 25px; margin-bottom: 30px; box-shadow: 0 5px 15px rgba(0,40,70,0.1); border: 1px solid #c7ddec; }
.add-project-panel { display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-end; }
.input-group { display: flex; flex-direction: column; min-width: 150px; }
.input-group label { font-size: 0.8rem; font-weight: 600; color: #1a4d6b; margin-bottom: 5px; }
.input-group input { background: white; border: 2px solid #c1dcec; border-radius: 40px; padding: 10px 16px; font-size: 0.95rem; width: 100%; }
.btn { background: #2c7da0; border: none; color: white; font-weight: 600; padding: 10px 28px; border-radius: 40px; cursor: pointer; border: 2px solid transparent; }
.btn-outline { background: white; border: 2px solid #2c7da0; color: #1d5a7a; }
.btn-sm { padding: 6px 16px; font-size: 0.9rem; border-radius: 30px; }
.btn-danger { background: #c13b3b; color: white; border: none; }
.search-section { margin: 20px 0 0; display: flex; align-items: center; gap: 15px; flex-wrap: wrap; }
.search-box { flex: 1; min-width: 260px; }
.search-box input { width: 100%; padding: 12px 20px; border-radius: 50px; border: 2px solid #b3d3e8; font-size: 1rem; }
.project-grid { display: flex; flex-direction: column; gap: 30px; }
.project-card { background: white; border-radius: 36px; border: 1px solid #cfdeec; padding: 20px 20px 25px; box-shadow: 0 8px 18px rgba(0,35,65,0.05); }
.project-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; background: #e2f0f9; padding: 15px 25px; border-radius: 60px; margin-bottom: 20px; }
.project-title { display: flex; align-items: baseline; gap: 25px; flex-wrap: wrap; }
.project-name { font-size: 1.6rem; font-weight: 700; color: #0b2b4f; }
.project-manager { background: #95bcd6; color: #0d3148; padding: 5px 18px; border-radius: 40px; font-weight: 500; }
.contract-amount { background: #12455f; color: white; padding: 5px 22px; border-radius: 40px; font-weight: 600; }
.delete-project-btn { background: #e1a1a1; color: #631e1e; border: none; border-radius: 40px; padding: 8px 20px; font-weight: 600; cursor: pointer; }
.invoice-toolbar { display: flex; justify-content: flex-end; margin: 10px 0 15px; }
.invoice-table { width: 100%; border-collapse: collapse; background: #f9fcff; border-radius: 20px; overflow: hidden; margin: 10px 0; }
.invoice-table th { background: #dbeaf3; color: #103a51; padding: 12px 5px; font-size: 0.9rem; text-align: center; }
.invoice-table td { padding: 8px 5px; text-align: center; border-bottom: 1px solid #d3e2ed; vertical-align: top; }
.invoice-main-row { background: #f3f9ff; }
.date-input, .amount-input { width: 130px; padding: 8px; border: 2px solid #c1dcec; border-radius: 40px; text-align: center; }
.deductions-container { background: #ffffff; border-radius: 16px; padding: 10px; margin: 5px 0; border: 1px dashed #9bc2db; }
.deduction-item { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; background: #f7fbfe; padding: 6px 12px; border-radius: 50px; }
.deduction-name { width: 130px; padding: 6px 10px; border: 2px solid #c1dcec; border-radius: 30px; }
.deduction-name-dropdown { background: white; border: 2px solid #c1dcec; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 0.8rem; display: inline-flex; align-items: center; justify-content: center; color: #12455f; margin-left: -8px; }
.deduction-type { width: 100px; padding: 6px; border: 2px solid #c1dcec; border-radius: 30px; background: white; }
.deduction-value { width: 100px; padding: 6px 10px; border: 2px solid #c1dcec; border-radius: 30px; }
.deduction-amount-display { background: #e6f2f9; padding: 4px 12px; border-radius: 30px; font-weight: 600; color: #12455f; min-width: 80px; text-align: center; font-size: 0.9rem; }
.remove-deduction-btn { background: #f0f0f0; border: none; border-radius: 50%; width: 28px; height: 28px; font-weight: bold; color: #b33; cursor: pointer; }
.add-deduction-btn { background: white; border: 2px dashed #2c7da0; border-radius: 40px; padding: 6px 18px; font-size: 0.85rem; cursor: pointer; margin-top: 6px; }
.invoice-paid { font-weight: 700; color: #0f6b3a; background: #def1e6; padding: 4px 12px; border-radius: 40px; display: inline-block; margin-top: 6px; }
/* 收款模块样式 */
.receipt-section { margin-top: 30px; border-top: 3px solid #2c7da0; padding-top: 20px; }
.receipt-toolbar { display: flex; justify-content: flex-end; gap: 10px; margin: 10px 0 15px; }
.receipt-table { width: 100%; border-collapse: collapse; background: #f0f7fd; border-radius: 20px; overflow: hidden; }
.receipt-table th { background: #c1d9ec; color: #0d3a51; padding: 12px 5px; font-size: 0.9rem; text-align: center; }
.receipt-table td { padding: 8px 5px; text-align: center; border-bottom: 1px solid #b3cde0; vertical-align: top; }
.receipt-main-row { background: #e8f1fa; }
.receipt-detail-row { background: #f9fcff; }
.receipt-detail-container { padding: 10px 20px; background: #ffffff; border-radius: 20px; border: 1px solid #b3d3e8; margin: 5px 0; }
.receipt-detail-item { display: flex; align-items: center; gap: 15px; flex-wrap: wrap; padding: 5px 0; border-bottom: 1px dashed #b0cde0; }
.receipt-detail-item:last-child { border-bottom: none; }
.receipt-date-input, .receipt-amount-input, .receipt-expense-input { width: 110px; padding: 6px 8px; border: 2px solid #c1dcec; border-radius: 30px; }
.receipt-memo-input { width: 140px; padding: 6px 8px; border: 2px solid #c1dcec; border-radius: 30px; }
.receipt-type-select { width: 100px; padding: 6px; border: 2px solid #c1dcec; border-radius: 30px; background: white; }
.receipt-table tfoot tr { background: #d4e7f5; font-weight: bold; }
.receipt-table tfoot td { padding: 10px 5px; }
.project-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; padding-top: 15px; border-top: 2px dashed #aac7df; }
.total-paid { font-size: 1.2rem; font-weight: 700; background: #def1e6; padding: 8px 25px; border-radius: 40px; color: #0f6b3a; }
.total-invoiced { background: #fde9d0; padding: 8px 20px; border-radius: 40px; color: #a45d1a; font-weight: 500; }
.empty-projects { background: white; border-radius: 40px; padding: 60px; text-align: center; color: #5b7f96; font-size: 1.2rem; }
.dropdown-menu {
position: absolute;
background: white;
border: 2px solid #c1dcec;
border-radius: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
z-index: 1000;
display: none;
min-width: 150px;
padding: 8px 0;
}
.dropdown-menu div {
padding: 8px 16px;
cursor: pointer;
color: #1a4d6b;
}
.dropdown-menu div:hover {
background: #e2f0f9;
}
</style>
</head>
<body>
<div id="app" class="container"></div>
<!-- 全局科目下拉菜单 -->
<div id="deductionDropdown" class="dropdown-menu">
<div data-value="管理费">管理费</div>
<div data-value="印花税">印花税</div>
<div data-value="企业所得税">企业所得税</div>
<div data-value="其他税金合计">其他税金合计</div>
</div>
<script>
(function() {
// 辅助ID
function genId() { return Date.now() + '-' + Math.random().toString(36).substr(2, 9); }
function todayStr() { return new Date().toISOString().split('T')[0]; }
// 固定科目名称列表(用于预设选项)
const FIXED_NAMES = ['管理费', '印花税', '企业所得税', '其他税金合计'];
// ---------- 初始数据 (增加 receipts 数组,增加 type, memo, manualExpense 字段) ----------
let projects = [
{
id: genId(),
name: '项目A',
manager: '张三',
contractAmount: 1000000.00,
invoices: [
{
id: genId(), date: '2025-03-10', amount: 333300.00,
deductions: [
{ id: genId(), name: '管理费', type: 'percent', value: 2 },
{ id: genId(), name: '印花税', type: 'percent', value: 1 },
{ id: genId(), name: '企业所得税', type: 'percent', value: 1 },
{ id: genId(), name: '其他税金合计', type: 'percent', value: 0.5 }
]
},
{
id: genId(), date: '2025-04-15', amount: 333300.00,
deductions: [
{ id: genId(), name: '管理费', type: 'percent', value: 2 },
{ id: genId(), name: '印花税', type: 'percent', value: 1 },
{ id: genId(), name: '企业所得税', type: 'percent', value: 1 },
{ id: genId(), name: '其他税金合计', type: 'percent', value: 0.5 }
]
},
{
id: genId(), date: '2025-05-20', amount: 333400.00,
deductions: [
{ id: genId(), name: '管理费', type: 'percent', value: 2 },
{ id: genId(), name: '印花税', type: 'percent', value: 1 },
{ id: genId(), name: '企业所得税', type: 'percent', value: 1 },
{ id: genId(), name: '其他税金合计', type: 'percent', value: 0.5 }
]
}
],
receipts: [
{ id: genId(), date: '2025-03-25', amount: 500000.00, type: 'normal', memo: '' },
{ id: genId(), date: '2025-06-01', amount: 600000.00, type: 'special', memo: '预付款', manualExpense: 50000 }
]
},
{
id: genId(),
name: '项目B',
manager: '李四',
contractAmount: 2500000.00,
invoices: [
{
id: genId(), date: '2025-03-18', amount: 625000.00,
deductions: [
{ id: genId(), name: '管理费', type: 'percent', value: 2 },
{ id: genId(), name: '印花税', type: 'percent', value: 1 },
{ id: genId(), name: '企业所得税', type: 'percent', value: 1 },
{ id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
]
},
{
id: genId(), date: '2025-04-22', amount: 625000.00,
deductions: [
{ id: genId(), name: '管理费', type: 'percent', value: 2 },
{ id: genId(), name: '印花税', type: 'percent', value: 1 },
{ id: genId(), name: '企业所得税', type: 'percent', value: 1 },
{ id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
]
},
{
id: genId(), date: '2025-05-30', amount: 625000.00,
deductions: [
{ id: genId(), name: '管理费', type: 'percent', value: 2 },
{ id: genId(), name: '印花税', type: 'percent', value: 1 },
{ id: genId(), name: '企业所得税', type: 'percent', value: 1 },
{ id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
]
},
{
id: genId(), date: '2025-06-12', amount: 625000.00,
deductions: [
{ id: genId(), name: '管理费', type: 'percent', value: 2 },
{ id: genId(), name: '印花税', type: 'percent', value: 1 },
{ id: genId(), name: '企业所得税', type: 'percent', value: 1 },
{ id: genId(), name: '其他税金合计', type: 'fixed', value: 2000 }
]
}
],
receipts: [
{ id: genId(), date: '2025-04-01', amount: 1000000.00, type: 'normal', memo: '' },
{ id: genId(), date: '2025-07-01', amount: 1200000.00, type: 'special', memo: '质保金', manualExpense: 30000 }
]
}
];
let searchKeyword = '';
const app = document.getElementById('app');
const dropdown = document.getElementById('deductionDropdown');
let activeDeductionInput = null; // 当前点击下拉按钮对应的输入框
// ---------- 工具函数:计算发票的扣费总额和实发 ----------
function calculateInvoice(invoice) {
const amount = invoice.amount || 0;
let totalDeduction = 0;
(invoice.deductions || []).forEach(d => {
if (d.type === 'percent') {
totalDeduction += amount * (d.value / 100);
} else { // fixed
totalDeduction += d.value;
}
});
const paid = amount - totalDeduction;
return { totalDeduction, paid };
}
// 计算指定开票的管理费、印花税、企业所得税三项合计(根据名称匹配)
function getThreeFeesTotal(invoice) {
const amount = invoice.amount || 0;
let total = 0;
(invoice.deductions || []).forEach(d => {
if (d.name === '管理费' || d.name === '印花税' || d.name === '企业所得税') {
if (d.type === 'percent') {
total += amount * (d.value / 100);
} else {
total += d.value;
}
}
});
return total;
}
// 获取开票中这三项费用的明细(用于显示)
function getThreeFeesDetail(invoice) {
const amount = invoice.amount || 0;
let mgr = 0, stamp = 0, tax = 0;
(invoice.deductions || []).forEach(d => {
const val = d.type === 'percent' ? amount * (d.value / 100) : d.value;
if (d.name === '管理费') mgr = val;
else if (d.name === '印花税') stamp = val;
else if (d.name === '企业所得税') tax = val;
});
return { mgr, stamp, tax, total: mgr + stamp + tax };
}
// ---------- 核心:计算每个收款的支出明细(普通收款自动扣除开票费用,特殊收款使用手动支出)----------
function calculateReceiptsDeductions(project) {
// 复制开票列表,并添加费用字段
let invoices = project.invoices.map(inv => ({
...inv,
threeTotal: getThreeFeesTotal(inv),
dateObj: new Date(inv.date),
used: false // 标记是否已被某个普通收款扣除
})).sort((a, b) => a.dateObj - b.dateObj); // 按开票日期排序
// 复制收款列表,按日期排序(特殊收款不参与扣除,但需要保留顺序用于显示)
let receipts = project.receipts.map(rec => {
const newRec = {
...rec,
dateObj: new Date(rec.date),
expenseDetails: [],
expenseTotal: 0,
payable: 0
};
if (rec.type === 'special') {
// 特殊收款:使用手动输入的支出
newRec.expenseTotal = rec.manualExpense || 0;
newRec.payable = (rec.amount || 0) - newRec.expenseTotal;
// 明细留空,但可以加一条备注说明
}
return newRec;
}).sort((a, b) => a.dateObj - b.dateObj);
// 遍历每个普通收款(按日期顺序)进行费用扣除
receipts.filter(r => r.type === 'normal').forEach(rec => {
let expenseTotal = 0;
let details = [];
// 找出所有未使用且开票日期 <= 收款日期的开票
invoices.forEach(inv => {
if (!inv.used && inv.dateObj <= rec.dateObj) {
const detail = getThreeFeesDetail(inv);
expenseTotal += detail.total;
details.push({
invoiceId: inv.id,
invoiceDate: inv.date,
mgr: detail.mgr,
stamp: detail.stamp,
tax: detail.tax,
total: detail.total
});
inv.used = true; // 标记已使用
}
});
rec.expenseDetails = details;
rec.expenseTotal = expenseTotal;
rec.payable = (rec.amount || 0) - expenseTotal;
});
return receipts;
}
// ---------- 全局函数:为指定项目添加新发票 ----------
window.addInvoice = function(projectId) {
const proj = projects.find(p => p.id === projectId);
if (proj) {
proj.invoices.push({
id: genId(),
date: todayStr(),
amount: 0,
deductions: []
});
render();
}
};
// 为指定项目添加新收款(默认普通收款)
window.addReceipt = function(projectId, type = 'normal') {
const proj = projects.find(p => p.id === projectId);
if (proj) {
const newReceipt = {
id: genId(),
date: todayStr(),
amount: 0,
type: type,
memo: ''
};
if (type === 'special') {
newReceipt.manualExpense = 0;
}
proj.receipts.push(newReceipt);
render();
}
};
// 关闭下拉菜单
function closeDropdown() {
dropdown.style.display = 'none';
activeDeductionInput = null;
}
// 显示下拉菜单
function showDropdown(btn, input) {
const rect = btn.getBoundingClientRect();
dropdown.style.display = 'block';
dropdown.style.top = rect.bottom + window.scrollY + 'px';
dropdown.style.left = rect.left + window.scrollX + 'px';
activeDeductionInput = input;
}
// ---------- 全量渲染 ----------
function render() {
// 计算所有项目的全局统计
let globalTotalInvoiced = 0;
let globalTotalPaid = 0;
projects.forEach(proj => {
proj.invoices.forEach(inv => {
globalTotalInvoiced += inv.amount || 0;
const { paid } = calculateInvoice(inv);
globalTotalPaid += paid;
});
});
const filtered = projects.filter(p =>
p.name.toLowerCase().includes(searchKeyword.toLowerCase()) ||
(p.manager && p.manager.toLowerCase().includes(searchKeyword.toLowerCase()))
);
let html = `
<h1>
📁 开票独立扣费 · 项目管理 <small>单位:元 (每张发票可自定扣费)</small>
</h1>
<!-- 全局统计卡片 -->
<div class="global-stats">
<div class="stat-item">
<span class="stat-label">📊 所有项目开票合计</span>
<span class="stat-value">${globalTotalInvoiced.toFixed(2)} 元</span>
</div>
<div class="stat-item">
<span class="stat-label">🧑‍🏭 总实发工资</span>
<span class="stat-value">${globalTotalPaid.toFixed(2)} 元</span>
</div>
</div>
<div class="toolbar">
<div class="add-project-panel">
<div class="input-group">
<label>项目名称</label>
<input type="text" id="newProjName" placeholder="例: 项目C" value="项目C">
</div>
<div class="input-group">
<label>负责人</label>
<input type="text" id="newProjManager" placeholder="姓名" value="王五">
</div>
<div class="input-group">
<label>合同金额 (元)</label>
<input type="number" id="newProjAmount" step="0.01" min="0" value="800000.00">
</div>
<button class="btn" id="addProjectBtn">➕ 添加项目</button>
<button class="btn-outline" id="downloadDataBtn" style="margin-left:auto;">📥 下载 CSV (明细)</button>
</div>
<div class="search-section">
<div class="search-box">
<input type="text" id="searchInput" placeholder="🔍 按项目名称或负责人搜索..." value="${searchKeyword}">
</div>
<button class="btn-sm btn-outline" id="clearSearchBtn">清除</button>
</div>
</div>
<div class="project-grid" id="projectList">
`;
if (filtered.length === 0) {
html += `<div class="empty-projects">😶 没有找到匹配的项目</div>`;
} else {
filtered.forEach(proj => {
const { id, name, manager, contractAmount, invoices } = proj;
// 项目开票统计
let totalInvoiced = 0, totalPaid = 0;
invoices.forEach(inv => {
totalInvoiced += inv.amount || 0;
const { paid } = calculateInvoice(inv);
totalPaid += paid;
});
// 开票表格主体
let invoiceRows = '';
invoices.forEach((inv, idx) => {
const amount = inv.amount || 0;
const { paid } = calculateInvoice(inv);
invoiceRows += `
<tr class="invoice-main-row" data-invoice-id="${inv.id}">
<td>${idx+1}</td>
<td><input type="date" class="date-input" data-project-id="${id}" data-invoice-id="${inv.id}" value="${inv.date}"></td>
<td><input type="number" step="0.01" min="0" class="amount-input" data-project-id="${id}" data-invoice-id="${inv.id}" value="${amount.toFixed(2)}"></td>
<td>
<button class="btn-sm btn-danger delete-invoice-btn" data-project-id="${id}" data-invoice-id="${inv.id}">✕ 删除</button>
</td>
</tr>
`;
invoiceRows += `<tr><td colspan="4" style="padding: 0 10px 15px 10px;">`;
invoiceRows += `<div class="deductions-container" data-invoice-id="${inv.id}">`;
(inv.deductions || []).forEach(ded => {
let dedAmount = 0;
if (ded.type === 'percent') {
dedAmount = amount * (ded.value / 100);
} else {
dedAmount = ded.value;
}
const unit = ded.type === 'percent' ? '%' : '元';
invoiceRows += `
<div class="deduction-item" data-deduction-id="${ded.id}">
<input type="text" class="deduction-name" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}" value="${ded.name}" placeholder="科目">
<button class="deduction-name-dropdown" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}">▼</button>
<select class="deduction-type" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}">
<option value="percent" ${ded.type==='percent'?'selected':''}>百分比(%)</option>
<option value="fixed" ${ded.type==='fixed'?'selected':''}>固定金额(元)</option>
</select>
<input type="number" step="0.01" min="0" class="deduction-value" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}" value="${ded.value}">
<span class="deduction-unit">${unit}</span>
<span class="deduction-amount-display">${dedAmount.toFixed(2)} 元</span>
<button class="remove-deduction-btn" data-project-id="${id}" data-invoice-id="${inv.id}" data-deduction-id="${ded.id}">✕</button>
</div>
`;
});
invoiceRows += `
<button class="add-deduction-btn" data-project-id="${id}" data-invoice-id="${inv.id}">➕ 添加扣费科目</button>
<div class="invoice-paid">公司人工费工资单: ${paid.toFixed(2)} 元</div>
`;
invoiceRows += `</div></td></tr>`;
});
// 计算收款的支出明细(包括普通和特殊)
const receiptsWithDetails = calculateReceiptsDeductions(proj);
// 收款合计
let totalReceiptAmount = 0, totalExpense = 0, totalPayable = 0;
receiptsWithDetails.forEach(rec => {
totalReceiptAmount += rec.amount || 0;
totalExpense += rec.expenseTotal;
totalPayable += rec.payable;
});
// 收款表格主体
let receiptRows = '';
receiptsWithDetails.forEach((rec, idx) => {
// 主行
const isSpecial = rec.type === 'special';
const expenseDisplay = isSpecial ?
`<input type="number" step="0.01" min="0" class="receipt-expense-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.expenseTotal.toFixed(2)}">` :
`${rec.expenseTotal.toFixed(2)} 元`;
const memoDisplay = isSpecial ?
`<input type="text" class="receipt-memo-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.memo || ''}" placeholder="备注">` :
'';
receiptRows += `
<tr class="receipt-main-row" data-receipt-id="${rec.id}">
<td>${idx+1}</td>
<td><input type="date" class="receipt-date-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.date}"></td>
<td><input type="number" step="0.01" min="0" class="receipt-amount-input" data-project-id="${id}" data-receipt-id="${rec.id}" value="${rec.amount.toFixed(2)}"></td>
<td>
<select class="receipt-type-select" data-project-id="${id}" data-receipt-id="${rec.id}">
<option value="normal" ${rec.type==='normal'?'selected':''}>普通</option>
<option value="special" ${rec.type==='special'?'selected':''}>特殊</option>
</select>
</td>
<td>${expenseDisplay}</td>
<td>${rec.payable.toFixed(2)} 元</td>
<td>${memoDisplay}</td>
<td><button class="btn-sm btn-danger delete-receipt-btn" data-project-id="${id}" data-receipt-id="${rec.id}">✕ 删除</button></td>
</tr>
`;
// 普通收款的明细行
if (rec.type === 'normal' && rec.expenseDetails.length > 0) {
receiptRows += `<tr class="receipt-detail-row"><td colspan="8" style="padding: 10px 20px;">`;
receiptRows += `<div class="receipt-detail-container">`;
rec.expenseDetails.forEach(detail => {
receiptRows += `
<div class="receipt-detail-item">
<span>📅 ${detail.invoiceDate}</span>
<span>管理费: ${detail.mgr.toFixed(2)}</span>
<span>印花税: ${detail.stamp.toFixed(2)}</span>
<span>企业所得税: ${detail.tax.toFixed(2)}</span>
<span>小计: ${detail.total.toFixed(2)}</span>
</div>
`;
});
receiptRows += `</div>`;
receiptRows += `</td></tr>`;
} else if (rec.type === 'normal' && rec.expenseDetails.length === 0) {
receiptRows += `<tr class="receipt-detail-row"><td colspan="8" style="padding: 5px 20px; color: #888;">本次收款无可扣除费用</td></tr>`;
}
// 特殊收款没有明细行,但可以显示备注(已在主行显示)
});
// 如果没有收款记录,显示一条消息
if (receiptsWithDetails.length === 0) {
receiptRows = `<tr><td colspan="8" style="text-align:center; padding:20px;">暂无收款记录</td></tr>`;
}
// 合计行
const totalRow = `
<tfoot>
<tr>
<td colspan="2">合计</td>
<td>${totalReceiptAmount.toFixed(2)} 元</td>
<td></td>
<td>${totalExpense.toFixed(2)} 元</td>
<td>${totalPayable.toFixed(2)} 元</td>
<td colspan="2"></td>
</tr>
</tfoot>
`;
html += `
<div class="project-card" data-project-id="${id}">
<div class="project-header">
<div class="project-title">
<span class="project-name">${name}</span>
<span class="project-manager">👤 ${manager || '未指定'}</span>
<span class="contract-amount">💰 ${contractAmount.toFixed(2)} 元</span>
</div>
<div>
<button class="delete-project-btn" data-project-id="${id}">🗑️ 删除项目</button>
</div>
</div>
<div class="invoice-toolbar">
<button class="btn-sm btn-outline" data-project-id="${id}" onclick="addInvoice('${id}')">➕ 新增开票</button>
</div>
<table class="invoice-table">
<thead><tr><th>序号</th><th>开票日期</th><th>开票金额(元)</th><th>操作</th></tr></thead>
<tbody>${invoiceRows}</tbody>
</table>
<!-- 资金收支模块 -->
<div class="receipt-section">
<div class="receipt-toolbar">
<button class="btn-sm btn-outline" onclick="addReceipt('${id}', 'normal')">💰 新增普通收款</button>
<button class="btn-sm btn-outline" onclick="addReceipt('${id}', 'special')">✨ 新增特殊收款</button>
</div>
<table class="receipt-table">
<thead>
<tr>
<th>序号</th>
<th>收款日期</th>
<th>收款金额(元)</th>
<th>类型</th>
<th>支出(元)</th>
<th>应付金额(元)</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${receiptRows}
</tbody>
${totalRow}
</table>
</div>
<div class="project-footer">
<div class="total-paid">🧑‍🏭 总实发: ${totalPaid.toFixed(2)} 元</div>
<div class="total-invoiced">📄 累计开票: ${totalInvoiced.toFixed(2)} 元</div>
</div>
</div>`;
});
}
html += `</div>`;
app.innerHTML = html;
document.getElementById('searchInput').value = searchKeyword;
closeDropdown(); // 关闭可能遗留的菜单
}
// 刷新项目汇总(包括全局统计)
function refreshProjectSummary(projectId) {
const card = document.querySelector(`.project-card[data-project-id="${projectId}"]`);
if (!card) return;
const proj = projects.find(p => p.id === projectId);
if (!proj) return;
// 重新计算开票统计
let totalInvoiced = 0, totalPaid = 0;
proj.invoices.forEach(inv => {
totalInvoiced += inv.amount || 0;
const { paid } = calculateInvoice(inv);
totalPaid += paid;
});
const totalPaidDiv = card.querySelector('.total-paid');
const totalInvoicedDiv = card.querySelector('.total-invoiced');
if (totalPaidDiv) totalPaidDiv.textContent = `🧑‍🏭 总实发: ${totalPaid.toFixed(2)} 元`;
if (totalInvoicedDiv) totalInvoicedDiv.textContent = `📄 累计开票: ${totalInvoiced.toFixed(2)} 元`;
// 更新全局统计
let globalTotalInvoiced = 0, globalTotalPaid = 0;
projects.forEach(p => {
p.invoices.forEach(inv => {
globalTotalInvoiced += inv.amount || 0;
const { paid } = calculateInvoice(inv);
globalTotalPaid += paid;
});
});
const globalStats = document.querySelector('.global-stats');
if (globalStats) {
const statValues = globalStats.querySelectorAll('.stat-value');
if (statValues.length >= 2) {
statValues[0].textContent = globalTotalInvoiced.toFixed(2) + ' 元';
statValues[1].textContent = globalTotalPaid.toFixed(2) + ' 元';
}
}
}
// ---------- 事件监听 ----------
app.addEventListener('click', (e) => {
// 添加项目
if (e.target.id === 'addProjectBtn') {
const name = document.getElementById('newProjName').value.trim() || '未命名';
const manager = document.getElementById('newProjManager').value.trim() || '';
const amount = parseFloat(document.getElementById('newProjAmount').value) || 0;
projects.push({
id: genId(),
name: name,
manager: manager,
contractAmount: amount,
invoices: [],
receipts: []
});
render();
return;
}
// 下载CSV
if (e.target.id === 'downloadDataBtn') {
if (projects.length === 0) { alert('无项目'); return; }
let csv = "\uFEFF项目名称,负责人,合同金额(元),开票日期,开票金额(元),扣费科目,扣费类型,扣费值,扣费金额(元),公司人工费工资单(元),收款日期,收款金额(元),收款类型,支出(元),应付金额(元),备注\n";
projects.forEach(p => {
// 开票明细
p.invoices.forEach(inv => {
const amount = inv.amount || 0;
const { paid } = calculateInvoice(inv);
if (!inv.deductions || inv.deductions.length === 0) {
csv += `"${p.name}",${p.manager},${p.contractAmount.toFixed(2)},${inv.date},${amount.toFixed(2)},,,,0,${paid.toFixed(2)},,,,,,\n`;
} else {
inv.deductions.forEach(d => {
const deductionAmount = d.type === 'percent' ? amount * (d.value / 100) : d.value;
csv += `"${p.name}",${p.manager},${p.contractAmount.toFixed(2)},${inv.date},${amount.toFixed(2)},"${d.name}",${d.type},${d.value},${deductionAmount.toFixed(2)},${paid.toFixed(2)},,,,,,\n`;
});
}
});
// 收款明细
const recsWithDetails = calculateReceiptsDeductions(p);
recsWithDetails.forEach(rec => {
csv += `"${p.name}",${p.manager},${p.contractAmount.toFixed(2)},,,,,,,,,${rec.date},${rec.amount.toFixed(2)},${rec.type},${rec.expenseTotal.toFixed(2)},${rec.payable.toFixed(2)},"${rec.memo || ''}"\n`;
});
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = '项目资金明细.csv';
link.click();
URL.revokeObjectURL(link.href);
return;
}
// 清除搜索
if (e.target.id === 'clearSearchBtn') {
searchKeyword = '';
render();
return;
}
// 删除项目
if (e.target.classList.contains('delete-project-btn')) {
const projId = e.target.dataset.projectId;
projects = projects.filter(p => p.id !== projId);
render();
return;
}
// 删除发票
if (e.target.classList.contains('delete-invoice-btn')) {
const projId = e.target.dataset.projectId;
const invId = e.target.dataset.invoiceId;
const proj = projects.find(p => p.id === projId);
if (proj) {
proj.invoices = proj.invoices.filter(i => i.id !== invId);
render();
}
return;
}
// 删除收款
if (e.target.classList.contains('delete-receipt-btn')) {
const projId = e.target.dataset.projectId;
const recId = e.target.dataset.receiptId;
const proj = projects.find(p => p.id === projId);
if (proj) {
proj.receipts = proj.receipts.filter(r => r.id !== recId);
render();
}
return;
}
// 添加扣费项
if (e.target.classList.contains('add-deduction-btn')) {
const projId = e.target.dataset.projectId;
const invId = e.target.dataset.invoiceId;
const proj = projects.find(p => p.id === projId);
if (proj) {
const inv = proj.invoices.find(i => i.id === invId);
if (inv) {
if (!inv.deductions) inv.deductions = [];
const count = inv.deductions.length;
const defaultName = FIXED_NAMES[count % FIXED_NAMES.length];
inv.deductions.push({
id: genId(),
name: defaultName,
type: 'percent',
value: 0
});
render();
}
}
return;
}
// 删除扣费项
if (e.target.classList.contains('remove-deduction-btn')) {
const projId = e.target.dataset.projectId;
const invId = e.target.dataset.invoiceId;
const dedId = e.target.dataset.deductionId;
const proj = projects.find(p => p.id === projId);
if (proj) {
const inv = proj.invoices.find(i => i.id === invId);
if (inv && inv.deductions) {
inv.deductions = inv.deductions.filter(d => d.id !== dedId);
render();
}
}
return;
}
// 点击科目名称下拉按钮
if (e.target.classList.contains('deduction-name-dropdown')) {
e.preventDefault();
e.stopPropagation();
const btn = e.target;
const deductionItem = btn.closest('.deduction-item');
const nameInput = deductionItem.querySelector('.deduction-name');
showDropdown(btn, nameInput);
return;
}
});
// 处理科目名称下拉菜单选择
dropdown.addEventListener('click', (e) => {
const target = e.target;
if (target.tagName === 'DIV' && target.dataset.value) {
const value = target.dataset.value;
if (activeDeductionInput) {
activeDeductionInput.value = value;
// 触发 input 事件以更新数据模型
const event = new Event('input', { bubbles: true });
activeDeductionInput.dispatchEvent(event);
closeDropdown();
}
}
});
// 点击页面其他地方关闭下拉菜单
document.addEventListener('click', (e) => {
if (!dropdown.contains(e.target) && !e.target.classList.contains('deduction-name-dropdown')) {
closeDropdown();
}
});
// 处理输入变更
app.addEventListener('input', (e) => {
if (e.target.classList.contains('date-input') || e.target.classList.contains('amount-input')) {
const projId = e.target.dataset.projectId;
const invId = e.target.dataset.invoiceId;
const proj = projects.find(p => p.id === projId);
if (!proj) return;
const inv = proj.invoices.find(i => i.id === invId);
if (!inv) return;
if (e.target.classList.contains('date-input')) {
inv.date = e.target.value;
} else if (e.target.classList.contains('amount-input')) {
let val = parseFloat(e.target.value);
inv.amount = isNaN(val) ? 0 : val;
}
render(); // 需要重绘以更新收款的支出(因为开票金额变化影响普通收款)
}
if (e.target.classList.contains('receipt-date-input') || e.target.classList.contains('receipt-amount-input')) {
const projId = e.target.dataset.projectId;
const recId = e.target.dataset.receiptId;
const proj = projects.find(p => p.id === projId);
if (!proj) return;
const rec = proj.receipts.find(r => r.id === recId);
if (!rec) return;
if (e.target.classList.contains('receipt-date-input')) {
rec.date = e.target.value;
} else if (e.target.classList.contains('receipt-amount-input')) {
let val = parseFloat(e.target.value);
rec.amount = isNaN(val) ? 0 : val;
}
render();
}
if (e.target.classList.contains('receipt-expense-input')) {
const projId = e.target.dataset.projectId;
const recId = e.target.dataset.receiptId;
const proj = projects.find(p => p.id === projId);
if (!proj) return;
const rec = proj.receipts.find(r => r.id === recId);
if (!rec) return;
let val = parseFloat(e.target.value);
rec.manualExpense = isNaN(val) ? 0 : val;
render();
}
if (e.target.classList.contains('receipt-memo-input')) {
const projId = e.target.dataset.projectId;
const recId = e.target.dataset.receiptId;
const proj = projects.find(p => p.id === projId);
if (!proj) return;
const rec = proj.receipts.find(r => r.id === recId);
if (!rec) return;
rec.memo = e.target.value;
// 不需要重绘,因为备注不影响计算,但为了保持数据一致,可以更新
}
if (e.target.classList.contains('deduction-name') || e.target.classList.contains('deduction-value')) {
const projId = e.target.dataset.projectId;
const invId = e.target.dataset.invoiceId;
const dedId = e.target.dataset.deductionId;
const proj = projects.find(p => p.id === projId);
if (!proj) return;
const inv = proj.invoices.find(i => i.id === invId);
if (!inv) return;
const ded = inv.deductions.find(d => d.id === dedId);
if (!ded) return;
if (e.target.classList.contains('deduction-name')) {
ded.name = e.target.value;
} else if (e.target.classList.contains('deduction-value')) {
let val = parseFloat(e.target.value);
ded.value = isNaN(val) ? 0 : val;
}
render();
}
});
// 处理扣费类型变更
app.addEventListener('change', (e) => {
if (e.target.classList.contains('deduction-type')) {
const projId = e.target.dataset.projectId;
const invId = e.target.dataset.invoiceId;
const dedId = e.target.dataset.deductionId;
const proj = projects.find(p => p.id === projId);
if (!proj) return;
const inv = proj.invoices.find(i => i.id === invId);
if (!inv) return;
const ded = inv.deductions.find(d => d.id === dedId);
if (!ded) return;
ded.type = e.target.value;
render();
}
if (e.target.classList.contains('receipt-type-select')) {
const projId = e.target.dataset.projectId;
const recId = e.target.dataset.receiptId;
const proj = projects.find(p => p.id === projId);
if (!proj) return;
const rec = proj.receipts.find(r => r.id === recId);
if (!rec) return;
rec.type = e.target.value;
if (rec.type === 'special' && rec.manualExpense === undefined) {
rec.manualExpense = 0;
}
render();
}
});
// 搜索
app.addEventListener('input', (e) => {
if (e.target.id === 'searchInput') {
searchKeyword = e.target.value;
render();
}
});
// 初始渲染
render();
})();
</script>
</body>
</html> |
|