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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? 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) RETURNING id`, [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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? 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: process.env.NODE_ENV === 'development' ? error.message : '操作失败' }); } }); router.post('/', async (req, res) => { try { const { name, code, customer_id, manager_id, contract_amount, start_date, end_date, description, status, location, type_template_id } = 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, project_manager_id, contract_amount, start_date, end_date, description, status, location, type_template_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) RETURNING id`, [name, projectCode, customer_id || null, manager_id || null, contract_amount || 0, start_date || '', end_date || '', description || '', status || 'planning', location || '', type_template_id || null] ); const projectId = result.rows[0]?.id || result.rows?.[0]?.id; if (type_template_id && projectId) { const tplResult = await db.query('SELECT phases FROM project_type_templates WHERE id = $1', [type_template_id]); if (tplResult.rows.length > 0) { const phases = tplResult.rows[0].phases; if (Array.isArray(phases)) { for (const phase of phases) { const subItems = (phase.sub_items || []).map(item => ({ name: item, completed: false })); await db.query( `INSERT INTO project_phases (project_id, template_key, phase_name, phase_order, phase_type, depends_on, sub_items, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [projectId, phase.key || `phase_${phase.order}`, phase.name, phase.order, phase.type || 'serial', phase.depends || [], JSON.stringify(subItems), phase.order === 1 ? 'in_progress' : 'pending'] ); } const firstPhase = phases.find(p => p.order === 1); await db.query('UPDATE projects SET current_phase = $1 WHERE id = $2', [firstPhase ? firstPhase.name : '', projectId]); } } } res.json({ success: true, message: '项目创建成功', data: { id: projectId, code: projectCode } }); } catch (error) { console.error('创建项目失败:', error); res.status(500).json({ success: false, message: '创建项目失败', error: process.env.NODE_ENV === 'development' ? 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: '操作失败' }); } }); 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; const updates = []; const values = []; let paramIndex = 1; if (name !== undefined && name !== null) { updates.push(`name = $${paramIndex++}`); values.push(name); } if (manager_id !== undefined && manager_id !== null) { updates.push(`project_manager_id = $${paramIndex++}`); values.push(manager_id); } if (location !== undefined && location !== null) { updates.push(`location = $${paramIndex++}`); values.push(location); } if (start_date !== undefined && start_date !== null) { updates.push(`start_date = $${paramIndex++}`); values.push(start_date); } if (end_date !== undefined && end_date !== null) { updates.push(`end_date = $${paramIndex++}`); values.push(end_date); } if (description !== undefined && description !== null) { updates.push(`description = $${paramIndex++}`); values.push(description); } if (status !== undefined && status !== null) { updates.push(`status = $${paramIndex++}`); values.push(status); } if (contract_amount !== undefined && contract_amount !== null) { updates.push(`contract_amount = $${paramIndex++}`); values.push(contract_amount); } updates.push(`updated_at = CURRENT_TIMESTAMP`); if (updates.length > 1) { values.push(id); await db.query( `UPDATE projects SET ${updates.join(', ')} WHERE id = $${paramIndex}`, values ); } 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: '操作失败' }); } }); 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; // 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) RETURNING id`, [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) RETURNING id`, [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) RETURNING id`, [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: '操作失败' }); } }); 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: process.env.NODE_ENV === 'development' ? error.message : '操作失败' }); } }); module.exports = router; router.get('/:id/phases', async (req, res) => { try { const { id } = req.params; const result = await db.query( 'SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order', [id] ); res.json({ success: true, data: result.rows }); } catch (error) { console.error('获取项目阶段失败:', error); res.status(500).json({ success: false, message: '获取项目阶段失败' }); } }); router.post('/:id/phases/init', async (req, res) => { try { const { id } = req.params; const { template_id } = req.body; const templateResult = await db.query('SELECT phases FROM project_type_templates WHERE id = $1', [template_id]); if (templateResult.rows.length === 0) { return res.status(404).json({ success: false, message: '模板不存在' }); } const phases = templateResult.rows[0].phases; if (!Array.isArray(phases)) { return res.status(400).json({ success: false, message: '模板阶段数据格式错误' }); } const existing = await db.query('SELECT id FROM project_phases WHERE project_id = $1', [id]); if (existing.rows.length > 0) { return res.status(400).json({ success: false, message: '该项目已有阶段数据' }); } for (const phase of phases) { const subItems = (phase.sub_items || []).map(item => ({ name: item, completed: false })); await db.query( `INSERT INTO project_phases (project_id, template_key, phase_name, phase_order, phase_type, depends_on, sub_items, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [id, phase.key || `phase_${phase.order}`, phase.name, phase.order, phase.type || 'serial', phase.depends || [], JSON.stringify(subItems), phase.order === 1 ? 'in_progress' : 'pending'] ); } const firstPhase = phases.find(p => p.order === 1); await db.query( 'UPDATE projects SET type_template_id = $1, current_phase = $2, phase_progress = 0, updated_at = CURRENT_TIMESTAMP WHERE id = $3', [template_id, firstPhase ? firstPhase.name : '', id] ); res.json({ success: true, message: '阶段初始化成功' }); } catch (error) { console.error('初始化项目阶段失败:', error); res.status(500).json({ success: false, message: '初始化项目阶段失败' }); } }); router.put('/:id/phases/:phaseId/complete', async (req, res) => { try { const { id, phaseId } = req.params; const { remark, photos, completed_by } = req.body; const phaseResult = await db.query('SELECT * FROM project_phases WHERE id = $1 AND project_id = $2', [phaseId, id]); if (phaseResult.rows.length === 0) { return res.status(404).json({ success: false, message: '阶段不存在' }); } const phase = phaseResult.rows[0]; await db.query( `UPDATE project_phases SET status = 'completed', completed_at = NOW(), completed_by = $1, remark = $2, photos = $3 WHERE id = $4`, [completed_by || null, remark || phase.remark, photos || phase.photos, phaseId] ); const allPhases = await db.query('SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order', [id]); const completedCount = allPhases.rows.filter(p => p.status === 'completed').length; const totalCount = allPhases.rows.length; const progress = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; let nextPhase = null; for (const p of allPhases.rows) { if (p.status === 'pending') { const deps = p.depends_on || []; const depsMet = deps.length === 0 || deps.every(depId => { const depPhase = allPhases.rows.find(ap => ap.id === depId || ap.phase_order === depId); return depPhase && depPhase.status === 'completed'; }); if (depsMet) { nextPhase = p; break; } } } if (nextPhase) { await db.query("UPDATE project_phases SET status = 'in_progress', started_at = NOW() WHERE id = $1", [nextPhase.id]); const newStatus = progress === 100 ? 'completed' : 'active'; await db.query( 'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4', [nextPhase.phase_name, progress, newStatus, id] ); } else { const newStatus = progress === 100 ? 'completed' : 'active'; await db.query( 'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4', ['', progress, newStatus, id] ); } res.json({ success: true, message: '阶段完成', data: { progress, nextPhase: nextPhase ? nextPhase.phase_name : null } }); } catch (error) { console.error('完成阶段失败:', error); res.status(500).json({ success: false, message: '完成阶段失败' }); } }); router.put('/:id/phases/:phaseId/reopen', async (req, res) => { try { const { id, phaseId } = req.params; await db.query( "UPDATE project_phases SET status = 'in_progress', completed_at = NULL, completed_by = NULL WHERE id = $1 AND project_id = $2", [phaseId, id] ); const allPhases = await db.query('SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order', [id]); const completedCount = allPhases.rows.filter(p => p.status === 'completed').length; const totalCount = allPhases.rows.length; const progress = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; const reopenedPhase = allPhases.rows.find(p => p.id === parseInt(phaseId)); await db.query( 'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4', [reopenedPhase ? reopenedPhase.phase_name : '', progress, 'active', id] ); res.json({ success: true, message: '阶段已重新打开', data: { progress } }); } catch (error) { console.error('重新打开阶段失败:', error); res.status(500).json({ success: false, message: '重新打开阶段失败' }); } }); router.put('/:id/phases/:phaseId/sub-item', async (req, res) => { try { const { id, phaseId } = req.params; const { sub_item_index, completed } = req.body; const phaseResult = await db.query('SELECT sub_items FROM project_phases WHERE id = $1 AND project_id = $2', [phaseId, id]); if (phaseResult.rows.length === 0) { return res.status(404).json({ success: false, message: '阶段不存在' }); } const subItems = phaseResult.rows[0].sub_items || []; if (sub_item_index >= 0 && sub_item_index < subItems.length) { subItems[sub_item_index].completed = completed; await db.query('UPDATE project_phases SET sub_items = $1 WHERE id = $2', [JSON.stringify(subItems), phaseId]); } res.json({ success: true, message: '子项状态更新成功' }); } catch (error) { console.error('更新子项状态失败:', error); res.status(500).json({ success: false, message: '更新子项状态失败' }); } });