// management-dashboard.js - 관리자 대시보드 전용 스크립트 // ================================================================= // 🌐 통합 API 설정 import // ================================================================= import { API, getAuthHeaders, apiCall } from '/js/api-config.js'; // 전역 변수 let workers = []; let workData = []; let filteredWorkData = []; let currentDate = ''; let currentUser = null; // 권한 레벨 매핑 const ACCESS_LEVELS = { worker: 1, group_leader: 2, support_team: 3, admin: 4, system: 5 }; // 한국 시간 기준 오늘 날짜 가져오기 function getKoreaToday() { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } // 현재 로그인한 사용자 정보 가져오기 function getCurrentUser() { try { const token = localStorage.getItem('token'); if (!token) return null; const payloadBase64 = token.split('.')[1]; if (payloadBase64) { const payload = JSON.parse(atob(payloadBase64)); console.log('토큰에서 추출한 사용자 정보:', payload); return payload; } } catch (error) { console.log('토큰에서 사용자 정보 추출 실패:', error); } try { const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser'); if (userInfo) { const parsed = JSON.parse(userInfo); console.log('localStorage에서 가져온 사용자 정보:', parsed); return parsed; } } catch (error) { console.log('localStorage에서 사용자 정보 가져오기 실패:', error); } return null; } // 권한 체크 함수 function checkPermission() { currentUser = getCurrentUser(); if (!currentUser) { showMessage('로그인이 필요합니다.', 'error'); setTimeout(() => { window.location.href = '/'; }, 2000); return false; } const userAccessLevel = currentUser.access_level; const accessLevelValue = ACCESS_LEVELS[userAccessLevel] || 0; console.log('사용자 권한 체크:', { username: currentUser.username || currentUser.name, access_level: userAccessLevel, level_value: accessLevelValue, required_level: ACCESS_LEVELS.group_leader }); if (accessLevelValue < ACCESS_LEVELS.group_leader) { showMessage('그룹장 이상의 권한이 필요합니다. 현재 권한: ' + userAccessLevel, 'error'); setTimeout(() => { window.location.href = '/'; }, 3000); return false; } return true; } // 메시지 표시 function showMessage(message, type = 'info') { const container = document.getElementById('message-container'); container.innerHTML = `
${message}
`; if (type === 'success') { setTimeout(() => { hideMessage(); }, 5000); } } function hideMessage() { document.getElementById('message-container').innerHTML = ''; } // 로딩 표시 function showLoading() { document.getElementById('loadingSpinner').style.display = 'flex'; document.getElementById('summarySection').style.display = 'none'; document.getElementById('actionBar').style.display = 'none'; document.getElementById('workersSection').style.display = 'none'; document.getElementById('noDataMessage').style.display = 'none'; } function hideLoading() { document.getElementById('loadingSpinner').style.display = 'none'; } // 작업자 데이터 로드 async function loadWorkers() { try { console.log('작업자 데이터 로딩 중... (통합 API)'); const data = await apiCall(`${API}/workers`); workers = Array.isArray(data) ? data : (data.workers || []); console.log('✅ 작업자 로드 성공:', workers.length); } catch (error) { console.error('작업자 로딩 오류:', error); throw error; } } // 특정 날짜의 작업 데이터 로드 (개선된 버전) async function loadWorkData(date) { try { console.log(`${date} 날짜의 작업 데이터 로딩 중... (통합 API)`); // 1차: view_all=true로 전체 데이터 시도 let queryParams = `date=${date}&view_all=true`; console.log(`🔍 1차 시도: ${API}/daily-work-reports?${queryParams}`); let data = await apiCall(`${API}/daily-work-reports?${queryParams}`); workData = Array.isArray(data) ? data : (data.data || []); // 데이터가 없으면 다른 방법들 시도 if (workData.length === 0) { console.log('⚠️ view_all로 데이터 없음, 다른 방법 시도...'); // 2차: admin=true로 시도 queryParams = `date=${date}&admin=true`; console.log(`🔍 2차 시도: ${API}/daily-work-reports?${queryParams}`); data = await apiCall(`${API}/daily-work-reports?${queryParams}`); workData = Array.isArray(data) ? data : (data.data || []); if (workData.length === 0) { // 3차: 날짜 경로 파라미터로 시도 console.log(`🔍 3차 시도: ${API}/daily-work-reports/date/${date}`); data = await apiCall(`${API}/daily-work-reports/date/${date}`); workData = Array.isArray(data) ? data : (data.data || []); if (workData.length === 0) { // 4차: 기본 파라미터만으로 시도 console.log(`🔍 4차 시도: ${API}/daily-work-reports?date=${date}`); data = await apiCall(`${API}/daily-work-reports?date=${date}`); workData = Array.isArray(data) ? data : (data.data || []); } } } console.log(`✅ 최종 작업 데이터 로드 결과: ${workData.length}개`); // 디버깅을 위한 상세 로그 if (workData.length > 0) { console.log('📊 로드된 데이터 샘플:', workData.slice(0, 3)); const uniqueWorkers = [...new Set(workData.map(w => w.worker_name))]; console.log('👥 데이터에 포함된 작업자들:', uniqueWorkers); } else { console.log('❌ 해당 날짜에 작업 데이터가 없거나 접근 권한이 없습니다.'); } return workData; } catch (error) { console.error('작업 데이터 로딩 오류:', error); // 에러 시에도 빈 배열 반환하여 앱이 중단되지 않도록 workData = []; // 구체적인 에러 정보 표시 if (error.message.includes('403')) { console.log('🔒 권한 부족으로 인한 접근 제한'); throw new Error('해당 날짜의 데이터에 접근할 권한이 없습니다.'); } else if (error.message.includes('404')) { console.log('📭 해당 날짜에 데이터 없음'); throw new Error('해당 날짜에 입력된 작업 데이터가 없습니다.'); } else { throw error; } } } // 대시보드 데이터 로드 async function loadDashboardData() { const selectedDate = document.getElementById('selectedDate').value; if (!selectedDate) { showMessage('날짜를 선택해주세요.', 'error'); return; } currentDate = selectedDate; showLoading(); hideMessage(); try { // 병렬로 데이터 로드 await Promise.all([ loadWorkers(), loadWorkData(selectedDate) ]); // 데이터 분석 및 표시 const dashboardData = analyzeDashboardData(); displayDashboard(dashboardData); hideLoading(); } catch (error) { console.error('대시보드 데이터 로드 실패:', error); hideLoading(); showMessage('데이터를 불러오는 중 오류가 발생했습니다: ' + error.message, 'error'); // 에러 시 데이터 없음 메시지 표시 document.getElementById('noDataMessage').style.display = 'block'; } } // 대시보드 데이터 분석 (개선된 버전) function analyzeDashboardData() { console.log('대시보드 데이터 분석 시작'); // 작업자별 데이터 그룹화 const workerWorkData = {}; workData.forEach(work => { const workerId = work.worker_id; if (!workerWorkData[workerId]) { workerWorkData[workerId] = []; } workerWorkData[workerId].push(work); }); // 전체 통계 계산 const totalWorkers = workers.length; const workersWithData = Object.keys(workerWorkData).length; const workersWithoutData = totalWorkers - workersWithData; const totalHours = workData.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0); const totalEntries = workData.length; const errorCount = workData.filter(work => work.work_status_id === 2).length; // 작업자별 상세 분석 (개선된 버전) const workerAnalysis = workers.map(worker => { const workerWorks = workerWorkData[worker.worker_id] || []; const workerHours = workerWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0); // 작업 유형 분석 (실제 이름으로) const workTypes = [...new Set(workerWorks.map(work => work.work_type_name).filter(Boolean))]; // 프로젝트 분석 const workerProjects = [...new Set(workerWorks.map(work => work.project_name).filter(Boolean))]; // 기여자 분석 const workerContributors = [...new Set(workerWorks.map(work => work.created_by_name).filter(Boolean))]; // 상태 결정 (더 세밀한 기준) let status = 'missing'; if (workerWorks.length > 0) { if (workerHours >= 6) { status = 'completed'; // 6시간 이상을 완료로 간주 } else { status = 'partial'; // 1시간 이상이지만 6시간 미만은 부분입력 } } // 최근 업데이트 시간 const lastUpdate = workerWorks.length > 0 ? new Date(Math.max(...workerWorks.map(work => new Date(work.created_at)))) : null; return { ...worker, status, totalHours: Math.round(workerHours * 10) / 10, // 소수점 1자리로 반올림 entryCount: workerWorks.length, workTypes, // 작업 유형 배열 (실제 이름) projects: workerProjects, contributors: workerContributors, lastUpdate, works: workerWorks }; }); const summary = { totalWorkers, completedWorkers: workerAnalysis.filter(w => w.status === 'completed').length, missingWorkers: workerAnalysis.filter(w => w.status === 'missing').length, partialWorkers: workerAnalysis.filter(w => w.status === 'partial').length, totalHours: Math.round(totalHours * 10) / 10, totalEntries, errorCount }; console.log('대시보드 분석 결과:', { summary, workerAnalysis }); return { summary, workers: workerAnalysis, date: currentDate }; } // 대시보드 표시 function displayDashboard(data) { displaySummary(data.summary); displayWorkers(data.workers); // 섹션 표시 document.getElementById('summarySection').style.display = 'block'; document.getElementById('actionBar').style.display = 'flex'; document.getElementById('workersSection').style.display = 'block'; // 필터링 설정 filteredWorkData = data.workers; setupFiltering(); console.log('✅ 대시보드 표시 완료'); } // 요약 섹션 표시 function displaySummary(summary) { document.getElementById('totalWorkers').textContent = summary.totalWorkers; document.getElementById('completedWorkers').textContent = summary.completedWorkers; document.getElementById('missingWorkers').textContent = summary.missingWorkers; document.getElementById('totalHours').textContent = summary.totalHours + 'h'; document.getElementById('totalEntries').textContent = summary.totalEntries; document.getElementById('errorCount').textContent = summary.errorCount; } // 작업자 목록 표시 (테이블 형태로 개선) function displayWorkers(workersData) { const tableBody = document.getElementById('workersTableBody'); tableBody.innerHTML = ''; if (workersData.length === 0) { tableBody.innerHTML = ` 표시할 작업자가 없습니다. `; return; } workersData.forEach(worker => { const row = createWorkerRow(worker); tableBody.appendChild(row); }); } // 작업자 테이블 행 생성 (개선된 버전) function createWorkerRow(worker) { const row = document.createElement('tr'); const statusText = { completed: '✅ 완료', missing: '❌ 미입력', partial: '⚠️ 부분입력' }; const statusClass = { completed: 'completed', missing: 'missing', partial: 'partial' }; // 작업 유형 태그 생성 (실제 이름으로) const workTypeTags = worker.workTypes && worker.workTypes.length > 0 ? worker.workTypes.map(type => `${type}`).join('') : '없음'; // 프로젝트 태그 생성 const projectTags = worker.projects && worker.projects.length > 0 ? worker.projects.map(project => `${project}`).join('') : '없음'; // 기여자 태그 생성 const contributorTags = worker.contributors && worker.contributors.length > 0 ? worker.contributors.map(contributor => `${contributor}`).join('') : '없음'; // 시간에 따른 스타일 클래스 let hoursClass = 'zero'; if (worker.totalHours > 0) { hoursClass = worker.totalHours >= 6 ? 'full' : 'partial'; } // 업데이트 시간 포맷팅 및 스타일 let updateTimeText = '없음'; let updateClass = ''; if (worker.lastUpdate) { const now = new Date(); const diff = now - worker.lastUpdate; const hours = diff / (1000 * 60 * 60); updateTimeText = formatDateTime(worker.lastUpdate); updateClass = hours < 1 ? 'recent' : hours > 24 ? 'old' : ''; } row.innerHTML = `
👤 ${worker.worker_name}
${statusText[worker.status]}
${worker.totalHours}h
${worker.entryCount}
${workTypeTags}
${projectTags}
${contributorTags}
${updateTimeText}
`; return row; } // 날짜/시간 포맷팅 function formatDateTime(date) { const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const hours = String(d.getHours()).padStart(2, '0'); const minutes = String(d.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } // 작업자 상세 모달 표시 (안전한 버전) function showWorkerDetailSafe(workerId) { // 현재 분석된 데이터에서 해당 작업자 찾기 const worker = filteredWorkData.find(w => w.worker_id == workerId); if (!worker) { showMessage('작업자 정보를 찾을 수 없습니다.', 'error'); return; } showWorkerDetail(worker); } // 작업자 상세 모달 표시 (개선된 버전) function showWorkerDetail(worker) { const modal = document.getElementById('workerDetailModal'); const modalTitle = document.getElementById('modalWorkerName'); const modalBody = document.getElementById('modalWorkerDetails'); modalTitle.textContent = `👤 ${worker.worker_name} 상세 현황`; let detailHtml = `

