diff --git a/scripts/migration-purchase-safety-patch2.sql b/scripts/migration-purchase-safety-patch2.sql new file mode 100644 index 0000000..35553d2 --- /dev/null +++ b/scripts/migration-purchase-safety-patch2.sql @@ -0,0 +1,10 @@ +-- migration-purchase-safety-patch2.sql +-- 협력업체 작업 신청 기능: status ENUM 확장 + requested_by 필드 추가 + +-- status ENUM에 requested, rejected 추가 (기존 값 모두 포함, DEFAULT 'scheduled' 유지) +ALTER TABLE partner_schedules + MODIFY COLUMN status ENUM('requested','scheduled','in_progress','completed','cancelled','rejected') + NOT NULL DEFAULT 'scheduled'; + +-- 신청자 필드 추가 +ALTER TABLE partner_schedules ADD COLUMN requested_by INT NULL AFTER registered_by; diff --git a/tkpurchase/api/controllers/scheduleController.js b/tkpurchase/api/controllers/scheduleController.js index ba1ac98..7502f5a 100644 --- a/tkpurchase/api/controllers/scheduleController.js +++ b/tkpurchase/api/controllers/scheduleController.js @@ -40,14 +40,104 @@ async function mySchedules(req, res) { if (!companyId) { return res.status(403).json({ success: false, error: '협력업체 계정이 아닙니다' }); } - const rows = await scheduleModel.findByCompanyToday(companyId); - res.json({ success: true, data: rows }); + const [schedules, requests] = await Promise.all([ + scheduleModel.findByCompanyToday(companyId), + scheduleModel.findRequestsByCompany(companyId) + ]); + res.json({ success: true, data: { schedules, requests } }); } catch (err) { console.error('Schedule mySchedules error:', err); res.status(500).json({ success: false, error: err.message }); } } +// 협력업체 작업 신청 +async function requestSchedule(req, res) { + try { + const companyId = req.user.partner_company_id; + if (!companyId) { + return res.status(403).json({ success: false, error: '협력업체 계정이 아닙니다' }); + } + const { start_date, work_description, workplace_name, expected_workers } = req.body; + if (!start_date) { + return res.status(400).json({ success: false, error: '작업일은 필수입니다' }); + } + const data = { + company_id: companyId, + project_id: null, + start_date, + end_date: start_date, + work_description: work_description || null, + workplace_name: workplace_name || null, + expected_workers: expected_workers || null, + requested_by: req.user.user_id || req.user.id, + status: 'requested' + }; + const row = await scheduleModel.create(data); + res.status(201).json({ success: true, data: row }); + } catch (err) { + console.error('Schedule requestSchedule error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 구매팀 승인 +async function approveRequest(req, res) { + try { + const schedule = await scheduleModel.findById(req.params.id); + if (!schedule) return res.status(404).json({ success: false, error: '일정을 찾을 수 없습니다' }); + if (schedule.status !== 'requested') { + return res.status(400).json({ success: false, error: '신청 상태가 아닙니다' }); + } + const updateData = { + status: 'scheduled', + registered_by: req.user.user_id || req.user.id + }; + if (req.body.project_id !== undefined) updateData.project_id = req.body.project_id; + if (req.body.workplace_name !== undefined) updateData.workplace_name = req.body.workplace_name; + if (req.body.start_date) updateData.start_date = req.body.start_date; + if (req.body.end_date) updateData.end_date = req.body.end_date; + if (req.body.expected_workers !== undefined) updateData.expected_workers = req.body.expected_workers; + if (req.body.work_description !== undefined) updateData.work_description = req.body.work_description; + + // update() handles dynamic fields including status and registered_by via the fields map + // But registered_by is not in the update() fields list, so use raw query for it + const db = require('../models/partnerModel').getPool(); + const fields = ['status = ?', 'registered_by = ?']; + const values = ['scheduled', updateData.registered_by]; + if (updateData.project_id !== undefined) { fields.push('project_id = ?'); values.push(updateData.project_id || null); } + if (updateData.workplace_name !== undefined) { fields.push('workplace_name = ?'); values.push(updateData.workplace_name || null); } + if (updateData.start_date) { fields.push('start_date = ?'); values.push(updateData.start_date); } + if (updateData.end_date) { fields.push('end_date = ?'); values.push(updateData.end_date); } + if (updateData.expected_workers !== undefined) { fields.push('expected_workers = ?'); values.push(updateData.expected_workers || null); } + if (updateData.work_description !== undefined) { fields.push('work_description = ?'); values.push(updateData.work_description || null); } + values.push(req.params.id); + await db.query(`UPDATE partner_schedules SET ${fields.join(', ')} WHERE id = ?`, values); + + const row = await scheduleModel.findById(req.params.id); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Schedule approveRequest error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 구매팀 반려 +async function rejectRequest(req, res) { + try { + const schedule = await scheduleModel.findById(req.params.id); + if (!schedule) return res.status(404).json({ success: false, error: '일정을 찾을 수 없습니다' }); + if (schedule.status !== 'requested') { + return res.status(400).json({ success: false, error: '신청 상태가 아닙니다' }); + } + const row = await scheduleModel.updateStatus(req.params.id, 'rejected'); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Schedule rejectRequest error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + // 일정 등록 async function create(req, res) { try { @@ -119,4 +209,4 @@ async function deleteSchedule(req, res) { } } -module.exports = { list, getById, mySchedules, create, update, updateStatus, deleteSchedule }; +module.exports = { list, getById, mySchedules, create, update, updateStatus, deleteSchedule, requestSchedule, approveRequest, rejectRequest }; diff --git a/tkpurchase/api/models/scheduleModel.js b/tkpurchase/api/models/scheduleModel.js index 20de57a..b271873 100644 --- a/tkpurchase/api/models/scheduleModel.js +++ b/tkpurchase/api/models/scheduleModel.js @@ -45,6 +45,19 @@ async function findByCompanyToday(companyId) { LEFT JOIN partner_companies pc ON ps.company_id = pc.id LEFT JOIN projects p ON ps.project_id = p.project_id WHERE ps.company_id = ? AND ps.start_date <= CURDATE() AND ps.end_date >= CURDATE() + AND ps.status NOT IN ('cancelled','rejected','requested') + ORDER BY ps.created_at DESC`, [companyId]); + return rows; +} + +async function findRequestsByCompany(companyId) { + const db = getPool(); + const [rows] = await db.query( + `SELECT ps.*, pc.company_name, su.name AS requested_by_name + FROM partner_schedules ps + LEFT JOIN partner_companies pc ON ps.company_id = pc.id + LEFT JOIN sso_users su ON ps.requested_by = su.user_id + WHERE ps.company_id = ? AND ps.status IN ('requested','rejected') ORDER BY ps.created_at DESC`, [companyId]); return rows; } @@ -63,13 +76,14 @@ async function findByProject(projectId) { async function create(data) { const db = getPool(); const [result] = await db.query( - `INSERT INTO partner_schedules (company_id, project_id, start_date, end_date, work_description, workplace_name, expected_workers, registered_by, notes) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO partner_schedules (company_id, project_id, start_date, end_date, work_description, workplace_name, expected_workers, registered_by, requested_by, status, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [data.company_id, data.project_id || null, data.start_date, data.end_date || data.start_date, data.work_description || null, data.workplace_name || null, data.expected_workers || null, - data.registered_by, data.notes || null]); + data.registered_by || null, data.requested_by || null, + data.status || 'scheduled', data.notes || null]); return findById(result.insertId); } @@ -103,4 +117,4 @@ async function deleteSchedule(id) { await db.query('DELETE FROM partner_schedules WHERE id = ?', [id]); } -module.exports = { findAll, findById, findByCompanyToday, findByProject, create, update, updateStatus, deleteSchedule }; +module.exports = { findAll, findById, findByCompanyToday, findRequestsByCompany, findByProject, create, update, updateStatus, deleteSchedule }; diff --git a/tkpurchase/api/routes/scheduleRoutes.js b/tkpurchase/api/routes/scheduleRoutes.js index 33feb92..da4da61 100644 --- a/tkpurchase/api/routes/scheduleRoutes.js +++ b/tkpurchase/api/routes/scheduleRoutes.js @@ -9,8 +9,11 @@ router.get('/', ctrl.list); router.get('/my', ctrl.mySchedules); // partner portal router.get('/:id', ctrl.getById); router.post('/', requirePage('purchasing_schedule'), ctrl.create); +router.post('/request', ctrl.requestSchedule); // 협력업체 작업 신청 router.put('/:id', requirePage('purchasing_schedule'), ctrl.update); router.put('/:id/status', requirePage('purchasing_schedule'), ctrl.updateStatus); +router.put('/:id/approve', requirePage('purchasing_schedule'), ctrl.approveRequest); +router.put('/:id/reject', requirePage('purchasing_schedule'), ctrl.rejectRequest); router.delete('/:id', requirePage('purchasing_schedule'), ctrl.deleteSchedule); module.exports = router; diff --git a/tkpurchase/web/partner-portal.html b/tkpurchase/web/partner-portal.html index 537c00e..a79943e 100644 --- a/tkpurchase/web/partner-portal.html +++ b/tkpurchase/web/partner-portal.html @@ -48,10 +48,36 @@
로딩 중...
- -오늘 예정된 작업 일정이 없습니다.
+ + + + +오늘 예정된 작업 일정이 없습니다. 작업이 필요하시면 아래에서 신청해주세요.
+