大改造:施工进度系统+模板管理+物流收货+分包评分

- 新增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,增加施工进度预览条和进入施工管理按钮
- 菜单更新:施工管理增加施工总览子菜单,后台管理增加工程模板管理
This commit is contained in:
root
2026-05-15 12:25:08 +08:00
parent fad28741bd
commit cc5d96e226
13 changed files with 1761 additions and 648 deletions
+2
View File
@@ -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'));
+18
View File
@@ -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({
+121
View File
@@ -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;
+785 -621
View File
File diff suppressed because it is too large Load Diff
+69
View File
@@ -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;
+60 -1
View File
@@ -514,4 +514,63 @@ router.delete('/payment-infos/:infoId', async (req, res) => {
}
});
module.exports = router;
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: '删除分包商事件失败' });
}
});