📊 기본 정보

작업자명: ${worker.worker_name}

총 작업시간: ${worker.totalHours}시간

작업 항목 수: ${worker.entryCount}개

상태: ${worker.status === 'completed' ? '✅ 완료' : worker.status === 'missing' ? '❌ 미입력' : '⚠️ 부분입력'}

작업 유형: ${worker.workTypes && worker.workTypes.length > 0 ? worker.workTypes.join(', ') : '없음'}

`; if (worker.works && worker.works.length > 0) { detailHtml += `

🔧 작업 내역

`; worker.works.forEach((work, index) => { detailHtml += `

작업 ${index + 1}

프로젝트: ${work.project_name || '미지정'}

작업 유형: ${work.work_type_name || '미지정'}

작업 시간: ${work.work_hours}시간

상태: ${work.work_status_name || '미지정'}

${work.error_type_name ? `

에러 유형: ${work.error_type_name}

` : ''}

입력자: ${work.created_by_name || '미지정'}

입력 시간: ${formatDateTime(work.created_at)}

`; }); detailHtml += `
`; } else { detailHtml += `

📭 작업 내역

입력된 작업이 없습니다.

`; } if (worker.contributors && worker.contributors.length > 0) { detailHtml += `

👥 기여자

${worker.contributors.join(', ')}

