From cc5d96e2264ee27aee0479f91e517e28101753db Mon Sep 17 00:00:00 2001 From: root Date: Fri, 15 May 2026 12:25:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E6=94=B9=E9=80=A0=EF=BC=9A=E6=96=BD?= =?UTF-8?q?=E5=B7=A5=E8=BF=9B=E5=BA=A6=E7=B3=BB=E7=BB=9F+=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=AE=A1=E7=90=86+=E7=89=A9=E6=B5=81=E6=94=B6?= =?UTF-8?q?=E8=B4=A7+=E5=88=86=E5=8C=85=E8=AF=84=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增5张数据库表:logistics_records, receiving_confirms, project_type_templates, project_phases, subcontractor_events - 修改3张表:projects(增加type_template_id/current_phase/phase_progress), purchase_orders(增加expected_delivery_date/receiving_status), subcontractors(增加score/initial_score) - 插入5套预设工程模板(配电安装/架空线路/电缆敷设/变电站/小型工程) - 后端新增:模板管理API、项目阶段管理API、收货确认API、分包商事件API - 后端修改:项目创建/预算签约支持模板选择,自动初始化阶段 - 前端新增:工程模板设计器(后台管理)、施工总览页、施工进度页(手机友好) - 前端修改:项目详情页重构为7个Tab,增加施工进度预览条和进入施工管理按钮 - 菜单更新:施工管理增加施工总览子菜单,后台管理增加工程模板管理 --- backend/app.js | 2 + backend/routes/budget.js | 18 + backend/routes/process-templates.js | 121 ++ backend/routes/projects.js | 1406 +++++++++-------- backend/routes/receiving.js | 69 + backend/routes/subcontractors.js | 61 +- frontend/src/App.tsx | 7 +- frontend/src/components/layout/MainLayout.tsx | 8 +- frontend/src/layouts/AdminLayout.tsx | 8 +- frontend/src/pages/admin/ProcessTemplates.tsx | 289 ++++ .../construction/ConstructionOverview.tsx | 127 ++ .../construction/ConstructionProgress.tsx | 248 +++ frontend/src/pages/projects/ProjectDetail.tsx | 45 +- 13 files changed, 1761 insertions(+), 648 deletions(-) create mode 100644 backend/routes/process-templates.js create mode 100644 backend/routes/receiving.js create mode 100644 frontend/src/pages/admin/ProcessTemplates.tsx create mode 100644 frontend/src/pages/construction/ConstructionOverview.tsx create mode 100644 frontend/src/pages/construction/ConstructionProgress.tsx diff --git a/backend/app.js b/backend/app.js index b2eed6d..afb236d 100644 --- a/backend/app.js +++ b/backend/app.js @@ -65,6 +65,8 @@ app.use('/api/payment-execution', require('./routes/payment-execution')); app.use('/api/verifications-new', require('./routes/verifications-new')); app.use('/api/returns', require('./routes/returns')); app.use('/api/project-materials', require('./routes/project-materials')); +app.use('/api/process-templates', require('./routes/process-templates')); +app.use('/api/receiving', require('./routes/receiving')); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../frontend/dist/index.html')); diff --git a/backend/routes/budget.js b/backend/routes/budget.js index e08a686..e3e31c0 100644 --- a/backend/routes/budget.js +++ b/backend/routes/budget.js @@ -255,6 +255,7 @@ router.put('/:id/sign', authenticate, async (req, res) => { ); let project_id = projectResult.rows[0].project_id; + const { type_template_id } = req.body; if (!project_id) { const projResult = await db.query( `INSERT INTO projects (name, customer_id, project_manager_id, status, created_at, updated_at) @@ -265,6 +266,23 @@ router.put('/:id/sign', authenticate, async (req, res) => { ); project_id = projResult.rows[0].id; await db.query('UPDATE budget_projects SET project_id = $1 WHERE id = $2', [project_id, id]); + if (type_template_id) { + 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)`, + [project_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 WHERE id = $3', [type_template_id, firstPhase ? firstPhase.name : '', project_id]); + } + } + } } res.json({ diff --git a/backend/routes/process-templates.js b/backend/routes/process-templates.js new file mode 100644 index 0000000..10a5042 --- /dev/null +++ b/backend/routes/process-templates.js @@ -0,0 +1,121 @@ +const express = require('express'); +const db = require('../db'); +const router = express.Router(); + +router.get('/', async (req, res) => { + try { + const result = await db.query( + 'SELECT id, name, description, phases, is_system, created_at, updated_at FROM project_type_templates ORDER BY is_system DESC, created_at ASC' + ); + res.json({ success: true, data: result.rows }); + } catch (error) { + console.error('获取模板列表失败:', error); + res.status(500).json({ success: false, message: '获取模板列表失败' }); + } +}); + +router.get('/:id', async (req, res) => { + try { + const result = await db.query( + 'SELECT id, name, description, phases, is_system, created_at, updated_at FROM project_type_templates WHERE id = $1', + [req.params.id] + ); + if (result.rows.length === 0) { + return res.status(404).json({ success: false, message: '模板不存在' }); + } + res.json({ success: true, data: result.rows[0] }); + } catch (error) { + console.error('获取模板详情失败:', error); + res.status(500).json({ success: false, message: '获取模板详情失败' }); + } +}); + +router.post('/', async (req, res) => { + try { + const { name, description, phases } = req.body; + if (!name) { + return res.status(400).json({ success: false, message: '模板名称不能为空' }); + } + if (!phases || !Array.isArray(phases) || phases.length === 0) { + return res.status(400).json({ success: false, message: '模板阶段不能为空' }); + } + const result = await db.query( + 'INSERT INTO project_type_templates (name, description, phases, is_system) VALUES ($1, $2, $3, false) RETURNING id', + [name, description || '', JSON.stringify(phases)] + ); + res.json({ success: true, message: '模板创建成功', data: { id: result.rows[0].id } }); + } catch (error) { + console.error('创建模板失败:', error); + res.status(500).json({ success: false, message: '创建模板失败' }); + } +}); + +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const existing = await db.query('SELECT is_system FROM project_type_templates WHERE id = $1', [id]); + if (existing.rows.length === 0) { + return res.status(404).json({ success: false, message: '模板不存在' }); + } + if (existing.rows[0].is_system) { + return res.status(403).json({ success: false, message: '系统预设模板不可修改' }); + } + const { name, description, phases } = req.body; + const updates = []; + const values = []; + let paramIndex = 1; + if (name !== undefined) { updates.push(`name = $${paramIndex++}`); values.push(name); } + if (description !== undefined) { updates.push(`description = $${paramIndex++}`); values.push(description); } + if (phases !== undefined) { updates.push(`phases = $${paramIndex++}`); values.push(JSON.stringify(phases)); } + if (updates.length === 0) { + return res.status(400).json({ success: false, message: '没有更新数据' }); + } + updates.push('updated_at = NOW()'); + values.push(id); + await db.query(`UPDATE project_type_templates SET ${updates.join(', ')} WHERE id = $${paramIndex}`, values); + res.json({ success: true, message: '模板更新成功' }); + } catch (error) { + console.error('更新模板失败:', error); + res.status(500).json({ success: false, message: '更新模板失败' }); + } +}); + +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + const existing = await db.query('SELECT is_system FROM project_type_templates WHERE id = $1', [id]); + if (existing.rows.length === 0) { + return res.status(404).json({ success: false, message: '模板不存在' }); + } + if (existing.rows[0].is_system) { + return res.status(403).json({ success: false, message: '系统预设模板不可删除' }); + } + await db.query('DELETE FROM project_type_templates WHERE id = $1', [id]); + res.json({ success: true, message: '模板删除成功' }); + } catch (error) { + console.error('删除模板失败:', error); + res.status(500).json({ success: false, message: '删除模板失败' }); + } +}); + +router.post('/:id/copy', async (req, res) => { + try { + const { id } = req.params; + const { name } = req.body; + const existing = await db.query('SELECT name, description, phases FROM project_type_templates WHERE id = $1', [id]); + if (existing.rows.length === 0) { + return res.status(404).json({ success: false, message: '模板不存在' }); + } + const src = existing.rows[0]; + const result = await db.query( + 'INSERT INTO project_type_templates (name, description, phases, is_system) VALUES ($1, $2, $3, false) RETURNING id', + [name || `${src.name} (副本)`, src.description, src.phases] + ); + res.json({ success: true, message: '模板复制成功', data: { id: result.rows[0].id } }); + } catch (error) { + console.error('复制模板失败:', error); + res.status(500).json({ success: false, message: '复制模板失败' }); + } +}); + +module.exports = router; diff --git a/backend/routes/projects.js b/backend/routes/projects.js index 373e75e..b980503 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -1,626 +1,790 @@ -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 } = req.body; - - const projectCode = code || 'PROJ' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + - String(Math.floor(Math.random() * 10000)).padStart(4, '0'); - +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 projects (name, code, customer_id, project_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) + `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`, - [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: 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) + [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) + 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) + 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; \ No newline at end of file + 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: '更新子项状态失败' }); + } +}); diff --git a/backend/routes/receiving.js b/backend/routes/receiving.js new file mode 100644 index 0000000..54d0a3e --- /dev/null +++ b/backend/routes/receiving.js @@ -0,0 +1,69 @@ +const express = require('express'); +const db = require('../db'); +const router = express.Router(); + +router.post('/', async (req, res) => { + try { + const { purchase_order_id, logistics_record_id, confirm_date, receiver_name, receiver_phone, received_location, is_complete, missing_items, damage_items, photo_urls, remark, confirmed_by } = req.body; + if (!purchase_order_id) { + return res.status(400).json({ success: false, message: '采购订单ID不能为空' }); + } + const result = await db.query( + `INSERT INTO receiving_confirms (purchase_order_id, logistics_record_id, confirm_date, receiver_name, receiver_phone, received_location, is_complete, missing_items, damage_items, photo_urls, remark, confirmed_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id`, + [purchase_order_id, logistics_record_id || null, confirm_date || new Date().toISOString().slice(0, 10), receiver_name || '', receiver_phone || '', received_location || '', is_complete !== false, missing_items || '', damage_items || '', photo_urls || [], remark || '', confirmed_by || null] + ); + if (is_complete) { + await db.query("UPDATE purchase_orders SET receiving_status = 'received' WHERE id = $1", [purchase_order_id]); + } else { + await db.query("UPDATE purchase_orders SET receiving_status = 'partial_received' WHERE id = $1", [purchase_order_id]); + } + if (logistics_record_id) { + await db.query("UPDATE logistics_records SET status = 'arrived', final_arrival_date = CURRENT_DATE WHERE id = $1", [logistics_record_id]); + } + res.json({ success: true, message: '收货确认成功', data: { id: result.rows[0].id } }); + } catch (error) { + console.error('收货确认失败:', error); + res.status(500).json({ success: false, message: '收货确认失败' }); + } +}); + +router.get('/:purchase_order_id', async (req, res) => { + try { + const result = await db.query( + 'SELECT * FROM receiving_confirms WHERE purchase_order_id = $1 ORDER BY created_at DESC', + [req.params.purchase_order_id] + ); + res.json({ success: true, data: result.rows }); + } catch (error) { + console.error('获取收货记录失败:', error); + res.status(500).json({ success: false, message: '获取收货记录失败' }); + } +}); + +router.put('/:id', async (req, res) => { + try { + const { id } = req.params; + const fields = ['confirm_date', 'receiver_name', 'receiver_phone', 'received_location', 'is_complete', 'missing_items', 'damage_items', 'photo_urls', 'remark']; + const updates = []; + const values = []; + let paramIndex = 1; + for (const field of fields) { + if (req.body[field] !== undefined) { + updates.push(`${field} = $${paramIndex++}`); + values.push(req.body[field]); + } + } + if (updates.length === 0) { + return res.status(400).json({ success: false, message: '没有更新数据' }); + } + values.push(id); + await db.query(`UPDATE receiving_confirms SET ${updates.join(', ')} WHERE id = $${paramIndex}`, values); + res.json({ success: true, message: '收货记录更新成功' }); + } catch (error) { + console.error('更新收货记录失败:', error); + res.status(500).json({ success: false, message: '更新收货记录失败' }); + } +}); + +module.exports = router; diff --git a/backend/routes/subcontractors.js b/backend/routes/subcontractors.js index 2329640..2d5ff74 100644 --- a/backend/routes/subcontractors.js +++ b/backend/routes/subcontractors.js @@ -514,4 +514,63 @@ router.delete('/payment-infos/:infoId', async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; +router.get('/:id/events', async (req, res) => { + try { + const result = await db.query( + 'SELECT * FROM subcontractor_events WHERE subcontractor_id = $1 ORDER BY event_date DESC, created_at DESC', + [req.params.id] + ); + res.json({ success: true, data: result.rows }); + } catch (error) { + console.error('获取分包商事件失败:', error); + res.status(500).json({ success: false, message: '获取分包商事件失败' }); + } +}); + +router.post('/:id/events', async (req, res) => { + try { + const { id } = req.params; + const { project_id, event_type, score_change, event_date, title, description, created_by } = req.body; + if (!event_type || !score_change || !event_date || !title) { + return res.status(400).json({ success: false, message: '缺少必填字段' }); + } + if (!['positive', 'negative'].includes(event_type)) { + return res.status(400).json({ success: false, message: '事件类型必须是positive或negative' }); + } + const actualChange = event_type === 'positive' ? Math.abs(score_change) : -Math.abs(score_change); + const result = await db.query( + `INSERT INTO subcontractor_events (subcontractor_id, project_id, event_type, score_change, event_date, title, description, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`, + [id, project_id || null, event_type, actualChange, event_date, title, description || '', created_by || null] + ); + await db.query( + 'UPDATE subcontractors SET score = GREATEST(0, score + $1) WHERE id = $2', + [actualChange, id] + ); + res.json({ success: true, message: '事件添加成功', data: { id: result.rows[0].id } }); + } catch (error) { + console.error('添加分包商事件失败:', error); + res.status(500).json({ success: false, message: '添加分包商事件失败' }); + } +}); + +router.delete('/:id/events/:eventId', async (req, res) => { + try { + const { id, eventId } = req.params; + const eventResult = await db.query('SELECT score_change FROM subcontractor_events WHERE id = $1 AND subcontractor_id = $2', [eventId, id]); + if (eventResult.rows.length === 0) { + return res.status(404).json({ success: false, message: '事件不存在' }); + } + const scoreChange = eventResult.rows[0].score_change; + await db.query('DELETE FROM subcontractor_events WHERE id = $1', [eventId]); + await db.query( + 'UPDATE subcontractors SET score = GREATEST(0, LEAST(200, score - $1)) WHERE id = $2', + [scoreChange, id] + ); + res.json({ success: true, message: '事件删除成功' }); + } catch (error) { + console.error('删除分包商事件失败:', error); + res.status(500).json({ success: false, message: '删除分包商事件失败' }); + } +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c83bca8..e7a61a1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -50,11 +50,14 @@ import BudgetProjectDetail from './pages/budget/BudgetProjectDetail' import ConstructionList from './pages/construction' import ConstructionLog from './pages/construction/ConstructionLog' import ConstructionMilestones from './pages/construction/ConstructionMilestones' +import ConstructionOverview from './pages/construction/ConstructionOverview' +import ConstructionProgress from './pages/construction/ConstructionProgress' // 后台管理 import AdminLayout from './layouts/AdminLayout' import BackupPage from './pages/admin/BackupPage' import ProcessManagement from './pages/admin/ProcessManagement' +import ProcessTemplates from './pages/admin/ProcessTemplates' import AboutPage from './pages/admin/AboutPage' // 布局组件 @@ -137,9 +140,10 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> + } /> } /> } /> } /> @@ -173,6 +177,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/MainLayout.tsx b/frontend/src/components/layout/MainLayout.tsx index 078d7b9..c34e936 100644 --- a/frontend/src/components/layout/MainLayout.tsx +++ b/frontend/src/components/layout/MainLayout.tsx @@ -68,7 +68,13 @@ const menuItems = [ { key: '/construction', icon: , - label: '施工管理' + label: '施工管理', + children: [ + { + key: '/construction', + label: '施工总览' + } + ] }, { key: 'approval', diff --git a/frontend/src/layouts/AdminLayout.tsx b/frontend/src/layouts/AdminLayout.tsx index 811c042..81ae094 100644 --- a/frontend/src/layouts/AdminLayout.tsx +++ b/frontend/src/layouts/AdminLayout.tsx @@ -8,7 +8,8 @@ import { DatabaseOutlined, InfoCircleOutlined, ArrowLeftOutlined, - SettingOutlined + SettingOutlined, + AppstoreOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; @@ -34,6 +35,11 @@ const AdminLayout: React.FC = () => { icon: , label: '流程管理' }, + { + key: '/admin/process-templates', + icon: , + label: '工程模板管理' + }, { key: '/admin/logs', icon: , diff --git a/frontend/src/pages/admin/ProcessTemplates.tsx b/frontend/src/pages/admin/ProcessTemplates.tsx new file mode 100644 index 0000000..56f95d6 --- /dev/null +++ b/frontend/src/pages/admin/ProcessTemplates.tsx @@ -0,0 +1,289 @@ +import React, { useState, useEffect } from 'react'; +import { Card, Button, Table, Space, Modal, Form, Input, Select, InputNumber, Tag, Steps, message, Popconfirm, Drawer, List, Checkbox, Radio, Empty, Spin, Typography, Row, Col } from 'antd'; +import { PlusOutlined, CopyOutlined, EditOutlined, DeleteOutlined, EyeOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; +import apiClient from '../../utils/request'; + +const { Title, Text, Paragraph } = Typography; +const { Option } = Select; +const { TextArea } = Input; + +interface PhaseItem { + order: number; + name: string; + key?: string; + type: 'serial' | 'parallel'; + depends: number[]; + sub_items: string[]; +} + +interface Template { + id: number; + name: string; + description: string; + phases: PhaseItem[]; + is_system: boolean; + created_at: string; +} + +const ProcessTemplates: React.FC = () => { + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(false); + const [createModalVisible, setCreateModalVisible] = useState(false); + const [viewDrawerVisible, setViewDrawerVisible] = useState(false); + const [currentTemplate, setCurrentTemplate] = useState