diff --git a/tkpurchase/api/controllers/checkinController.js b/tkpurchase/api/controllers/checkinController.js index 27455c6..2c7c3d4 100644 --- a/tkpurchase/api/controllers/checkinController.js +++ b/tkpurchase/api/controllers/checkinController.js @@ -120,4 +120,59 @@ async function deleteCheckin(req, res) { } } -module.exports = { list, myCheckins, checkIn, checkOut, update, stats, deleteCheckin }; +// 체크아웃 + 보고 통합 (협력업체 포탈 전용) +async function checkOutWithReport(req, res) { + try { + const checkinId = parseInt(req.params.id); + const checkin = await checkinModel.findById(checkinId); + if (!checkin) return res.status(404).json({ success: false, error: '체크인 기록을 찾을 수 없습니다' }); + + // 소유권 검증: 협력업체 본인 체크인만 가능 + const companyId = req.user.partner_company_id; + if (companyId && checkin.company_id !== companyId) { + return res.status(403).json({ success: false, error: '권한이 없습니다' }); + } + + // schedule에서 work_description 가져오기 + let workContent = '작업 완료'; + if (checkin.schedule_id) { + const schedule = await scheduleModel.findById(checkin.schedule_id); + if (schedule && schedule.work_description) { + workContent = schedule.work_description; + } + } + + const reportData = { + reporter_id: req.user.user_id || req.user.id, + workers: req.body.workers || [], + work_content: workContent + }; + + const row = await checkinModel.checkOutWithReport(checkinId, reportData); + res.json({ success: true, data: row }); + } catch (err) { + console.error('Checkin checkOutWithReport error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +// 작업 이력 (협력업체 포탈) +async function myHistory(req, res) { + try { + const companyId = req.user.partner_company_id; + if (!companyId) { + return res.status(403).json({ success: false, error: '협력업체 계정이 아닙니다' }); + } + const { date_from, date_to, page, limit } = req.query; + const result = await checkinModel.findHistoryByCompany(companyId, { + dateFrom: date_from, dateTo: date_to, + page: parseInt(page) || 1, limit: parseInt(limit) || 20 + }); + res.json({ success: true, ...result }); + } catch (err) { + console.error('Checkin myHistory error:', err); + res.status(500).json({ success: false, error: err.message }); + } +} + +module.exports = { list, myCheckins, checkIn, checkOut, update, stats, deleteCheckin, checkOutWithReport, myHistory }; diff --git a/tkpurchase/api/models/checkinModel.js b/tkpurchase/api/models/checkinModel.js index d9f128c..75acaef 100644 --- a/tkpurchase/api/models/checkinModel.js +++ b/tkpurchase/api/models/checkinModel.js @@ -106,4 +106,114 @@ async function deleteCheckin(id) { } } -module.exports = { findBySchedule, findById, findTodayByCompany, checkIn, checkOut, update, resetCheckout, countActive, deleteCheckin }; +async function checkOutWithReport(id, reportData) { + const db = getPool(); + const checkin = await findById(id); + if (!checkin) throw new Error('체크인 기록을 찾을 수 없습니다'); + if (checkin.check_out_time) throw new Error('이미 체크아웃된 기록입니다'); + + const conn = await db.getConnection(); + try { + await conn.beginTransaction(); + + // report_seq: 같은 checkin_id 내 최대값 + 1 + const [seqRows] = await conn.query( + 'SELECT COALESCE(MAX(report_seq), 0) + 1 AS next_seq FROM partner_work_reports WHERE checkin_id = ?', + [id]); + const reportSeq = seqRows[0].next_seq; + + const workers = reportData.workers || []; + + const [reportResult] = await conn.query( + `INSERT INTO partner_work_reports (schedule_id, checkin_id, company_id, report_date, report_seq, reporter_id, actual_workers, work_content, progress_rate) + VALUES (?, ?, ?, CURDATE(), ?, ?, ?, ?, ?)`, + [checkin.schedule_id, id, checkin.company_id, reportSeq, + reportData.reporter_id, workers.length, + reportData.work_content || '작업 완료', 100]); + + const reportId = reportResult.insertId; + + // workers 삽입 + for (const w of workers) { + await conn.query( + `INSERT INTO work_report_workers (report_id, partner_worker_id, worker_name, hours_worked) + VALUES (?, ?, ?, ?)`, + [reportId, w.partner_worker_id || null, w.worker_name, w.hours_worked ?? 8.0]); + } + + // 체크아웃 + await conn.query('UPDATE partner_work_checkins SET check_out_time = NOW() WHERE id = ?', [id]); + + await conn.commit(); + return findById(id); + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } +} + +async function findHistoryByCompany(companyId, { dateFrom, dateTo, page = 1, limit = 20 } = {}) { + const db = getPool(); + + // 1. 체크인 페이지네이션 조회 + let sql = `SELECT pc.*, ps.work_description, ps.workplace_name, ps.start_date, ps.end_date + FROM partner_work_checkins pc + LEFT JOIN partner_schedules ps ON pc.schedule_id = ps.id + WHERE pc.company_id = ?`; + const params = [companyId]; + if (dateFrom) { sql += ' AND DATE(pc.check_in_time) >= ?'; params.push(dateFrom); } + if (dateTo) { sql += ' AND DATE(pc.check_in_time) <= ?'; params.push(dateTo); } + sql += ' ORDER BY pc.check_in_time DESC'; + + // count + const countSql = sql.replace(/SELECT pc\.\*.*?FROM/, 'SELECT COUNT(*) AS total FROM'); + const [countRows] = await db.query(countSql, params); + const total = countRows[0].total; + + const offset = (page - 1) * limit; + sql += ' LIMIT ? OFFSET ?'; + params.push(limit, offset); + const [checkins] = await db.query(sql, params); + + if (checkins.length === 0) return { data: [], total, page, limit }; + + // 2. 해당 체크인들의 reports 일괄 조회 + const checkinIds = checkins.map(c => c.id); + const [reports] = await db.query( + `SELECT * FROM partner_work_reports WHERE checkin_id IN (?) ORDER BY report_seq ASC`, + [checkinIds]); + + // 3. 해당 reports의 workers 일괄 조회 + const reportIds = reports.map(r => r.id); + let workers = []; + if (reportIds.length > 0) { + const [workerRows] = await db.query( + `SELECT * FROM work_report_workers WHERE report_id IN (?) ORDER BY id`, + [reportIds]); + workers = workerRows; + } + + // 조립 + const workersByReport = {}; + workers.forEach(w => { + if (!workersByReport[w.report_id]) workersByReport[w.report_id] = []; + workersByReport[w.report_id].push(w); + }); + + const reportsByCheckin = {}; + reports.forEach(r => { + r.workers = workersByReport[r.id] || []; + if (!reportsByCheckin[r.checkin_id]) reportsByCheckin[r.checkin_id] = []; + reportsByCheckin[r.checkin_id].push(r); + }); + + checkins.forEach(c => { + c.reports = reportsByCheckin[c.id] || []; + }); + + return { data: checkins, total, page, limit }; +} + +module.exports = { findBySchedule, findById, findTodayByCompany, checkIn, checkOut, update, resetCheckout, countActive, deleteCheckin, checkOutWithReport, findHistoryByCompany }; diff --git a/tkpurchase/api/routes/checkinRoutes.js b/tkpurchase/api/routes/checkinRoutes.js index f0193cc..03b8fa2 100644 --- a/tkpurchase/api/routes/checkinRoutes.js +++ b/tkpurchase/api/routes/checkinRoutes.js @@ -8,7 +8,9 @@ router.use(requireAuth); router.get('/', ctrl.stats); // dashboard stats router.get('/schedule/:scheduleId', ctrl.list); router.get('/my', ctrl.myCheckins); // partner portal +router.get('/my-history', ctrl.myHistory); // partner history router.post('/', ctrl.checkIn); // partner can do this +router.put('/:id/checkout-with-report', ctrl.checkOutWithReport); // partner portal simplified router.put('/:id/checkout', ctrl.checkOut); router.put('/:id', requirePage('purchasing_schedule'), ctrl.update); router.delete('/:id', requirePage('purchasing_schedule'), ctrl.deleteCheckin); diff --git a/tkpurchase/web/Dockerfile b/tkpurchase/web/Dockerfile index 65a21ec..1835df5 100644 --- a/tkpurchase/web/Dockerfile +++ b/tkpurchase/web/Dockerfile @@ -8,6 +8,7 @@ COPY workreport.html /usr/share/nginx/html/workreport.html COPY workreport-summary.html /usr/share/nginx/html/workreport-summary.html COPY accounts.html /usr/share/nginx/html/accounts.html COPY partner-portal.html /usr/share/nginx/html/partner-portal.html +COPY partner-history.html /usr/share/nginx/html/partner-history.html COPY static/ /usr/share/nginx/html/static/ EXPOSE 80 diff --git a/tkpurchase/web/partner-history.html b/tkpurchase/web/partner-history.html new file mode 100644 index 0000000..52497f2 --- /dev/null +++ b/tkpurchase/web/partner-history.html @@ -0,0 +1,68 @@ + + +
+ + +오늘의 작업 일정을 확인하고 업무현황을 입력해주세요.
+오늘의 작업 일정을 확인하세요.