`; } modalBody.innerHTML = detailHtml; modal.style.display = 'flex'; } // 작업 항목 수정 함수 (통합 API 사용) async function editWorkItem(workId) { try { console.log('수정할 작업 ID:', workId); // 현재 작업 데이터에서 해당 작업 찾기 let workData = null; for (const worker of filteredWorkData) { if (worker.works) { workData = worker.works.find(work => work.id == workId); if (workData) break; } } if (!workData) { showMessage('수정할 작업을 찾을 수 없습니다.', 'error'); return; } // 필요한 마스터 데이터 로드 await loadMasterDataForEdit(); // 수정 모달 표시 showEditModal(workData); } catch (error) { console.error('작업 정보 조회 오류:', error); showMessage('작업 정보를 불러올 수 없습니다: ' + error.message, 'error'); } } // 수정용 마스터 데이터 로드 async function loadMasterDataForEdit() { try { if (!window.projects || window.projects.length === 0) { const projectData = await apiCall(`${API}/projects`); window.projects = Array.isArray(projectData) ? projectData : (projectData.projects || []); } if (!window.workTypes || window.workTypes.length === 0) { const workTypeData = await apiCall(`${API}/daily-work-reports/work-types`); window.workTypes = Array.isArray(workTypeData) ? workTypeData : []; } if (!window.workStatusTypes || window.workStatusTypes.length === 0) { const statusData = await apiCall(`${API}/daily-work-reports/work-status-types`); window.workStatusTypes = Array.isArray(statusData) ? statusData : []; } if (!window.errorTypes || window.errorTypes.length === 0) { const errorData = await apiCall(`${API}/daily-work-reports/error-types`); window.errorTypes = Array.isArray(errorData) ? errorData : []; } } catch (error) { console.error('마스터 데이터 로드 오류:', error); // 기본값 설정 window.projects = window.projects || []; window.workTypes = window.workTypes || [ {id: 1, name: 'Base'}, {id: 2, name: 'Vessel'}, {id: 3, name: 'Piping'} ]; window.workStatusTypes = window.workStatusTypes || [ {id: 1, name: '정규'}, {id: 2, name: '에러'} ]; window.errorTypes = window.errorTypes || [ {id: 1, name: '설계미스'}, {id: 2, name: '외주작업 불량'}, {id: 3, name: '입고지연'}, {id: 4, name: '작업 불량'} ]; } } // 수정 모달 표시 function showEditModal(workData) { // 기존 상세 모달 닫기 closeWorkerDetailModal(); const modalHtml = `

