작업자 퇴사 시 비활성화 기능이 제대로 작동하지 않던 문제 해결 백엔드 수정: - is_active 가상 필드 추가 (status 기반 자동 생성) - ISO 8601 날짜 형식을 MySQL DATE 형식으로 변환 - 작업자 업데이트 필드 오류 수정 (salary, annual_leave 제거) 프론트엔드 수정 (11개 파일): - 모든 페이지에서 비활성 작업자 필터링 로직 추가 - 대시보드, 작업보고서, 근태관리, 사용자관리 등 전체 페이지 적용 영향받는 기능: - 작업자 관리: 비활성화 상태가 DB에 저장되고 새로고침 후에도 유지 - 모든 페이지: 비활성화된 작업자가 선택 목록에서 제외됨 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
960 lines
32 KiB
JavaScript
960 lines
32 KiB
JavaScript
// 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 = `<div class="message ${type}">${message}</div>`;
|
||
|
||
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`);
|
||
const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []);
|
||
|
||
// 활성화된 작업자만 필터링
|
||
workers = allWorkers.filter(worker => {
|
||
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
|
||
});
|
||
|
||
console.log(`✅ 작업자 로드 성공: ${workers.length}명 (전체: ${allWorkers.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 = `
|
||
<tr>
|
||
<td colspan="9" class="no-data-row">표시할 작업자가 없습니다.</td>
|
||
</tr>
|
||
`;
|
||
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 => `<span class="work-type-tag">${type}</span>`).join('')
|
||
: '<span class="work-type-tag" style="background: #f8f9fa; color: #6c757d;">없음</span>';
|
||
|
||
// 프로젝트 태그 생성
|
||
const projectTags = worker.projects && worker.projects.length > 0
|
||
? worker.projects.map(project => `<span class="project-tag">${project}</span>`).join('')
|
||
: '<span class="project-tag" style="background: #f8f9fa; color: #6c757d;">없음</span>';
|
||
|
||
// 기여자 태그 생성
|
||
const contributorTags = worker.contributors && worker.contributors.length > 0
|
||
? worker.contributors.map(contributor => `<span class="contributor-tag">${contributor}</span>`).join('')
|
||
: '<span class="contributor-tag" style="background: #f8f9fa; color: #6c757d;">없음</span>';
|
||
|
||
// 시간에 따른 스타일 클래스
|
||
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 = `
|
||
<td>
|
||
<div class="worker-name-cell">
|
||
👤 ${worker.worker_name}
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="status-badge ${statusClass[worker.status]}">${statusText[worker.status]}</span>
|
||
</td>
|
||
<td>
|
||
<div class="hours-cell ${hoursClass}">${worker.totalHours}h</div>
|
||
</td>
|
||
<td>
|
||
<strong>${worker.entryCount}</strong>개
|
||
</td>
|
||
<td>
|
||
<div class="work-types-container">${workTypeTags}</div>
|
||
</td>
|
||
<td>
|
||
<div class="projects-container">${projectTags}</div>
|
||
</td>
|
||
<td>
|
||
<div class="contributors-container">${contributorTags}</div>
|
||
</td>
|
||
<td>
|
||
<div class="update-time ${updateClass}">${updateTimeText}</div>
|
||
</td>
|
||
<td>
|
||
<button class="detail-btn" onclick="showWorkerDetailSafe('${worker.worker_id}')">
|
||
📋 상세
|
||
</button>
|
||
</td>
|
||
`;
|
||
|
||
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 = `
|
||
<div style="margin-bottom: 20px;">
|
||
<h4>📊 기본 정보</h4>
|
||
<p><strong>작업자명:</strong> ${worker.worker_name}</p>
|
||
<p><strong>총 작업시간:</strong> ${worker.totalHours}시간</p>
|
||
<p><strong>작업 항목 수:</strong> ${worker.entryCount}개</p>
|
||
<p><strong>상태:</strong> ${worker.status === 'completed' ? '✅ 완료' : worker.status === 'missing' ? '❌ 미입력' : '⚠️ 부분입력'}</p>
|
||
<p><strong>작업 유형:</strong> ${worker.workTypes && worker.workTypes.length > 0 ? worker.workTypes.join(', ') : '없음'}</p>
|
||
</div>
|
||
`;
|
||
|
||
if (worker.works && worker.works.length > 0) {
|
||
detailHtml += `
|
||
<div style="margin-bottom: 20px;">
|
||
<h4>🔧 작업 내역</h4>
|
||
<div style="max-height: 400px; overflow-y: auto;">
|
||
`;
|
||
|
||
worker.works.forEach((work, index) => {
|
||
detailHtml += `
|
||
<div style="border: 1px solid #e9ecef; padding: 15px; margin-bottom: 10px; border-radius: 8px; background: #f8f9fa; position: relative;">
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;">
|
||
<p><strong>작업 ${index + 1}</strong></p>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button onclick="editWorkItem('${work.id}')" style="background: #007bff; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">
|
||
✏️ 수정
|
||
</button>
|
||
<button onclick="deleteWorkItem('${work.id}')" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">
|
||
🗑️ 삭제
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p><strong>프로젝트:</strong> ${work.project_name || '미지정'}</p>
|
||
<p><strong>작업 유형:</strong> ${work.work_type_name || '미지정'}</p>
|
||
<p><strong>작업 시간:</strong> ${work.work_hours}시간</p>
|
||
<p><strong>상태:</strong> ${work.work_status_name || '미지정'}</p>
|
||
${work.error_type_name ? `<p><strong>에러 유형:</strong> ${work.error_type_name}</p>` : ''}
|
||
<p><strong>입력자:</strong> ${work.created_by_name || '미지정'}</p>
|
||
<p><strong>입력 시간:</strong> ${formatDateTime(work.created_at)}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
detailHtml += `
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
detailHtml += `
|
||
<div style="margin-bottom: 20px;">
|
||
<h4>📭 작업 내역</h4>
|
||
<p style="text-align: center; color: #666; padding: 20px;">입력된 작업이 없습니다.</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (worker.contributors && worker.contributors.length > 0) {
|
||
detailHtml += `
|
||
<div>
|
||
<h4>👥 기여자</h4>
|
||
<p>${worker.contributors.join(', ')}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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 = `
|
||
<div class="edit-modal" id="editModal">
|
||
<div class="edit-modal-content">
|
||
<div class="edit-modal-header">
|
||
<h3>✏️ 작업 수정</h3>
|
||
<button class="close-modal-btn" onclick="closeEditModal()">×</button>
|
||
</div>
|
||
<div class="edit-modal-body">
|
||
<div class="edit-form-group">
|
||
<label>🏗️ 프로젝트</label>
|
||
<select class="edit-select" id="editProject">
|
||
<option value="">프로젝트 선택</option>
|
||
${(window.projects || []).map(p => `
|
||
<option value="${p.project_id}" ${p.project_id == workData.project_id ? 'selected' : ''}>
|
||
${p.project_name}
|
||
</option>
|
||
`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="edit-form-group">
|
||
<label>⚙️ 작업 유형</label>
|
||
<select class="edit-select" id="editWorkType">
|
||
<option value="">작업 유형 선택</option>
|
||
${(window.workTypes || []).map(wt => `
|
||
<option value="${wt.id}" ${wt.id == workData.work_type_id ? 'selected' : ''}>
|
||
${wt.name}
|
||
</option>
|
||
`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="edit-form-group">
|
||
<label>📊 업무 상태</label>
|
||
<select class="edit-select" id="editWorkStatus">
|
||
<option value="">업무 상태 선택</option>
|
||
${(window.workStatusTypes || []).map(ws => `
|
||
<option value="${ws.id}" ${ws.id == workData.work_status_id ? 'selected' : ''}>
|
||
${ws.name}
|
||
</option>
|
||
`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="edit-form-group" id="editErrorTypeGroup" style="${workData.work_status_id == 2 ? '' : 'display: none;'}">
|
||
<label>❌ 에러 유형</label>
|
||
<select class="edit-select" id="editErrorType">
|
||
<option value="">에러 유형 선택</option>
|
||
${(window.errorTypes || []).map(et => `
|
||
<option value="${et.id}" ${et.id == workData.error_type_id ? 'selected' : ''}>
|
||
${et.name}
|
||
</option>
|
||
`).join('')}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="edit-form-group">
|
||
<label>⏰ 작업 시간</label>
|
||
<input type="number" class="edit-input" id="editWorkHours"
|
||
value="${workData.work_hours}"
|
||
min="0" max="24" step="0.5">
|
||
</div>
|
||
</div>
|
||
<div class="edit-modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeEditModal()">취소</button>
|
||
<button class="btn btn-success" onclick="saveEditedWork('${workData.id}')">💾 저장</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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; |