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, 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 `); 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, project_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 (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); 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'] ); } } 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. 处理单价项 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/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 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 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 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, income: { total: totalIncome, by_category: incomeByCategory }, expense: { total: totalExpense, by_category: expenseByCategory, by_level1: expenseByLevel1 }, total_cost: totalExpense, profit: totalIncome - totalExpense } }); } 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, attachments } = 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]; const allAttachments = attachments || photos || phase.photos || []; 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, JSON.stringify(allAttachments), 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: '更新子项状态失败' }); } });