✏️ 작업 수정

`; document.body.insertAdjacentHTML('beforeend', modalHtml); // 업무 상태 변경 이벤트 document.getElementById('editWorkStatus').addEventListener('change', (e) => { const errorTypeGroup = document.getElementById('editErrorTypeGroup'); if (e.target.value === '2') { errorTypeGroup.style.display = 'block'; } else { errorTypeGroup.style.display = 'none'; } }); } // 수정 모달 닫기 function closeEditModal() { const modal = document.getElementById('editModal'); if (modal) { modal.remove(); } } // 수정된 작업 저장 (통합 API 사용) async function saveEditedWork(workId) { try { const projectId = document.getElementById('editProject').value; const workTypeId = document.getElementById('editWorkType').value; const workStatusId = document.getElementById('editWorkStatus').value; const errorTypeId = document.getElementById('editErrorType').value; const workHours = document.getElementById('editWorkHours').value; if (!projectId || !workTypeId || !workStatusId || !workHours) { showMessage('모든 필수 항목을 입력해주세요.', 'error'); return; } if (workStatusId === '2' && !errorTypeId) { showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error'); return; } const updateData = { project_id: parseInt(projectId), work_type_id: parseInt(workTypeId), work_status_id: parseInt(workStatusId), error_type_id: errorTypeId ? parseInt(errorTypeId) : null, work_hours: parseFloat(workHours) }; showMessage('작업을 수정하는 중... (통합 API)', 'loading'); const result = await apiCall(`${API}/daily-work-reports/${workId}`, { method: 'PUT', body: JSON.stringify(updateData) }); console.log('✅ 수정 성공 (통합 API):', result); showMessage('✅ 작업이 성공적으로 수정되었습니다!', 'success'); closeEditModal(); closeWorkerDetailModal(); // 데이터 새로고침 await loadDashboardData(); } catch (error) { console.error('❌ 수정 실패:', error); showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error'); } } // 작업 항목 삭제 함수 (통합 API 사용) async function deleteWorkItem(workId) { if (!confirm('정말로 이 작업을 삭제하시겠습니까?\n삭제된 작업은 복구할 수 없습니다.')) { return; } try { console.log('삭제할 작업 ID:', workId); showMessage('작업을 삭제하는 중... (통합 API)', 'loading'); // 개별 항목 삭제 API 호출 - 통합 API 사용 const result = await apiCall(`${API}/daily-work-reports/${workId}`, { method: 'DELETE' }); console.log('✅ 삭제 성공 (통합 API):', result); showMessage('✅ 작업이 성공적으로 삭제되었습니다!', 'success'); closeWorkerDetailModal(); // 데이터 새로고침 await loadDashboardData(); } catch (error) { console.error('❌ 삭제 실패:', error); showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error'); } } // 작업자 상세 모달 닫기 function closeWorkerDetailModal() { document.getElementById('workerDetailModal').style.display = 'none'; } // 필터링 설정 function setupFiltering() { const showOnlyMissingCheckbox = document.getElementById('showOnlyMissing'); showOnlyMissingCheckbox.addEventListener('change', (e) => { if (e.target.checked) { // 미입력자만 필터링 const missingWorkers = filteredWorkData.filter(worker => worker.status === 'missing'); displayWorkers(missingWorkers); } else { // 전체 표시 displayWorkers(filteredWorkData); } }); } // 엑셀 다운로드 (개선된 버전) function exportToExcel() { try { // CSV 형태로 데이터 구성 (개선된 버전) let csvContent = "작업자명,상태,총시간,작업항목수,작업유형,프로젝트,기여자,최근업데이트\n"; filteredWorkData.forEach(worker => { const statusText = { completed: '완료', missing: '미입력', partial: '부분입력' }; const workTypes = worker.workTypes && worker.workTypes.length > 0 ? worker.workTypes.join('; ') : '없음'; const projects = worker.projects && worker.projects.length > 0 ? worker.projects.join('; ') : '없음'; const contributors = worker.contributors && worker.contributors.length > 0 ? worker.contributors.join('; ') : '없음'; const lastUpdate = worker.lastUpdate ? formatDateTime(worker.lastUpdate) : '없음'; csvContent += `"${worker.worker_name}","${statusText[worker.status]}","${worker.totalHours}","${worker.entryCount}","${workTypes}","${projects}","${contributors}","${lastUpdate}"\n`; }); // UTF-8 BOM 추가 (한글 깨짐 방지) const BOM = '\uFEFF'; const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `작업현황_${currentDate}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); showMessage('✅ 엑셀 파일이 다운로드되었습니다!', 'success'); } catch (error) { console.error('엑셀 다운로드 오류:', error); showMessage('엑셀 다운로드 중 오류가 발생했습니다.', 'error'); } } // 새로고침 function refreshData() { loadDashboardData(); } // 이벤트 리스너 설정 function setupEventListeners() { document.getElementById('loadDataBtn').addEventListener('click', loadDashboardData); document.getElementById('refreshBtn').addEventListener('click', refreshData); document.getElementById('exportBtn').addEventListener('click', exportToExcel); // 엔터키로 조회 document.getElementById('selectedDate').addEventListener('keypress', (e) => { if (e.key === 'Enter') { loadDashboardData(); } }); } // 초기화 async function init() { try { // 권한 체크 if (!checkPermission()) { return; } // 권한 체크 메시지 숨기기 document.getElementById('permission-check-message').style.display = 'none'; // 오늘 날짜 설정 document.getElementById('selectedDate').value = getKoreaToday(); // 이벤트 리스너 설정 setupEventListeners(); console.log('✅ 관리자 대시보드 초기화 완료 (통합 API 설정 적용)'); // 자동으로 오늘 데이터 로드 loadDashboardData(); } catch (error) { console.error('초기화 오류:', error); showMessage('초기화 중 오류가 발생했습니다.', 'error'); } } // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', () => { // 권한 체크 메시지 표시 document.getElementById('permission-check-message').style.display = 'block'; // 토큰 확인 const token = localStorage.getItem('token'); if (!token || token === 'undefined') { showMessage('로그인이 필요합니다.', 'error'); localStorage.removeItem('token'); setTimeout(() => { window.location.href = '/'; }, 2000); return; } // 초기화 실행 init(); }); // 전역 함수로 노출 window.closeWorkerDetailModal = closeWorkerDetailModal; window.refreshData = refreshData; window.showWorkerDetailSafe = showWorkerDetailSafe; window.showWorkerDetail = showWorkerDetail; window.editWorkItem = editWorkItem; window.deleteWorkItem = deleteWorkItem; window.closeEditModal = closeEditModal; window.saveEditedWork = saveEditedWork;