// js/work-report-review.js - 휴가 연동 완전판 import { API, getAuthHeaders } from '/js/api-config.js'; class WorkReportReviewManager { constructor() { this.workers = []; this.projects = []; this.workTypes = []; this.workStatusTypes = []; this.errorTypes = []; this.reports = []; this.filteredReports = []; this.attendanceData = []; // 휴가 정보 this.reviewedReports = new Set(); // 검토완료된 보고서 ID this.selectedReport = null; this.init(); } async init() { try { // 필수 DOM 요소 확인 const requiredElements = ['reportsTableBody', 'editPanelContent', 'totalReports']; const missingElements = requiredElements.filter(id => !document.getElementById(id)); if (missingElements.length > 0) { console.error('필수 DOM 요소가 없습니다:', missingElements); return; } this.showLoading(true); await this.loadData(); this.setupEventListeners(); this.setDefaultDates(); await this.loadReports(); this.showLoading(false); } catch (error) { this.showMessage('데이터 로드 중 오류가 발생했습니다.', 'error'); console.error('초기화 오류:', error); this.showLoading(false); } } async loadData() { try { // 기본 데이터 로딩 const [workersRes, projectsRes, workTypesRes, workStatusRes, errorTypesRes] = await Promise.all([ fetch(`${API}/workers`, { headers: getAuthHeaders() }), fetch(`${API}/projects/active/list`, { headers: getAuthHeaders() }), fetch(`${API}/daily-work-reports/work-types`, { headers: getAuthHeaders() }), fetch(`${API}/daily-work-reports/work-status-types`, { headers: getAuthHeaders() }), fetch(`${API}/daily-work-reports/error-types`, { headers: getAuthHeaders() }) ]); if (!workersRes.ok || !projectsRes.ok) { throw new Error(`API 응답 오류: Workers(${workersRes.status}), Projects(${projectsRes.status})`); } const allWorkers = await workersRes.json(); // 활성화된 작업자만 필터링 this.workers = allWorkers.filter(worker => { return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true; }); this.projects = await projectsRes.json(); // 새로운 API들 로드 (실패해도 기본값 사용) try { this.workTypes = workTypesRes.ok ? await workTypesRes.json() : [ {id: 1, name: 'Base'}, {id: 2, name: 'Vessel'}, {id: 3, name: 'Piping'} ]; this.workStatusTypes = workStatusRes.ok ? await workStatusRes.json() : [ {id: 1, name: '정규'}, {id: 2, name: '에러'} ]; this.errorTypes = errorTypesRes.ok ? await errorTypesRes.json() : [ {id: 1, name: '설계미스'}, {id: 2, name: '외주작업 불량'}, {id: 3, name: '입고지연'}, {id: 4, name: '작업 불량'} ]; } catch (error) { console.log('⚠️ 일부 API 사용 불가, 기본값 사용'); } // 휴가 정보 로드 await this.loadAttendanceData(); this.populateFilters(); console.log('기본 데이터 로드 완료'); } catch (error) { console.error('기본 데이터 로드 오류:', error); throw error; } } async loadAttendanceData() { try { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; if (!startDate || !endDate) return; // 휴가 정보 API 호출 (예상 엔드포인트) const response = await fetch(`${API}/attendance?start_date=${startDate}&end_date=${endDate}`, { headers: getAuthHeaders() }); if (response.ok) { this.attendanceData = await response.json(); console.log('휴가 정보 로드 완료:', this.attendanceData.length); } else if (response.status === 404) { console.log('⚠️ 휴가 API 없음, 더미 데이터 생성'); this.attendanceData = this.generateDummyAttendance(); } else { throw new Error(`휴가 정보 로드 실패: ${response.status}`); } } catch (error) { console.log('⚠️ 휴가 정보 로드 오류, 더미 데이터 사용:', error.message); this.attendanceData = this.generateDummyAttendance(); } } generateDummyAttendance() { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; if (!startDate || !endDate) return []; const dummyAttendance = []; const start = new Date(startDate); const end = new Date(endDate); // 날짜별로 작업자들의 출근 상태 생성 for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { const dateStr = d.toISOString().split('T')[0]; this.workers.forEach(worker => { const rand = Math.random(); let attendanceType = 'NORMAL'; // 기본값 // 5% 확률로 휴가 if (rand < 0.05) { const types = ['HALF_DAY', 'HALF_HALF_DAY', 'EARLY_LEAVE']; attendanceType = types[Math.floor(Math.random() * types.length)]; } dummyAttendance.push({ id: `att_${worker.worker_id}_${dateStr}`, worker_id: worker.worker_id, worker_name: worker.worker_name, date: dateStr, attendance_type: attendanceType, expected_hours: this.getExpectedHours(attendanceType) }); }); } return dummyAttendance; } getExpectedHours(attendanceType) { switch (attendanceType) { case 'HALF_DAY': return 4; // 반차 case 'HALF_HALF_DAY': return 6; // 반반차 case 'EARLY_LEAVE': return 2; // 조퇴 case 'NORMAL': default: return 8; // 일반근무 } } getAttendanceTypeName(attendanceType) { switch (attendanceType) { case 'HALF_DAY': return '반차'; case 'HALF_HALF_DAY': return '반반차'; case 'EARLY_LEAVE': return '조퇴'; case 'NORMAL': default: return '정상근무'; } } async loadReports() { try { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const workerId = document.getElementById('workerFilter').value; const projectId = document.getElementById('projectFilter').value; // 필수 조건 체크 if (!startDate || !endDate) { this.showMessage('시작 날짜와 종료 날짜는 필수입니다.', 'error'); return; } // 휴가 정보 먼저 로드 await this.loadAttendanceData(); let url = `${API}/daily-work-reports/search?`; const params = new URLSearchParams(); if (startDate) params.append('start_date', startDate); if (endDate) params.append('end_date', endDate); if (workerId) params.append('worker_id', workerId); if (projectId) params.append('project_id', projectId); // 페이지네이션 (일단 많이 가져오기) params.append('page', '1'); params.append('limit', '1000'); url += params.toString(); const response = await fetch(url, { headers: getAuthHeaders() }); if (response.status === 404 || response.status === 500) { // API가 아직 준비되지 않은 경우 더미 데이터 사용 this.reports = this.generateDummyData(); console.log('⚠️ API 응답 오류, 더미 데이터 사용:', response.status); if (response.status === 404) { this.showMessage('⚠️ 검토 API가 준비되지 않아 더미 데이터를 표시합니다.', 'warning'); } else { this.showMessage('⚠️ 서버 오류로 더미 데이터를 표시합니다.', 'warning'); } } else if (response.ok) { const result = await response.json(); this.reports = result.reports || []; console.log('검색 결과:', result.summary); } else { throw new Error(`보고서 로드 실패: ${response.status}`); } // 근무시간 검증 추가 this.validateWorkHours(); this.filteredReports = [...this.reports]; this.updateDashboard(); this.updateAlertsSection(); this.updateTable(); } catch (error) { console.log('⚠️ 네트워크 오류로 더미 데이터 사용:', error.message); // 더미 데이터로 대체 this.reports = this.generateDummyData(); this.validateWorkHours(); this.filteredReports = [...this.reports]; this.updateDashboard(); this.updateAlertsSection(); this.updateTable(); // 사용자에게는 부드러운 메시지 표시 this.showMessage('💡 데모 데이터로 화면을 구성했습니다.', 'info'); } } validateWorkHours() { // 각 보고서에 대해 근무시간 검증 this.reports.forEach(report => { const attendance = this.attendanceData.find(att => att.worker_id === report.worker_id && att.date === report.report_date ); if (attendance) { const expectedHours = attendance.expected_hours; const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date); report.expected_hours = expectedHours; report.actual_hours = actualHours; report.attendance_type = attendance.attendance_type; report.hours_status = this.getHoursStatus(actualHours, expectedHours); report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`); } else { // 휴가 정보가 없으면 기본 8시간으로 가정 const actualHours = this.getWorkerDailyHours(report.worker_id, report.report_date); report.expected_hours = 8; report.actual_hours = actualHours; report.attendance_type = 'NORMAL'; report.hours_status = this.getHoursStatus(actualHours, 8); report.is_reviewed = this.reviewedReports.has(`${report.worker_id}_${report.report_date}`); } }); } getWorkerDailyHours(workerId, date) { // 해당 작업자의 특정 날짜 총 작업시간 계산 return this.reports .filter(r => r.worker_id === workerId && r.report_date === date) .reduce((sum, r) => sum + (r.work_hours || 0), 0); } getHoursStatus(actualHours, expectedHours) { const tolerance = 0.5; // 30분 허용 오차 if (Math.abs(actualHours - expectedHours) <= tolerance) { return 'NORMAL'; // 정상 } else if (actualHours < expectedHours) { return 'UNDER'; // 부족 } else { return 'OVER'; // 초과 } } generateDummyData() { const today = new Date(); const dummyData = []; // 더미 데이터 생성 (최근 7일) for (let i = 0; i < 7; i++) { const date = new Date(today); date.setDate(date.getDate() - i); const dateStr = date.toISOString().split('T')[0]; this.workers.forEach(worker => { // 80% 확률로 보고서 생성 if (Math.random() > 0.2) { const numEntries = Math.floor(Math.random() * 3) + 1; // 1-3개 작업 for (let j = 0; j < numEntries; j++) { const workStatus = Math.random() > 0.9 ? 2 : 1; // 10% 확률로 에러 const workHours = Math.random() > 0.95 ? (Math.random() * 4 + 10) : // 5% 확률로 이상 시간 (10-14시간) (Math.random() * 6 + 2); // 정상 시간 (2-8시간) dummyData.push({ id: 1000000 + i * 100 + worker.worker_id * 10 + j, // 고유 정수 ID report_date: dateStr, worker_id: worker.worker_id, worker_name: worker.worker_name, project_id: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_id || 1, project_name: this.projects[Math.floor(Math.random() * this.projects.length)]?.project_name || 'Unknown', work_type_id: Math.floor(Math.random() * 3) + 1, work_type_name: this.workTypes[Math.floor(Math.random() * 3)]?.name || 'Base', work_status_id: workStatus, work_status_name: workStatus === 2 ? '에러' : '정규', error_type_id: workStatus === 2 ? Math.floor(Math.random() * 4) + 1 : null, error_type_name: workStatus === 2 ? this.errorTypes[Math.floor(Math.random() * 4)]?.name : null, work_hours: Math.round(workHours * 2) / 2, // 0.5시간 단위 created_at: `${dateStr}T${String(Math.floor(Math.random() * 24)).padStart(2, '0')}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}:00` }); } } }); } return dummyData.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); } populateFilters() { // 작업자 필터 const workerSelect = document.getElementById('workerFilter'); if (workerSelect) { workerSelect.innerHTML = ''; this.workers.forEach(worker => { workerSelect.innerHTML += ``; }); } // 프로젝트 필터 const projectSelect = document.getElementById('projectFilter'); if (projectSelect) { projectSelect.innerHTML = ''; this.projects.forEach(project => { projectSelect.innerHTML += ``; }); } } setDefaultDates() { const today = new Date(); const weekAgo = new Date(today); weekAgo.setDate(weekAgo.getDate() - 7); const startDateInput = document.getElementById('startDate'); const endDateInput = document.getElementById('endDate'); if (startDateInput) startDateInput.value = weekAgo.toISOString().split('T')[0]; if (endDateInput) endDateInput.value = today.toISOString().split('T')[0]; } updateDashboard() { // 날짜별, 작업자별로 그룹화하여 계산 const groupedReports = this.groupReportsByWorkerAndDate(); const groups = Object.values(groupedReports); const total = groups.length; const errors = groups.filter(group => group.some(r => r.work_status_id === 2)).length; const warnings = groups.filter(group => { const firstReport = group[0]; return firstReport.hours_status === 'UNDER' || firstReport.hours_status === 'OVER'; }).length; const reviewed = groups.filter(group => group[0].is_reviewed).length; document.getElementById('totalReports').textContent = total; document.getElementById('errorReports').textContent = errors; document.getElementById('warningReports').textContent = warnings; document.getElementById('missingReports').textContent = total - reviewed; // 미검토 건수 } updateAlertsSection() { const alertsList = document.getElementById('alertsList'); if (!alertsList) return; const alerts = []; const groupedReports = this.groupReportsByWorkerAndDate(); // 각 그룹별로 알림 체크 Object.values(groupedReports).forEach(group => { const firstReport = group[0]; const hasError = group.some(r => r.work_status_id === 2); const workerName = firstReport.worker_name; const date = firstReport.report_date; // 에러 발생 건 if (hasError) { const errorReports = group.filter(r => r.work_status_id === 2); alerts.push({ type: 'error', text: `${workerName} (${date}) - 에러 ${errorReports.length}건`, time: this.formatDate(date), priority: 1 }); } // 시간 불일치 if (firstReport.hours_status === 'UNDER' || firstReport.hours_status === 'OVER') { const statusText = firstReport.hours_status === 'UNDER' ? '부족' : '초과'; alerts.push({ type: 'warning', text: `${workerName} (${date}) - 시간 ${statusText} (${firstReport.actual_hours}h/${firstReport.expected_hours}h)`, time: this.formatDate(date), priority: 2 }); } // 검토 대기 (정상이지만 아직 검토되지 않음) if (!firstReport.is_reviewed && firstReport.hours_status === 'NORMAL' && !hasError) { alerts.push({ type: 'pending', text: `${workerName} (${date}) - 검토 대기`, time: this.formatDate(date), priority: 3 }); } }); // 우선순위별 정렬 alerts.sort((a, b) => a.priority - b.priority); // 알림 표시 if (alerts.length === 0) { alertsList.innerHTML = `