备份:PWA配置前的完整版本

包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
a273825743
2026-06-13 12:44:48 +08:00
parent 5fd19822a2
commit 706dcc24eb
83 changed files with 26590 additions and 13379 deletions
+109 -35
View File
@@ -19,10 +19,17 @@ router.get('/', async (req, res) => {
p.status,
p.location,
c.name as customer_name,
u.name as manager_name
u.name as manager_name,
COALESCE(expense_summary.total_expense, 0) as total_expense
FROM projects p
LEFT JOIN customers c ON p.customer_id = c.id
LEFT JOIN users u ON p.project_manager_id = u.id
LEFT JOIN (
SELECT project_id, SUM(amount_cny::numeric) as total_expense
FROM financial_records
WHERE txn_type = 'expense' AND status = 'confirmed'
GROUP BY project_id
) expense_summary ON p.id = expense_summary.project_id
ORDER BY p.created_at DESC
LIMIT 50
`);
@@ -472,6 +479,14 @@ router.put('/:id', async (req, res) => {
);
}
// 合同金额变更时,自动按比例更新付款节点金额
if (contract_amount !== undefined && contract_amount !== null) {
await db.query(
`UPDATE project_milestones SET amount = ROUND($1 * percentage / 100, 2) WHERE project_id = $2`,
[contract_amount, id]
);
}
if (start_date && end_date) {
const start = new Date(start_date);
const end = new Date(end_date);
@@ -553,6 +568,12 @@ router.put('/:id/contract', async (req, res) => {
[id, node.name, node.condition || '', node.percentage, node.amount, 'pending']
);
}
} else if (contract_total) {
// 没有传付款节点但合同金额变了,按比例更新现有里程碑金额
await db.query(
`UPDATE project_milestones SET amount = ROUND($1 * percentage / 100, 2) WHERE project_id = $2`,
[contract_total, id]
);
}
// 4. 处理单价项
@@ -581,55 +602,108 @@ router.put('/:id/contract', async (req, res) => {
}
});
router.get('/:id/financial-details', async (req, res) => {
try {
const { id } = req.params;
const { txn_type, category_level1, category_level2, date_from, date_to, page = 1, pageSize = 20 } = req.query;
let sql = `SELECT fr.*, p.name as project_name FROM financial_records fr LEFT JOIN projects p ON fr.project_id = p.id WHERE fr.project_id = $1 AND fr.status != 'voided'`;
const params = [id];
let idx = 2;
if (txn_type) { params.push(txn_type); sql += ` AND fr.txn_type = $${idx++}`; }
if (category_level1) { params.push(category_level1); sql += ` AND fr.category_level1 = $${idx++}`; }
if (category_level2) { params.push(category_level2); sql += ` AND fr.category_level2 = $${idx++}`; }
if (date_from) { params.push(date_from); sql += ` AND fr.record_date >= $${idx++}`; }
if (date_to) { params.push(date_to); sql += ` AND fr.record_date <= $${idx++}`; }
const countResult = await db.query(`SELECT COUNT(*) as total FROM (${sql}) sub`, params);
const total = parseInt(countResult.rows[0].total);
sql += ' ORDER BY fr.record_date DESC, fr.created_at DESC';
const offset = (parseInt(page) - 1) * parseInt(pageSize);
params.push(parseInt(pageSize));
sql += ` LIMIT $${idx++}`;
params.push(offset);
sql += ` OFFSET $${idx++}`;
const result = await db.query(sql, params);
res.json({
success: true,
data: result.rows,
pagination: { page: parseInt(page), pageSize: parseInt(pageSize), total, totalPages: Math.ceil(total / parseInt(pageSize)) }
});
} catch (error) {
console.error('获取项目财务明细失败:', error);
res.status(500).json({ success: false, message: '获取项目财务明细失败' });
}
});
router.get('/:id/cost-summary', async (req, res) => {
try {
const { id } = req.params;
const purchaseResult = await db.query(`
SELECT
expense_category,
SUM(total_amount) as total_amount
FROM purchase_requests
WHERE project_id = $1 AND status IN ('approved', 'executed')
GROUP BY expense_category
`, [id]);
const paymentResult = await db.query(`
SELECT
SUM(amount) as total_payment
FROM payment_requests
WHERE project_id = $1 AND status = 'approved' AND payment_type = 'company'
`, [id]);
const projectResult = await db.query('SELECT * FROM projects WHERE id = $1', [id]);
if (projectResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '项目不存在' });
}
const project = projectResult.rows[0];
const purchaseByCategory = {};
let totalPurchase = 0;
purchaseResult.rows.forEach(row => {
purchaseByCategory[row.expense_category] = row.total_amount;
totalPurchase += row.total_amount;
const incomeResult = await db.query(`
SELECT category_level2, COALESCE(SUM(amount_cny), 0) as total_amount
FROM financial_records
WHERE project_id = $1 AND txn_type = 'income' AND status != 'voided'
GROUP BY category_level2
`, [id]);
const expenseResult = await db.query(`
SELECT category_level1, category_level2, COALESCE(SUM(amount_cny), 0) as total_amount
FROM financial_records
WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided'
GROUP BY category_level1, category_level2
`, [id]);
const totalIncomeRow = await db.query(`
SELECT COALESCE(SUM(amount_cny), 0) as total FROM financial_records
WHERE project_id = $1 AND txn_type = 'income' AND status != 'voided'
`, [id]);
const totalExpenseRow = await db.query(`
SELECT COALESCE(SUM(amount_cny), 0) as total FROM financial_records
WHERE project_id = $1 AND txn_type = 'expense' AND status != 'voided'
`, [id]);
const incomeByCategory = {};
let totalIncome = parseFloat(totalIncomeRow.rows[0].total);
incomeResult.rows.forEach(row => {
incomeByCategory[row.category_level2] = parseFloat(row.total_amount);
});
const totalPayment = paymentResult.rows[0]?.total_payment || 0;
const expenseByCategory = {};
const expenseByLevel1 = {};
let totalExpense = parseFloat(totalExpenseRow.rows[0].total);
expenseResult.rows.forEach(row => {
expenseByCategory[row.category_level2] = parseFloat(row.total_amount);
if (!expenseByLevel1[row.category_level1]) expenseByLevel1[row.category_level1] = 0;
expenseByLevel1[row.category_level1] += parseFloat(row.total_amount);
});
res.json({
success: true,
data: {
project_name: project.name,
contract_amount: project.contract_amount || 0,
purchase_cost: {
total: totalPurchase,
by_category: purchaseByCategory
income: {
total: totalIncome,
by_category: incomeByCategory
},
payment_cost: totalPayment,
total_cost: totalPurchase + totalPayment,
profit: (project.contract_amount || 0) - (totalPurchase + totalPayment)
expense: {
total: totalExpense,
by_category: expenseByCategory,
by_level1: expenseByLevel1
},
total_cost: totalExpense,
profit: totalIncome - totalExpense
}
});
} catch (error) {