329 lines
12 KiB
JavaScript
329 lines
12 KiB
JavaScript
const express = require('express');
|
|
const db = require('../db');
|
|
const { authenticate, requireAdmin } = require('../middleware/auth');
|
|
|
|
const router = express.Router();
|
|
|
|
// 获取施工项目列表
|
|
router.get('/my-projects', async (req, res) => {
|
|
try {
|
|
const result = 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.status IN ('active', 'in_progress', 'planning', 'pending', 'completed')
|
|
ORDER BY p.created_at DESC
|
|
`);
|
|
|
|
const projects = result.rows.map(project => ({
|
|
...project,
|
|
latest_log: null,
|
|
progress: 0
|
|
}));
|
|
|
|
res.json({ success: true, data: projects, count: projects.length });
|
|
} catch (error) {
|
|
console.error('获取施工项目失败:', error);
|
|
res.status(500).json({ success: false, message: '操作失败' });
|
|
}
|
|
});
|
|
|
|
// ==================== 施工日志 ====================
|
|
|
|
// 获取项目施工日志
|
|
router.get('/projects/:projectId/logs', async (req, res) => {
|
|
try {
|
|
const { projectId } = req.params;
|
|
const result = await db.query(
|
|
'SELECT * FROM construction_logs WHERE project_id = $1 ORDER BY log_date DESC, created_at DESC',
|
|
[projectId]
|
|
);
|
|
res.json({ success: true, data: result.rows });
|
|
} catch (error) {
|
|
console.error('获取施工日志失败:', error);
|
|
res.status(500).json({ success: false, message: '获取施工日志失败' });
|
|
}
|
|
});
|
|
|
|
// 新增施工日志
|
|
router.post('/projects/:projectId/logs', authenticate, async (req, res) => {
|
|
try {
|
|
const { projectId } = req.params;
|
|
const { log_date, weather, work_content, next_plan, issues, photos } = req.body;
|
|
const userId = req.user?.id || req.user?.userId;
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO construction_logs (project_id, log_date, weather, recorded_by, work_content, next_plan, issues, photos)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
|
|
[projectId, log_date, weather || 'sunny', userId, work_content || '', next_plan || '', issues || '', photos || []]
|
|
);
|
|
res.json({ success: true, data: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('新增施工日志失败:', error);
|
|
res.status(500).json({ success: false, message: '新增施工日志失败' });
|
|
}
|
|
});
|
|
|
|
// 删除施工日志
|
|
router.delete('/logs/:logId', authenticate, async (req, res) => {
|
|
try {
|
|
const { logId } = req.params;
|
|
const result = await db.query('DELETE FROM construction_logs WHERE id = $1 RETURNING id', [logId]);
|
|
if (result.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: '日志不存在' });
|
|
}
|
|
res.json({ success: true, message: '删除成功' });
|
|
} catch (error) {
|
|
console.error('删除施工日志失败:', error);
|
|
res.status(500).json({ success: false, message: '删除施工日志失败' });
|
|
}
|
|
});
|
|
|
|
// ==================== 施工进度 ====================
|
|
|
|
// 获取项目施工进度(包含phases和项目信息)
|
|
router.get('/projects/:projectId/progress', async (req, res) => {
|
|
try {
|
|
const { projectId } = 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`,
|
|
[projectId]
|
|
);
|
|
|
|
if (projectResult.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: '项目不存在' });
|
|
}
|
|
|
|
// 获取阶段
|
|
const phasesResult = await db.query(
|
|
'SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order',
|
|
[projectId]
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
project: projectResult.rows[0],
|
|
phases: phasesResult.rows
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取施工进度失败:', error);
|
|
res.status(500).json({ success: false, message: '获取施工进度失败' });
|
|
}
|
|
});
|
|
|
|
// 完成阶段
|
|
router.post('/projects/:projectId/phases/:phaseId/complete', authenticate, async (req, res) => {
|
|
try {
|
|
const { projectId, phaseId } = req.params;
|
|
const { remark, photos } = req.body;
|
|
const userId = req.user?.id || req.user?.userId;
|
|
|
|
// 标记阶段完成
|
|
await db.query(
|
|
`UPDATE project_phases SET status = 'completed', completed_at = CURRENT_TIMESTAMP, completed_by = $1, remark = COALESCE(remark, '') || $2, photos = COALESCE(photos, '{}') || $3 WHERE id = $4 AND project_id = $5`,
|
|
[userId, remark ? '\n' + remark : '', photos || [], phaseId, projectId]
|
|
);
|
|
|
|
// 计算总体进度
|
|
const progressResult = await db.query(
|
|
'SELECT COUNT(*) as total, COUNT(CASE WHEN status = $1 THEN 1 END) as completed FROM project_phases WHERE project_id = $2',
|
|
['completed', projectId]
|
|
);
|
|
const total = parseInt(progressResult.rows[0].total);
|
|
const completed = parseInt(progressResult.rows[0].completed);
|
|
const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
|
|
// 找下一个待开始的阶段
|
|
const nextPhaseResult = await db.query(
|
|
`SELECT * FROM project_phases WHERE project_id = $1 AND status = 'pending' ORDER BY phase_order LIMIT 1`,
|
|
[projectId]
|
|
);
|
|
|
|
if (nextPhaseResult.rows.length > 0) {
|
|
await db.query(
|
|
`UPDATE project_phases SET status = 'in_progress', started_at = CURRENT_TIMESTAMP WHERE id = $1`,
|
|
[nextPhaseResult.rows[0].id]
|
|
);
|
|
// 更新项目当前阶段
|
|
await db.query(
|
|
`UPDATE projects SET current_phase = $1, phase_progress = $2 WHERE id = $3`,
|
|
[nextPhaseResult.rows[0].phase_name, progress, projectId]
|
|
);
|
|
} else {
|
|
// 所有阶段完成
|
|
await db.query(
|
|
`UPDATE projects SET current_phase = '已完成', phase_progress = 100, status = 'completed' WHERE id = $1`,
|
|
[projectId]
|
|
);
|
|
}
|
|
|
|
res.json({ success: true, progress });
|
|
} catch (error) {
|
|
console.error('完成阶段失败:', error);
|
|
res.status(500).json({ success: false, message: '完成阶段失败' });
|
|
}
|
|
});
|
|
|
|
// 重新打开阶段
|
|
router.put('/phases/:phaseId/reopen', authenticate, async (req, res) => {
|
|
try {
|
|
const { phaseId } = req.params;
|
|
|
|
await db.query(
|
|
`UPDATE project_phases SET status = 'in_progress', completed_at = NULL, completed_by = NULL WHERE id = $1`,
|
|
[phaseId]
|
|
);
|
|
|
|
// 重新计算进度
|
|
const phaseResult = await db.query('SELECT project_id FROM project_phases WHERE id = $1', [phaseId]);
|
|
if (phaseResult.rows.length > 0) {
|
|
const projectId = phaseResult.rows[0].project_id;
|
|
const progressResult = await db.query(
|
|
'SELECT COUNT(*) as total, COUNT(CASE WHEN status = $1 THEN 1 END) as completed FROM project_phases WHERE project_id = $2',
|
|
['completed', projectId]
|
|
);
|
|
const total = parseInt(progressResult.rows[0].total);
|
|
const completed = parseInt(progressResult.rows[0].completed);
|
|
const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
|
|
await db.query(
|
|
`UPDATE projects SET phase_progress = $1, status = 'active' WHERE id = $2`,
|
|
[progress, projectId]
|
|
);
|
|
}
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('重新打开阶段失败:', error);
|
|
res.status(500).json({ success: false, message: '重新打开阶段失败' });
|
|
}
|
|
});
|
|
|
|
// 更新子项状态(支持完成时间、照片、与资料管理共享)
|
|
router.put('/phases/:phaseId/sub-item', authenticate, async (req, res) => {
|
|
try {
|
|
const { phaseId } = req.params;
|
|
const { itemIndex, completed, completed_at, photos } = req.body;
|
|
const userId = req.user?.id || req.user?.userId;
|
|
|
|
const phaseResult = await db.query('SELECT sub_items, project_id FROM project_phases WHERE id = $1', [phaseId]);
|
|
if (phaseResult.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: '阶段不存在' });
|
|
}
|
|
|
|
const subItems = phaseResult.rows[0].sub_items || [];
|
|
const projectId = phaseResult.rows[0].project_id;
|
|
|
|
if (itemIndex >= 0 && itemIndex < subItems.length) {
|
|
subItems[itemIndex].completed = completed;
|
|
if (completed) {
|
|
subItems[itemIndex].completed_at = completed_at || new Date().toISOString();
|
|
if (photos && photos.length > 0) {
|
|
subItems[itemIndex].photos = photos;
|
|
// 同步写入 project_documents 实现与资料管理共享
|
|
for (const url of photos) {
|
|
const fileName = url.split('/').pop() || 'photo.jpg';
|
|
await db.query(
|
|
`INSERT INTO project_documents (project_id, doc_type, file_name, file_url, description, uploaded_by)
|
|
VALUES ($1, 'image', $2, $3, $4, $5)`,
|
|
[projectId, fileName, url, `阶段子项: ${subItems[itemIndex].name}`, userId]
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
subItems[itemIndex].completed_at = null;
|
|
subItems[itemIndex].photos = [];
|
|
}
|
|
await db.query('UPDATE project_phases SET sub_items = $1 WHERE id = $2', [JSON.stringify(subItems), phaseId]);
|
|
}
|
|
|
|
res.json({ success: true, subItems });
|
|
} catch (error) {
|
|
console.error('更新子项状态失败:', error);
|
|
res.status(500).json({ success: false, message: '更新子项状态失败' });
|
|
}
|
|
});
|
|
|
|
// ==================== 资料管理 ====================
|
|
|
|
// 获取项目资料列表
|
|
router.get('/projects/:projectId/documents', async (req, res) => {
|
|
try {
|
|
const { projectId } = req.params;
|
|
const { doc_type } = req.query;
|
|
|
|
let query = 'SELECT d.*, u.name as uploader_name FROM project_documents d LEFT JOIN users u ON d.uploaded_by = u.id WHERE d.project_id = $1';
|
|
const params = [projectId];
|
|
|
|
if (doc_type) {
|
|
query += ' AND d.doc_type = $2';
|
|
params.push(doc_type);
|
|
}
|
|
|
|
query += ' ORDER BY d.created_at DESC';
|
|
|
|
const result = await db.query(query, params);
|
|
res.json({ success: true, data: result.rows });
|
|
} catch (error) {
|
|
console.error('获取项目资料失败:', error);
|
|
res.status(500).json({ success: false, message: '获取项目资料失败' });
|
|
}
|
|
});
|
|
|
|
// 上传项目资料
|
|
router.post('/projects/:projectId/documents', authenticate, async (req, res) => {
|
|
try {
|
|
const { projectId } = req.params;
|
|
const { doc_type, file_name, file_url, description } = req.body;
|
|
const userId = req.user?.id || req.user?.userId;
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO project_documents (project_id, doc_type, file_name, file_url, description, uploaded_by)
|
|
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
|
|
[projectId, doc_type || 'document', file_name, file_url, description || '', userId]
|
|
);
|
|
res.json({ success: true, data: result.rows[0] });
|
|
} catch (error) {
|
|
console.error('上传项目资料失败:', error);
|
|
res.status(500).json({ success: false, message: '上传项目资料失败' });
|
|
}
|
|
});
|
|
|
|
// 删除项目资料
|
|
router.delete('/documents/:docId', authenticate, async (req, res) => {
|
|
try {
|
|
const { docId } = req.params;
|
|
const userId = req.user?.id || req.user?.userId;
|
|
const isAdmin = req.user?.role === 'admin';
|
|
|
|
// 管理员或上传者可删除
|
|
const docResult = await db.query('SELECT * FROM project_documents WHERE id = $1', [docId]);
|
|
if (docResult.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: '资料不存在' });
|
|
}
|
|
|
|
if (!isAdmin && docResult.rows[0].uploaded_by !== userId) {
|
|
return res.status(403).json({ success: false, message: '无权删除' });
|
|
}
|
|
|
|
await db.query('DELETE FROM project_documents WHERE id = $1', [docId]);
|
|
res.json({ success: true, message: '删除成功' });
|
|
} catch (error) {
|
|
console.error('删除项目资料失败:', error);
|
|
res.status(500).json({ success: false, message: '删除项目资料失败' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|