586 lines
19 KiB
JavaScript
586 lines
19 KiB
JavaScript
const express = require('express');
|
|
const db = require('../db');
|
|
const { authenticate, requireAdmin } = require('../middleware/auth');
|
|
|
|
const router = express.Router();
|
|
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const result = await db.query(`
|
|
SELECT
|
|
p.id,
|
|
p.name,
|
|
p.customer_id,
|
|
p.project_manager_id,
|
|
p.contract_amount,
|
|
p.start_date,
|
|
p.end_date,
|
|
p.description,
|
|
p.status,
|
|
p.location,
|
|
c.name as customer_name,
|
|
u.name as manager_name
|
|
FROM projects p
|
|
LEFT JOIN customers c ON p.customer_id = c.id
|
|
LEFT JOIN users u ON p.project_manager_id = u.id
|
|
ORDER BY p.created_at DESC
|
|
LIMIT 50
|
|
`);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows,
|
|
count: result.rows.length
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// 获取项目基本信息
|
|
const projectResult = await db.query(`
|
|
SELECT
|
|
p.*,
|
|
c.name as customer_name,
|
|
u.name as manager_name
|
|
FROM projects p
|
|
LEFT JOIN customers c ON p.customer_id = c.id
|
|
LEFT JOIN users u ON p.project_manager_id = u.id
|
|
WHERE p.id = $1
|
|
`, [id]);
|
|
|
|
if (projectResult.rows.length > 0) {
|
|
const project = projectResult.rows[0];
|
|
|
|
// 获取项目合同信息
|
|
const contractResult = await db.query(`
|
|
SELECT * FROM project_contracts
|
|
WHERE project_id = $1
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
`, [id]);
|
|
|
|
const contract = contractResult.rows[0];
|
|
|
|
// 从合同表读取质保金数据,如果没有则使用默认值
|
|
const warrantyPercent = contract?.warranty_deposit_percentage || 5;
|
|
const warrantyMonths = contract?.warranty_period || 12;
|
|
const contractAmount = parseFloat(project.contract_amount || 0);
|
|
|
|
// 计算质保金金额:合同金额 * 质保比例 / 100
|
|
const warrantyAmount = Math.round(contractAmount * warrantyPercent / 100);
|
|
|
|
// 计算质保期结束日期
|
|
const warrantyStartDate = project.end_date;
|
|
const warrantyEndDate = warrantyStartDate
|
|
? new Date(new Date(warrantyStartDate).getTime() + warrantyMonths * 30 * 24 * 60 * 60 * 1000).toISOString()
|
|
: null;
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
id: project.id,
|
|
project_code: project.code || `PROJ-${String(project.id).padStart(4, '0')}`,
|
|
name: project.name,
|
|
customer_id: project.customer_id,
|
|
customer_name: project.customer_name || '未知客户',
|
|
status: project.status || 'planning',
|
|
budget: '0',
|
|
spent: '0',
|
|
start_date: project.start_date,
|
|
end_date: project.end_date,
|
|
description: project.description,
|
|
contract_type: 'lump_sum',
|
|
contract_amount: project.contract_amount?.toString() || '0',
|
|
currency: 'CNY',
|
|
contract_days: contract?.contract_period || 180,
|
|
project_manager_id: project.project_manager_id,
|
|
manager_id: project.project_manager_id,
|
|
manager_name: project.manager_name || '未知经理',
|
|
location: project.location || '',
|
|
work_quantity: '',
|
|
project_situation: project.description || '',
|
|
settlement_type: contract?.settlement_method || 'lump_sum',
|
|
has_warranty: true,
|
|
warranty_amount: warrantyAmount.toString(),
|
|
warranty_percent: warrantyPercent.toString(),
|
|
warranty_months: warrantyMonths,
|
|
warranty_start_date: warrantyStartDate,
|
|
warranty_end_date: warrantyEndDate,
|
|
warranty_status: 'pending'
|
|
}
|
|
});
|
|
} else {
|
|
res.status(404).json({
|
|
success: false,
|
|
message: '项目不存在'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('获取项目详情失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目详情失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id/contracts', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await db.query(`
|
|
SELECT * FROM project_contracts
|
|
WHERE project_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目合同失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目合同失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id/subcontracts', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await db.query(`
|
|
SELECT * FROM subcontracts
|
|
WHERE project_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [id]);
|
|
|
|
// 解析unit_price_items字段
|
|
const subcontracts = result.rows.map(subcontract => {
|
|
if (subcontract.unit_price_items) {
|
|
try {
|
|
subcontract.unit_price_items = JSON.parse(subcontract.unit_price_items);
|
|
} catch (error) {
|
|
subcontract.unit_price_items = [];
|
|
}
|
|
} else {
|
|
subcontract.unit_price_items = [];
|
|
}
|
|
return subcontract;
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: subcontracts
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目分包失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目分包失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.post('/:id/subcontracts', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { subcontractor_id, subcontractor_name, contract_amount, currency, settlement_type, other_terms, payment_description, unit_price_items, start_date, end_date, work_days, status } = req.body;
|
|
|
|
const unitPriceItemsJson = unit_price_items ? JSON.stringify(unit_price_items) : null;
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO subcontracts (project_id, subcontractor_id, subcontractor_name, contract_amount, currency, settlement_type, other_terms, payment_description, unit_price_items, start_date, end_date, work_days, paid_amount, status, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
|
[id, subcontractor_id, subcontractor_name, contract_amount, currency || 'CNY', settlement_type || 'lump_sum', other_terms, payment_description, unitPriceItemsJson, start_date, end_date, work_days, 0, status || 'active']
|
|
);
|
|
|
|
const subcontractId = (result.rows[0]?.id || result.rows?.[0]?.id);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '新增分包成功',
|
|
data: {
|
|
id: subcontractId,
|
|
project_id: id,
|
|
subcontractor_id,
|
|
subcontractor_name,
|
|
contract_amount,
|
|
currency: currency || 'CNY',
|
|
settlement_type: settlement_type || 'lump_sum',
|
|
other_terms,
|
|
payment_description,
|
|
unit_price_items,
|
|
start_date,
|
|
end_date,
|
|
work_days,
|
|
paid_amount: 0,
|
|
status: status || 'active',
|
|
created_at: new Date().toISOString()
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('新增项目分包失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '新增项目分包失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id/materials', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await db.query(`
|
|
SELECT * FROM project_materials
|
|
WHERE project_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目材料失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目材料失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id/milestones', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await db.query(`
|
|
SELECT * FROM project_milestones
|
|
WHERE project_id = $1
|
|
ORDER BY expected_date ASC
|
|
`, [id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目施工节点失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目施工节点失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id/finances', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await db.query(`
|
|
SELECT * FROM project_finances
|
|
WHERE project_id = $1
|
|
ORDER BY payment_date DESC
|
|
`, [id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目财务失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目财务失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id/warranty-deposits', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await db.query(`
|
|
SELECT * FROM warranty_deposits
|
|
WHERE project_id = $1
|
|
ORDER BY created_at DESC
|
|
`, [id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: result.rows
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目质保金失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目质保金失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/:id/construction-logs', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// 由于施工日志表可能不存在,返回空数组
|
|
res.json({
|
|
success: true,
|
|
data: []
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目施工日志失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目施工日志失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.post('/', async (req, res) => {
|
|
try {
|
|
const { name, code, customer_id, manager_id, contract_amount, start_date, end_date, description, status, location } = req.body;
|
|
|
|
const projectCode = code || 'PROJ' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
|
|
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO projects (name, code, customer_id, manager_id, contract_amount, start_date, end_date, description, status, location, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
|
[name, projectCode, customer_id || null, manager_id || null, contract_amount || 0, start_date || '', end_date || '', description || '', status || 'planning', location || '']
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '项目创建成功',
|
|
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id), code: projectCode }
|
|
});
|
|
} catch (error) {
|
|
console.error('创建项目失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '创建项目失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
router.delete('/:id', authenticate, requireAdmin, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
await db.query('DELETE FROM projects WHERE id = $1', [id]);
|
|
res.json({ success: true, message: '项目已删除' });
|
|
} catch (error) {
|
|
console.error('删除项目失败:', error);
|
|
res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
});
|
|
|
|
router.put('/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { name, manager_id, location, start_date, end_date, description, status, contract_amount } = req.body;
|
|
|
|
console.log('收到项目更新请求:', { id, name, manager_id, location, start_date, end_date, description });
|
|
|
|
// 更新项目信息
|
|
await db.query(
|
|
'UPDATE projects SET name = CASE WHEN $1 IS NOT NULL THEN $2 ELSE name END, manager_id = CASE WHEN $3 IS NOT NULL THEN $4 ELSE manager_id END, location = CASE WHEN $5 IS NOT NULL THEN $6 ELSE location END, start_date = CASE WHEN $7 IS NOT NULL THEN $8 ELSE start_date END, end_date = CASE WHEN $9 IS NOT NULL THEN $10 ELSE end_date END, description = CASE WHEN $11 IS NOT NULL THEN $12 ELSE description END, status = CASE WHEN $13 IS NOT NULL THEN $14 ELSE status END, contract_amount = CASE WHEN $15 IS NOT NULL THEN $16 ELSE contract_amount END, updated_at = CURRENT_TIMESTAMP WHERE id = $17',
|
|
[name, name, manager_id, manager_id, location, location, start_date, start_date, end_date, end_date, description, description, status, status, contract_amount, contract_amount, id]
|
|
);
|
|
|
|
// 如果提供了开始和结束日期,更新合同的工期信息
|
|
if (start_date && end_date) {
|
|
const start = new Date(start_date);
|
|
const end = new Date(end_date);
|
|
const contractPeriod = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
|
|
// 更新合同信息
|
|
await db.query(
|
|
'UPDATE project_contracts SET start_date = $1, end_date = $2, contract_period = $3 WHERE project_id = $4',
|
|
[start_date, end_date, contractPeriod, id]
|
|
);
|
|
}
|
|
|
|
// 查询更新后的数据
|
|
const updatedResult = await db.query('SELECT * FROM projects WHERE id = $1', [id]);
|
|
res.json({ success: true, data: updatedResult.rows[0] });
|
|
} catch (error) {
|
|
console.error('更新项目失败:', error);
|
|
res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
});
|
|
|
|
router.put('/:id/contract', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const {
|
|
project_overview,
|
|
settlement_type,
|
|
contract_total,
|
|
tax_included,
|
|
unit_price_items,
|
|
payment_nodes,
|
|
other_info,
|
|
contract_file
|
|
} = req.body;
|
|
|
|
console.log('收到合同细节保存请求:', { id, project_overview, settlement_type, contract_total, tax_included, contract_file });
|
|
|
|
// 1. 更新项目基本信息
|
|
await db.query(
|
|
`UPDATE projects
|
|
SET description = $1, contract_amount = $2
|
|
WHERE id = $3`,
|
|
[project_overview, contract_total, id]
|
|
);
|
|
|
|
// 2. 更新或创建项目合同
|
|
const contractResult = await db.query(
|
|
`SELECT * FROM project_contracts WHERE project_id = $1`,
|
|
[id]
|
|
);
|
|
|
|
if (contractResult.rows.length > 0) {
|
|
// 更新现有合同
|
|
await db.query(
|
|
`UPDATE project_contracts
|
|
SET settlement_method = $1, contract_amount = $2, contract_file = $3, other_info = $4, tax_included = $5
|
|
WHERE project_id = $6`,
|
|
[settlement_type, contract_total, contract_file, other_info, tax_included, id]
|
|
);
|
|
} else {
|
|
// 创建新合同
|
|
const contractCode = `CONTRACT-${Date.now()}`;
|
|
await db.query(
|
|
`INSERT INTO project_contracts (project_id, contract_code, contract_amount, settlement_method, contract_file, other_info, tax_included, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
|
[id, contractCode, contract_total, settlement_type, contract_file, other_info, tax_included]
|
|
);
|
|
}
|
|
|
|
// 3. 处理付款节点
|
|
if (payment_nodes && Array.isArray(payment_nodes)) {
|
|
// 删除旧的付款节点
|
|
await db.query(`DELETE FROM project_milestones WHERE project_id = $1`, [id]);
|
|
|
|
// 创建新的付款节点
|
|
for (const node of payment_nodes) {
|
|
await db.query(
|
|
`INSERT INTO project_milestones (project_id, milestone_name, condition, percentage, amount, status, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
|
[id, node.name, node.condition || '', node.percentage, node.amount, 'pending']
|
|
);
|
|
}
|
|
}
|
|
|
|
// 4. 处理单价项
|
|
if (unit_price_items && Array.isArray(unit_price_items) && settlement_type === 'unit_price') {
|
|
// 删除旧的材料项
|
|
await db.query(`DELETE FROM project_materials WHERE project_id = $1`, [id]);
|
|
|
|
// 创建新的材料项
|
|
for (const item of unit_price_items) {
|
|
await db.query(
|
|
`INSERT INTO project_materials (project_id, product_name, unit, budget_quantity, average_price, total_amount, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
|
|
[id, item.name, item.unit, item.quantity, item.price, item.total]
|
|
);
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '合同细节保存成功'
|
|
});
|
|
} catch (error) {
|
|
console.error('保存合同细节失败:', error);
|
|
res.status(500).json({ success: false, message: error.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 totalPayment = paymentResult.rows[0]?.total_payment || 0;
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
project_name: project.name,
|
|
contract_amount: project.contract_amount || 0,
|
|
purchase_cost: {
|
|
total: totalPurchase,
|
|
by_category: purchaseByCategory
|
|
},
|
|
payment_cost: totalPayment,
|
|
total_cost: totalPurchase + totalPayment,
|
|
profit: (project.contract_amount || 0) - (totalPurchase + totalPayment)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取项目成本统计失败:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '获取项目成本统计失败',
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
module.exports = router; |