- 월별 캘린더 UI로 작업 현황을 한눈에 확인 가능 - 미입력(빨강), 부분입력(주황), 확인필요(보라), 이상없음(초록) 상태 표시 - 범례 아이콘(●)을 사용한 직관적인 상태 표시 - 날짜 클릭 시 해당일 작업자별 상세 현황 모달 - 작업자 클릭 시 개별 작업 입력/수정 모달 - 휴가 처리 기능 (연차, 반차, 반반차, 조퇴) - 월별 집계 데이터 최적화로 API 호출 최소화 백엔드: - monthly_worker_status, monthly_summary 테이블 추가 - 자동 집계 stored procedure 및 trigger 구현 - 확인필요(12시간 초과) 상태 감지 로직 - 출석 관리 시스템 확장 프론트엔드: - 캘린더 그리드 UI 구현 - 상태별 색상 및 아이콘 표시 - 모달 기반 상세 정보 표시 - 반응형 디자인 적용
1266 lines
43 KiB
JavaScript
1266 lines
43 KiB
JavaScript
// ✅ modern-dashboard.js - 모던 대시보드 JavaScript
|
||
|
||
// API 설정 및 함수들은 api-config.js에서 로드됨
|
||
// window.API, window.apiCall, window.getAuthHeaders 사용
|
||
|
||
// 인증 관련 함수들
|
||
function getAuthData() {
|
||
const token = localStorage.getItem('token');
|
||
const user = localStorage.getItem('user');
|
||
return {
|
||
token,
|
||
user: user ? JSON.parse(user) : null
|
||
};
|
||
}
|
||
|
||
// 전역 변수
|
||
let currentUser = null;
|
||
let workersData = [];
|
||
let workData = [];
|
||
let selectedDate = new Date().toISOString().split('T')[0];
|
||
|
||
// 모달 관련 변수
|
||
let currentModalWorker = null;
|
||
let modalWorkTypes = [];
|
||
let modalWorkStatusTypes = [];
|
||
let modalErrorTypes = [];
|
||
let modalProjects = [];
|
||
let modalExistingWork = [];
|
||
|
||
// DOM 요소
|
||
const elements = {
|
||
currentTime: document.getElementById('currentTime'),
|
||
timeValue: document.getElementById('timeValue'),
|
||
userName: document.getElementById('userName'),
|
||
userRole: document.getElementById('userRole'),
|
||
userInitial: document.getElementById('userInitial'),
|
||
selectedDate: document.getElementById('selectedDate'),
|
||
refreshBtn: document.getElementById('refreshBtn'),
|
||
logoutBtn: document.getElementById('logoutBtn'),
|
||
|
||
// 요약 카드
|
||
todayWorkers: document.getElementById('todayWorkers'),
|
||
totalHours: document.getElementById('totalHours'),
|
||
activeProjects: document.getElementById('activeProjects'),
|
||
errorCount: document.getElementById('errorCount'),
|
||
|
||
// 컨테이너
|
||
workStatusContainer: document.getElementById('workStatusContainer'),
|
||
workersContainer: document.getElementById('workersContainer'),
|
||
toastContainer: document.getElementById('toastContainer')
|
||
};
|
||
|
||
// ========== 초기화 ========== //
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
// API 함수가 로드될 때까지 기다림
|
||
let retryCount = 0;
|
||
const maxRetries = 50; // 5초 대기
|
||
|
||
while (!window.apiCall && retryCount < maxRetries) {
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
retryCount++;
|
||
}
|
||
|
||
if (!window.apiCall) {
|
||
console.error('❌ API 함수를 로드할 수 없습니다.');
|
||
showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await initializeDashboard();
|
||
} catch (error) {
|
||
console.error('대시보드 초기화 오류:', error);
|
||
showToast('대시보드를 불러오는 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
});
|
||
|
||
async function initializeDashboard() {
|
||
console.log('🚀 모던 대시보드 초기화 시작');
|
||
|
||
// 사용자 정보 설정
|
||
setupUserInfo();
|
||
|
||
// 시간 업데이트 시작
|
||
updateCurrentTime();
|
||
setInterval(updateCurrentTime, 1000);
|
||
|
||
// 날짜 설정
|
||
elements.selectedDate.value = selectedDate;
|
||
|
||
// 이벤트 리스너 설정
|
||
setupEventListeners();
|
||
|
||
// 데이터 로드
|
||
await loadDashboardData();
|
||
|
||
// 관리자 권한 확인
|
||
checkAdminAccess();
|
||
|
||
console.log('✅ 모던 대시보드 초기화 완료');
|
||
}
|
||
|
||
// ========== 사용자 정보 설정 ========== //
|
||
function setupUserInfo() {
|
||
const authData = getAuthData();
|
||
if (authData && authData.user) {
|
||
currentUser = authData.user;
|
||
|
||
// 사용자 이름 설정
|
||
elements.userName.textContent = currentUser.name || currentUser.username;
|
||
|
||
// 사용자 역할 설정
|
||
const roleMap = {
|
||
'admin': '관리자',
|
||
'system': '시스템 관리자',
|
||
'group_leader': '그룹장',
|
||
'leader': '그룹장',
|
||
'user': '작업자'
|
||
};
|
||
elements.userRole.textContent = roleMap[currentUser.role] || '작업자';
|
||
|
||
// 아바타 초기값 설정
|
||
const initial = (currentUser.name || currentUser.username).charAt(0);
|
||
elements.userInitial.textContent = initial;
|
||
|
||
console.log('👤 사용자 정보 설정 완료:', currentUser.name);
|
||
}
|
||
}
|
||
|
||
// ========== 시간 업데이트 ========== //
|
||
function updateCurrentTime() {
|
||
const now = new Date();
|
||
const timeString = now.toLocaleTimeString('ko-KR', {
|
||
hour12: false,
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
elements.timeValue.textContent = timeString;
|
||
}
|
||
|
||
// ========== 이벤트 리스너 ========== //
|
||
function setupEventListeners() {
|
||
// 날짜 변경
|
||
elements.selectedDate.addEventListener('change', (e) => {
|
||
selectedDate = e.target.value;
|
||
loadDashboardData();
|
||
});
|
||
|
||
// 새로고침 버튼
|
||
elements.refreshBtn.addEventListener('click', () => {
|
||
loadDashboardData();
|
||
showToast('데이터를 새로고침했습니다.', 'success');
|
||
});
|
||
|
||
// 로그아웃 버튼
|
||
elements.logoutBtn.addEventListener('click', () => {
|
||
if (confirm('로그아웃하시겠습니까?')) {
|
||
localStorage.clear();
|
||
window.location.href = '/index.html';
|
||
}
|
||
});
|
||
|
||
// 뷰 컨트롤 버튼들
|
||
const listViewBtn = document.getElementById('listViewBtn');
|
||
const cardViewBtn = document.getElementById('cardViewBtn');
|
||
|
||
if (listViewBtn) {
|
||
listViewBtn.addEventListener('click', () => {
|
||
displayWorkers(workersData, 'list');
|
||
updateViewButtons('list');
|
||
});
|
||
}
|
||
|
||
if (cardViewBtn) {
|
||
cardViewBtn.addEventListener('click', () => {
|
||
displayWorkers(workersData, 'card');
|
||
updateViewButtons('card');
|
||
});
|
||
}
|
||
}
|
||
|
||
// ========== 데이터 로드 ========== //
|
||
async function loadDashboardData() {
|
||
console.log('📊 대시보드 데이터 로딩 시작');
|
||
|
||
try {
|
||
// 로딩 상태 표시
|
||
showLoadingState();
|
||
|
||
// 병렬로 데이터 로드
|
||
const [workersResult, workResult] = await Promise.all([
|
||
loadWorkers(),
|
||
loadWorkData(selectedDate)
|
||
]);
|
||
|
||
// 요약 데이터 업데이트
|
||
updateSummaryCards();
|
||
|
||
// 작업 현황 표시
|
||
displayWorkStatus();
|
||
|
||
// 작업자 현황 표시
|
||
displayWorkers(workersData, 'card');
|
||
|
||
console.log('✅ 대시보드 데이터 로딩 완료');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 대시보드 데이터 로딩 오류:', error);
|
||
showErrorState();
|
||
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
}
|
||
|
||
async function loadWorkers() {
|
||
try {
|
||
console.log('👥 작업자 데이터 로딩...');
|
||
const response = await window.apiCall('/workers');
|
||
workersData = Array.isArray(response) ? response : (response.data || []);
|
||
console.log(`✅ 작업자 ${workersData.length}명 로드 완료`);
|
||
return workersData;
|
||
} catch (error) {
|
||
console.error('작업자 데이터 로딩 오류:', error);
|
||
workersData = [];
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function loadWorkData(date) {
|
||
try {
|
||
console.log(`📋 ${date} 작업 데이터 로딩...`);
|
||
const response = await window.apiCall(`/daily-work-reports?date=${date}&view_all=true`);
|
||
workData = Array.isArray(response) ? response : (response.data || []);
|
||
console.log(`✅ 작업 데이터 ${workData.length}건 로드 완료`);
|
||
return workData;
|
||
} catch (error) {
|
||
console.error('작업 데이터 로딩 오류:', error);
|
||
workData = [];
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// ========== 요약 카드 업데이트 ========== //
|
||
function updateSummaryCards() {
|
||
// 오늘 작업자 수
|
||
const todayWorkersCount = new Set(workData.map(w => w.worker_id)).size;
|
||
updateSummaryCard(elements.todayWorkers, todayWorkersCount, '명');
|
||
|
||
// 총 작업 시간
|
||
const totalHours = workData.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
|
||
updateSummaryCard(elements.totalHours, totalHours.toFixed(1), '시간');
|
||
|
||
// 진행 중인 프로젝트
|
||
const activeProjectsCount = new Set(workData.map(w => w.project_id)).size;
|
||
updateSummaryCard(elements.activeProjects, activeProjectsCount, '개');
|
||
|
||
// 오류 발생 건수
|
||
const errorCount = workData.filter(w => w.work_status_id === 2).length;
|
||
updateSummaryCard(elements.errorCount, errorCount, '건');
|
||
}
|
||
|
||
function updateSummaryCard(element, value, unit) {
|
||
if (element) {
|
||
const numberElement = element.querySelector('.value-number');
|
||
const unitElement = element.querySelector('.value-unit');
|
||
|
||
if (numberElement) numberElement.textContent = value;
|
||
if (unitElement) unitElement.textContent = unit;
|
||
}
|
||
}
|
||
|
||
// ========== 작업 현황 표시 (작업자 중심) ========== //
|
||
function displayWorkStatus() {
|
||
if (!elements.workStatusContainer) return;
|
||
|
||
// 모든 작업자 데이터 가져오기 (작업이 없는 작업자도 포함)
|
||
const allWorkers = workersData || [];
|
||
|
||
if (allWorkers.length === 0) {
|
||
elements.workStatusContainer.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-icon">👥</div>
|
||
<h3>등록된 작업자가 없습니다</h3>
|
||
<p>시스템에 작업자가 등록되어 있지 않습니다.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 작업자별 상황 분석
|
||
const workerStatusList = allWorkers.map(worker => {
|
||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
|
||
// 휴가/연차 제외한 실제 작업시간 계산
|
||
const actualWorkHours = todayWork
|
||
.filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외
|
||
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
|
||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||
|
||
// 정규 작업과 에러 작업 건수 분리
|
||
const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length;
|
||
const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length;
|
||
|
||
// 상태 판단 로직 (개선된 버전)
|
||
let status = 'incomplete';
|
||
let statusText = '미입력';
|
||
let statusBadge = '미입력';
|
||
let vacationType = null;
|
||
|
||
// 휴가 처리된 경우 확인 (프로젝트 ID 13 = "연차/휴무" 또는 설명에 휴가 키워드)
|
||
const hasVacationRecord = todayWork.some(w =>
|
||
w.project_id === 13 || // 연차/휴무 프로젝트
|
||
(w.description && (
|
||
w.description.includes('연차') ||
|
||
w.description.includes('반차') ||
|
||
w.description.includes('휴가')
|
||
))
|
||
);
|
||
|
||
// 연차/휴무 프로젝트의 시간 계산
|
||
const vacationHours = todayWork
|
||
.filter(w => w.project_id === 13)
|
||
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
|
||
if (totalHours > 12) {
|
||
status = 'overtime-warning';
|
||
statusText = '초과근무 확인필요';
|
||
statusBadge = '확인필요';
|
||
} else if (hasVacationRecord && vacationHours > 0) {
|
||
// 연차/휴무 시간에 따른 상태 결정
|
||
if (vacationHours === 8) {
|
||
status = 'vacation-full';
|
||
statusText = '연차';
|
||
statusBadge = '연차';
|
||
} else if (vacationHours === 6) {
|
||
status = 'vacation-half-half';
|
||
statusText = '조퇴';
|
||
statusBadge = '조퇴';
|
||
} else if (vacationHours === 4) {
|
||
status = 'vacation-half';
|
||
statusText = '반차';
|
||
statusBadge = '반차';
|
||
} else if (vacationHours === 2) {
|
||
status = 'vacation-quarter';
|
||
statusText = '반반차';
|
||
statusBadge = '반반차';
|
||
}
|
||
} else if (totalHours > 8) {
|
||
// 8시간 초과 - 연장근로
|
||
status = 'overtime';
|
||
statusText = '연장근로';
|
||
statusBadge = '연장근로';
|
||
} else if (totalHours === 8) {
|
||
// 정확히 8시간 - 정시근로
|
||
status = 'complete';
|
||
statusText = '정시근로';
|
||
statusBadge = '정시근로';
|
||
} else if (totalHours > 0) {
|
||
// 0시간 초과 8시간 미만 - 부분 입력
|
||
status = 'partial';
|
||
statusText = '부분 입력';
|
||
statusBadge = '부분입력';
|
||
|
||
// 휴가 처리 필요 여부 판단
|
||
if (totalHours === 0) {
|
||
vacationType = 'full';
|
||
} else if (totalHours === 4) {
|
||
vacationType = 'half';
|
||
} else if (totalHours === 6) {
|
||
vacationType = 'half-half'; // 2시간 더 추가해서 조퇴 처리
|
||
}
|
||
} else {
|
||
// 0시간 - 미입력
|
||
status = 'incomplete';
|
||
statusText = '미입력';
|
||
statusBadge = '미입력';
|
||
vacationType = 'full';
|
||
}
|
||
|
||
return {
|
||
...worker,
|
||
todayWork,
|
||
totalHours,
|
||
actualWorkHours,
|
||
regularWorkCount,
|
||
errorWorkCount,
|
||
hasError,
|
||
status,
|
||
statusText,
|
||
statusBadge,
|
||
vacationType
|
||
};
|
||
});
|
||
|
||
elements.workStatusContainer.innerHTML = `
|
||
<div class="worker-status-list">
|
||
<div class="worker-status-header">
|
||
<div class="header-title">
|
||
<h3>작업자별 현황</h3>
|
||
<span class="header-date">${selectedDate}</span>
|
||
</div>
|
||
<div class="status-legend">
|
||
<span class="legend-item legend-complete">정시근로</span>
|
||
<span class="legend-item legend-overtime">연장근로</span>
|
||
<span class="legend-item legend-vacation">휴가</span>
|
||
<span class="legend-item legend-partial">부분입력</span>
|
||
<span class="legend-item legend-incomplete">미입력</span>
|
||
<span class="legend-item legend-error">오류</span>
|
||
</div>
|
||
</div>
|
||
<div class="worker-status-rows">
|
||
${workerStatusList.map(worker => `
|
||
<div class="worker-status-row ${worker.status}" data-worker-id="${worker.worker_id}">
|
||
<div class="worker-basic-info">
|
||
<div class="worker-avatar">
|
||
<span>${worker.worker_name.charAt(0)}</span>
|
||
</div>
|
||
<div class="worker-details">
|
||
<h4 class="worker-name">${worker.worker_name}</h4>
|
||
<p class="worker-job">${worker.job_type || '작업자'}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="worker-status-indicator">
|
||
<span class="status-badge status-${worker.status}">${worker.statusBadge}</span>
|
||
</div>
|
||
|
||
<div class="worker-stats-inline">
|
||
<div class="stat-item">
|
||
<span class="stat-label">작업시간</span>
|
||
<span class="stat-value ${worker.actualWorkHours > 12 ? 'warning' : ''}">${worker.actualWorkHours.toFixed(1)}h</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">정규</span>
|
||
<span class="stat-value">${worker.regularWorkCount}건</span>
|
||
</div>
|
||
${worker.errorWorkCount > 0 ? `
|
||
<div class="stat-item error">
|
||
<span class="stat-label">에러</span>
|
||
<span class="stat-value">${worker.errorWorkCount}건</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div class="worker-actions-inline">
|
||
<button class="btn btn-sm btn-primary worker-edit-btn" onclick="openWorkerModal(${worker.worker_id}, '${worker.worker_name}')">
|
||
작업입력
|
||
</button>
|
||
${worker.vacationType ? `
|
||
<button class="btn btn-sm btn-secondary vacation-btn" onclick="handleVacation(${worker.worker_id}, '${worker.vacationType}')">
|
||
${worker.vacationType === 'full' ? '연차처리' : worker.vacationType === 'half' ? '반차처리' : '반반차처리'}
|
||
</button>
|
||
` : ''}
|
||
${worker.status === 'overtime-warning' ? `
|
||
<button class="btn btn-sm btn-warning confirm-overtime-btn" onclick="confirmOvertime(${worker.worker_id})">
|
||
정상확인
|
||
</button>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function groupWorkDataByProject() {
|
||
const groups = {};
|
||
workData.forEach(work => {
|
||
const projectName = work.project_name || '미지정 프로젝트';
|
||
if (!groups[projectName]) {
|
||
groups[projectName] = [];
|
||
}
|
||
groups[projectName].push(work);
|
||
});
|
||
return groups;
|
||
}
|
||
|
||
// ========== 작업자 현황 표시 ========== //
|
||
function displayWorkers(workers, viewType = 'card') {
|
||
if (!elements.workersContainer) return;
|
||
|
||
if (workers.length === 0) {
|
||
elements.workersContainer.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-icon">👥</div>
|
||
<h3>작업자 데이터가 없습니다</h3>
|
||
<p>등록된 작업자가 없습니다.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
if (viewType === 'list') {
|
||
displayWorkersAsList(workers);
|
||
} else {
|
||
displayWorkersAsCards(workers);
|
||
}
|
||
}
|
||
|
||
function displayWorkersAsCards(workers) {
|
||
elements.workersContainer.innerHTML = `
|
||
<div class="workers-grid grid grid-cols-4">
|
||
${workers.map(worker => {
|
||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
|
||
// 휴가/연차 제외한 실제 작업시간 계산
|
||
const actualWorkHours = todayWork
|
||
.filter(w => w.project_id !== 13) // 연차/휴무 프로젝트 제외
|
||
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
|
||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||
|
||
// 정규 작업과 에러 작업 건수 분리
|
||
const regularWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length;
|
||
const errorWorkCount = todayWork.filter(w => w.project_id !== 13 && w.work_status_id === 2).length;
|
||
|
||
return `
|
||
<div class="worker-card card">
|
||
<div class="card-body">
|
||
<div class="worker-header">
|
||
<div class="worker-avatar">
|
||
<span>${worker.worker_name.charAt(0)}</span>
|
||
</div>
|
||
<div class="worker-info">
|
||
<h4 class="worker-name">${worker.worker_name}</h4>
|
||
<p class="worker-job">${worker.job_type || '작업자'}</p>
|
||
</div>
|
||
<div class="worker-status">
|
||
<span class="status-dot ${todayWork.length > 0 ? 'active' : 'inactive'}"></span>
|
||
</div>
|
||
</div>
|
||
<div class="worker-stats">
|
||
<div class="stat">
|
||
<span class="stat-label">작업시간</span>
|
||
<span class="stat-value">${actualWorkHours.toFixed(1)}h</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat-label">정규</span>
|
||
<span class="stat-value">${regularWorkCount}건</span>
|
||
</div>
|
||
${errorWorkCount > 0 ? `
|
||
<div class="stat error">
|
||
<span class="stat-label">에러</span>
|
||
<span class="stat-value">${errorWorkCount}건</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function displayWorkersAsList(workers) {
|
||
elements.workersContainer.innerHTML = `
|
||
<div class="workers-table">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>작업자</th>
|
||
<th>직종</th>
|
||
<th>오늘 작업</th>
|
||
<th>작업 시간</th>
|
||
<th>상태</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${workers.map(worker => {
|
||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||
|
||
return `
|
||
<tr>
|
||
<td>
|
||
<div class="worker-cell">
|
||
<div class="worker-avatar small">
|
||
<span>${worker.worker_name.charAt(0)}</span>
|
||
</div>
|
||
<span class="worker-name">${worker.worker_name}</span>
|
||
</div>
|
||
</td>
|
||
<td>${worker.job_type || '작업자'}</td>
|
||
<td>${todayWork.length}건</td>
|
||
<td>${totalHours.toFixed(1)}시간</td>
|
||
<td>
|
||
<span class="badge ${todayWork.length > 0 ? 'badge-success' : 'badge-gray'}">
|
||
${todayWork.length > 0 ? '작업 중' : '대기'}
|
||
</span>
|
||
${hasError ? '<span class="badge badge-error">오류</span>' : ''}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ========== 뷰 버튼 업데이트 ========== //
|
||
function updateViewButtons(activeView) {
|
||
const listBtn = document.getElementById('listViewBtn');
|
||
const cardBtn = document.getElementById('cardViewBtn');
|
||
|
||
if (listBtn && cardBtn) {
|
||
listBtn.classList.toggle('btn-primary', activeView === 'list');
|
||
listBtn.classList.toggle('btn-secondary', activeView !== 'list');
|
||
|
||
cardBtn.classList.toggle('btn-primary', activeView === 'card');
|
||
cardBtn.classList.toggle('btn-secondary', activeView !== 'card');
|
||
}
|
||
}
|
||
|
||
// ========== 관리자 권한 확인 ========== //
|
||
function checkAdminAccess() {
|
||
const adminElements = document.querySelectorAll('.admin-only');
|
||
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.access_level);
|
||
|
||
adminElements.forEach(element => {
|
||
if (isAdmin) {
|
||
element.classList.add('visible');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ========== 상태 표시 ========== //
|
||
function showLoadingState() {
|
||
const loadingHTML = `
|
||
<div class="loading-state">
|
||
<div class="spinner"></div>
|
||
<p>데이터를 불러오는 중...</p>
|
||
</div>
|
||
`;
|
||
|
||
if (elements.workStatusContainer) {
|
||
elements.workStatusContainer.innerHTML = loadingHTML;
|
||
}
|
||
|
||
if (elements.workersContainer) {
|
||
elements.workersContainer.innerHTML = loadingHTML;
|
||
}
|
||
}
|
||
|
||
function showErrorState() {
|
||
const errorHTML = `
|
||
<div class="error-state">
|
||
<div class="error-icon">⚠️</div>
|
||
<h3>데이터를 불러올 수 없습니다</h3>
|
||
<p>네트워크 연결을 확인하고 다시 시도해주세요.</p>
|
||
<button class="btn btn-primary" onclick="loadDashboardData()">
|
||
<span>🔄</span>
|
||
다시 시도
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
if (elements.workStatusContainer) {
|
||
elements.workStatusContainer.innerHTML = errorHTML;
|
||
}
|
||
|
||
if (elements.workersContainer) {
|
||
elements.workersContainer.innerHTML = errorHTML;
|
||
}
|
||
}
|
||
|
||
// ========== 토스트 알림 ========== //
|
||
function showToast(message, type = 'info', duration = 3000) {
|
||
if (!elements.toastContainer) return;
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
|
||
const iconMap = {
|
||
success: '✅',
|
||
error: '❌',
|
||
warning: '⚠️',
|
||
info: 'ℹ️'
|
||
};
|
||
|
||
toast.innerHTML = `
|
||
<div class="toast-icon">${iconMap[type] || 'ℹ️'}</div>
|
||
<div class="toast-message">${message}</div>
|
||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||
`;
|
||
|
||
elements.toastContainer.appendChild(toast);
|
||
|
||
// 자동 제거
|
||
setTimeout(() => {
|
||
if (toast.parentElement) {
|
||
toast.remove();
|
||
}
|
||
}, duration);
|
||
}
|
||
|
||
// ========== 작업자 관련 액션 함수들 ========== //
|
||
function openWorkerModal(workerId, workerName) {
|
||
console.log(`📝 ${workerName}(ID: ${workerId}) 작업 보고서 모달 열기`);
|
||
|
||
// 모달 데이터 설정
|
||
currentModalWorker = {
|
||
id: workerId,
|
||
name: workerName,
|
||
date: selectedDate
|
||
};
|
||
|
||
// 모달 표시
|
||
showWorkerModal();
|
||
}
|
||
|
||
function handleVacation(workerId, vacationType) {
|
||
console.log(`🏖️ 작업자 ${workerId} 휴가 처리: ${vacationType}`);
|
||
|
||
const vacationNames = {
|
||
'full': '연차',
|
||
'half': '반차',
|
||
'half-half': '반반차'
|
||
};
|
||
|
||
const vacationHours = {
|
||
'full': 8,
|
||
'half': 4,
|
||
'half-half': 2
|
||
};
|
||
|
||
if (confirm(`${vacationNames[vacationType]} 처리하시겠습니까?\n(${vacationHours[vacationType]}시간으로 자동 입력됩니다)`)) {
|
||
// 휴가 처리 API 호출
|
||
processVacation(workerId, vacationType, vacationHours[vacationType]);
|
||
}
|
||
}
|
||
|
||
async function processVacation(workerId, vacationType, hours) {
|
||
try {
|
||
showToast(`휴가 처리 중...`, 'info');
|
||
|
||
// 휴가용 작업 보고서 생성 (특별한 작업 유형으로)
|
||
const vacationReport = {
|
||
report_date: selectedDate,
|
||
worker_id: workerId,
|
||
project_id: 1, // 기본 프로젝트 (휴가용)
|
||
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
|
||
work_status_id: 1, // 정상 상태
|
||
error_type_id: null,
|
||
work_hours: hours,
|
||
created_by: currentUser?.user_id || 1
|
||
};
|
||
|
||
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(vacationReport)
|
||
});
|
||
|
||
showToast(`휴가 처리가 완료되었습니다.`, 'success');
|
||
await loadDashboardData(); // 데이터 새로고침
|
||
|
||
} catch (error) {
|
||
console.error('휴가 처리 오류:', error);
|
||
showToast(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function confirmOvertime(workerId) {
|
||
console.log(`⚠️ 작업자 ${workerId} 초과근무 확인`);
|
||
|
||
if (confirm('12시간을 초과한 작업시간이 정상적인 입력인지 확인하시겠습니까?')) {
|
||
// 초과근무 확인 처리
|
||
processOvertimeConfirmation(workerId);
|
||
}
|
||
}
|
||
|
||
async function processOvertimeConfirmation(workerId) {
|
||
try {
|
||
showToast('초과근무 승인 처리 중...', 'info');
|
||
|
||
// 새로운 근태 관리 API 사용
|
||
const overtimeData = {
|
||
worker_id: workerId,
|
||
date: selectedDate
|
||
};
|
||
|
||
const response = await window.apiCall('/attendance/overtime/approve', 'POST', overtimeData);
|
||
|
||
if (response.success) {
|
||
showToast('초과근무가 정상으로 승인되었습니다.', 'success');
|
||
await loadDashboardData(); // 데이터 새로고침
|
||
} else {
|
||
throw new Error(response.message || '초과근무 승인에 실패했습니다.');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('초과근무 승인 오류:', error);
|
||
showToast(`초과근무 승인 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
// ========== 모달 시스템 ========== //
|
||
function showWorkerModal() {
|
||
// 모달이 없으면 생성
|
||
if (!document.getElementById('workerModal')) {
|
||
createWorkerModal();
|
||
}
|
||
|
||
// 모달 데이터 로드 및 표시
|
||
loadModalData();
|
||
document.getElementById('workerModal').style.display = 'flex';
|
||
document.body.style.overflow = 'hidden'; // 배경 스크롤 방지
|
||
}
|
||
|
||
function hideWorkerModal() {
|
||
document.getElementById('workerModal').style.display = 'none';
|
||
document.body.style.overflow = 'auto'; // 배경 스크롤 복원
|
||
resetModalForm();
|
||
}
|
||
|
||
function createWorkerModal() {
|
||
const modalHTML = `
|
||
<div id="workerModal" class="modal-overlay">
|
||
<div class="modal-container">
|
||
<div class="modal-header">
|
||
<h2 id="modalTitle">작업자 보고서</h2>
|
||
<button class="modal-close-btn" onclick="hideWorkerModal()">×</button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<!-- 작업자 정보 -->
|
||
<div class="modal-worker-info">
|
||
<div class="modal-worker-avatar">
|
||
<span id="modalWorkerInitial">작</span>
|
||
</div>
|
||
<div class="modal-worker-details">
|
||
<h3 id="modalWorkerName">작업자명</h3>
|
||
<p id="modalWorkerDate">날짜</p>
|
||
</div>
|
||
<div class="modal-worker-summary">
|
||
<div class="modal-stat">
|
||
<span class="modal-stat-label">총 시간</span>
|
||
<span class="modal-stat-value" id="modalTotalHours">0h</span>
|
||
</div>
|
||
<div class="modal-stat">
|
||
<span class="modal-stat-label">작업 건수</span>
|
||
<span class="modal-stat-value" id="modalWorkCount">0건</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 기존 작업 목록 -->
|
||
<div class="modal-section">
|
||
<div class="modal-section-header">
|
||
<h4>기존 작업 목록</h4>
|
||
<button class="btn btn-sm btn-primary" id="modalAddWorkBtn">새 작업 추가</button>
|
||
</div>
|
||
<div id="modalExistingWork" class="modal-existing-work">
|
||
<!-- 기존 작업들이 여기에 표시됩니다 -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 새 작업 추가 폼 -->
|
||
<div class="modal-section" id="modalNewWorkSection" style="display: none;">
|
||
<div class="modal-section-header">
|
||
<h4>새 작업 추가</h4>
|
||
<button class="btn btn-sm btn-secondary" id="modalCancelWorkBtn">취소</button>
|
||
</div>
|
||
<div class="modal-work-form">
|
||
<div class="modal-form-row">
|
||
<div class="modal-form-group">
|
||
<label>프로젝트</label>
|
||
<select id="modalProjectSelect" class="modal-select">
|
||
<option value="">프로젝트 선택</option>
|
||
</select>
|
||
</div>
|
||
<div class="modal-form-group">
|
||
<label>작업 유형</label>
|
||
<select id="modalWorkTypeSelect" class="modal-select">
|
||
<option value="">작업 유형 선택</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-form-group">
|
||
<label>업무 상태</label>
|
||
<select id="modalWorkStatusSelect" class="modal-select">
|
||
<option value="">업무 상태 선택</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="modal-form-group" id="modalErrorTypeGroup" style="display: none;">
|
||
<label>에러 유형</label>
|
||
<select id="modalErrorTypeSelect" class="modal-select">
|
||
<option value="">에러 유형 선택</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="modal-form-group">
|
||
<label>작업 시간</label>
|
||
<input type="number" id="modalWorkHours" class="modal-input" step="0.25" min="0.25" max="24" value="1.00">
|
||
<div class="modal-quick-time">
|
||
<button type="button" class="modal-time-btn" data-hours="0.5">0.5h</button>
|
||
<button type="button" class="modal-time-btn" data-hours="1">1h</button>
|
||
<button type="button" class="modal-time-btn" data-hours="2">2h</button>
|
||
<button type="button" class="modal-time-btn" data-hours="4">4h</button>
|
||
<button type="button" class="modal-time-btn" data-hours="8">8h</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-success modal-save-btn" id="modalSaveWorkBtn">작업 저장</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 휴가 처리 -->
|
||
<div class="modal-section">
|
||
<div class="modal-section-header">
|
||
<h4>휴가 처리</h4>
|
||
</div>
|
||
<div class="modal-vacation-buttons">
|
||
<button class="btn btn-warning modal-vacation-btn" data-type="full">연차 (8시간)</button>
|
||
<button class="btn btn-warning modal-vacation-btn" data-type="half-half">반반차 (6시간)</button>
|
||
<button class="btn btn-warning modal-vacation-btn" data-type="half">반차 (4시간)</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="hideWorkerModal()">닫기</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||
setupModalEventListeners();
|
||
}
|
||
|
||
function setupModalEventListeners() {
|
||
// 모달 외부 클릭 시 닫기
|
||
document.getElementById('workerModal').addEventListener('click', (e) => {
|
||
if (e.target.id === 'workerModal') {
|
||
hideWorkerModal();
|
||
}
|
||
});
|
||
|
||
// 새 작업 추가/취소 버튼
|
||
document.getElementById('modalAddWorkBtn').addEventListener('click', showModalNewWorkForm);
|
||
document.getElementById('modalCancelWorkBtn').addEventListener('click', hideModalNewWorkForm);
|
||
document.getElementById('modalSaveWorkBtn').addEventListener('click', saveModalNewWork);
|
||
|
||
// 업무 상태 변경 시 에러 유형 토글
|
||
document.getElementById('modalWorkStatusSelect').addEventListener('change', toggleModalErrorType);
|
||
|
||
// 빠른 시간 버튼
|
||
document.querySelectorAll('.modal-time-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.getElementById('modalWorkHours').value = e.target.dataset.hours;
|
||
});
|
||
});
|
||
|
||
// 휴가 처리 버튼
|
||
document.querySelectorAll('.modal-vacation-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const vacationType = e.target.dataset.type;
|
||
handleModalVacation(vacationType);
|
||
});
|
||
});
|
||
}
|
||
|
||
async function loadModalData() {
|
||
if (!currentModalWorker) return;
|
||
|
||
try {
|
||
// 모달 헤더 업데이트
|
||
document.getElementById('modalTitle').textContent = `${currentModalWorker.name} 작업 보고서`;
|
||
document.getElementById('modalWorkerName').textContent = currentModalWorker.name;
|
||
document.getElementById('modalWorkerDate').textContent = currentModalWorker.date;
|
||
document.getElementById('modalWorkerInitial').textContent = currentModalWorker.name.charAt(0);
|
||
|
||
// 병렬로 데이터 로드
|
||
await Promise.all([
|
||
loadModalExistingWork(),
|
||
loadModalDropdownData()
|
||
]);
|
||
|
||
// UI 업데이트
|
||
updateModalSummary();
|
||
renderModalExistingWork();
|
||
populateModalDropdowns();
|
||
|
||
} catch (error) {
|
||
console.error('모달 데이터 로드 오류:', error);
|
||
showToast('데이터 로드 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
}
|
||
|
||
async function loadModalExistingWork() {
|
||
try {
|
||
const response = await window.apiCall(`${window.API}/daily-work-reports?date=${currentModalWorker.date}&worker_id=${currentModalWorker.id}`);
|
||
modalExistingWork = Array.isArray(response) ? response : (response.data || []);
|
||
} catch (error) {
|
||
console.error('기존 작업 로드 오류:', error);
|
||
modalExistingWork = [];
|
||
}
|
||
}
|
||
|
||
async function loadModalDropdownData() {
|
||
try {
|
||
const [projectsRes, workTypesRes, workStatusRes, errorTypesRes] = await Promise.all([
|
||
window.apiCall(`${window.API}/projects`),
|
||
window.apiCall(`${window.API}/daily-work-reports/work-types`),
|
||
window.apiCall(`${window.API}/daily-work-reports/work-status-types`),
|
||
window.apiCall(`${window.API}/daily-work-reports/error-types`)
|
||
]);
|
||
|
||
modalProjects = Array.isArray(projectsRes) ? projectsRes : (projectsRes.data || []);
|
||
modalWorkTypes = Array.isArray(workTypesRes) ? workTypesRes : (workTypesRes.data || []);
|
||
modalWorkStatusTypes = Array.isArray(workStatusRes) ? workStatusRes : (workStatusRes.data || []);
|
||
modalErrorTypes = Array.isArray(errorTypesRes) ? errorTypesRes : (errorTypesRes.data || []);
|
||
} catch (error) {
|
||
console.error('드롭다운 데이터 로드 오류:', error);
|
||
}
|
||
}
|
||
|
||
function updateModalSummary() {
|
||
const totalHours = modalExistingWork.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
|
||
const workCount = modalExistingWork.length;
|
||
|
||
document.getElementById('modalTotalHours').textContent = `${totalHours.toFixed(1)}h`;
|
||
document.getElementById('modalWorkCount').textContent = `${workCount}건`;
|
||
|
||
// 12시간 초과 경고
|
||
if (totalHours > 12) {
|
||
document.getElementById('modalTotalHours').classList.add('warning');
|
||
} else {
|
||
document.getElementById('modalTotalHours').classList.remove('warning');
|
||
}
|
||
}
|
||
|
||
function renderModalExistingWork() {
|
||
const container = document.getElementById('modalExistingWork');
|
||
|
||
if (modalExistingWork.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="modal-empty-state">
|
||
<div class="modal-empty-icon">—</div>
|
||
<p>등록된 작업이 없습니다.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = modalExistingWork.map(work => `
|
||
<div class="modal-work-item">
|
||
<div class="modal-work-info">
|
||
<h5>${work.project_name || '미지정 프로젝트'}</h5>
|
||
<p>${work.work_type_name || '미지정 작업'}</p>
|
||
${work.work_status_id === 2 && work.error_type_name ? `
|
||
<span class="modal-error-badge">${work.error_type_name}</span>
|
||
` : ''}
|
||
</div>
|
||
<div class="modal-work-actions">
|
||
<span class="modal-work-hours">${work.work_hours}h</span>
|
||
<button class="btn btn-xs btn-danger" onclick="deleteModalWork(${work.id})">삭제</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function populateModalDropdowns() {
|
||
// 프로젝트 드롭다운
|
||
const projectSelect = document.getElementById('modalProjectSelect');
|
||
projectSelect.innerHTML = '<option value="">프로젝트 선택</option>';
|
||
modalProjects.forEach(project => {
|
||
projectSelect.innerHTML += `<option value="${project.project_id}">${project.project_name}</option>`;
|
||
});
|
||
|
||
// 작업 유형 드롭다운
|
||
const workTypeSelect = document.getElementById('modalWorkTypeSelect');
|
||
workTypeSelect.innerHTML = '<option value="">작업 유형 선택</option>';
|
||
modalWorkTypes.forEach(type => {
|
||
workTypeSelect.innerHTML += `<option value="${type.id}">${type.name}</option>`;
|
||
});
|
||
|
||
// 작업 상태 드롭다운
|
||
const workStatusSelect = document.getElementById('modalWorkStatusSelect');
|
||
workStatusSelect.innerHTML = '<option value="">업무 상태 선택</option>';
|
||
modalWorkStatusTypes.forEach(status => {
|
||
workStatusSelect.innerHTML += `<option value="${status.id}">${status.name}</option>`;
|
||
});
|
||
|
||
// 에러 유형 드롭다운
|
||
const errorTypeSelect = document.getElementById('modalErrorTypeSelect');
|
||
errorTypeSelect.innerHTML = '<option value="">에러 유형 선택</option>';
|
||
modalErrorTypes.forEach(error => {
|
||
errorTypeSelect.innerHTML += `<option value="${error.id}">${error.name}</option>`;
|
||
});
|
||
}
|
||
|
||
function showModalNewWorkForm() {
|
||
document.getElementById('modalNewWorkSection').style.display = 'block';
|
||
document.getElementById('modalAddWorkBtn').style.display = 'none';
|
||
}
|
||
|
||
function hideModalNewWorkForm() {
|
||
document.getElementById('modalNewWorkSection').style.display = 'none';
|
||
document.getElementById('modalAddWorkBtn').style.display = 'block';
|
||
resetModalForm();
|
||
}
|
||
|
||
function resetModalForm() {
|
||
document.getElementById('modalProjectSelect').value = '';
|
||
document.getElementById('modalWorkTypeSelect').value = '';
|
||
document.getElementById('modalWorkStatusSelect').value = '';
|
||
document.getElementById('modalErrorTypeSelect').value = '';
|
||
document.getElementById('modalWorkHours').value = '1.00';
|
||
document.getElementById('modalErrorTypeGroup').style.display = 'none';
|
||
}
|
||
|
||
function toggleModalErrorType() {
|
||
const workStatusSelect = document.getElementById('modalWorkStatusSelect');
|
||
const errorTypeGroup = document.getElementById('modalErrorTypeGroup');
|
||
|
||
if (workStatusSelect.value === '2') { // 에러 상태
|
||
errorTypeGroup.style.display = 'block';
|
||
} else {
|
||
errorTypeGroup.style.display = 'none';
|
||
document.getElementById('modalErrorTypeSelect').value = '';
|
||
}
|
||
}
|
||
|
||
async function saveModalNewWork() {
|
||
try {
|
||
const projectId = document.getElementById('modalProjectSelect').value;
|
||
const workTypeId = document.getElementById('modalWorkTypeSelect').value;
|
||
const workStatusId = document.getElementById('modalWorkStatusSelect').value;
|
||
const errorTypeId = document.getElementById('modalErrorTypeSelect').value;
|
||
const workHours = document.getElementById('modalWorkHours').value;
|
||
|
||
// 유효성 검사
|
||
if (!projectId || !workTypeId || !workStatusId || !workHours) {
|
||
showToast('모든 필수 필드를 입력해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
if (workStatusId === '2' && !errorTypeId) {
|
||
showToast('에러 상태일 때는 에러 유형을 선택해야 합니다.', 'error');
|
||
return;
|
||
}
|
||
|
||
const workData = {
|
||
report_date: currentModalWorker.date,
|
||
worker_id: currentModalWorker.id,
|
||
project_id: parseInt(projectId),
|
||
work_type_id: parseInt(workTypeId),
|
||
work_status_id: parseInt(workStatusId),
|
||
error_type_id: workStatusId === '2' ? parseInt(errorTypeId) : null,
|
||
work_hours: parseFloat(workHours),
|
||
created_by: currentUser?.user_id || 1
|
||
};
|
||
|
||
await window.apiCall(`${window.API}/daily-work-reports`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(workData)
|
||
});
|
||
|
||
showToast('작업이 성공적으로 저장되었습니다.', 'success');
|
||
|
||
// 데이터 새로고침
|
||
await loadModalExistingWork();
|
||
updateModalSummary();
|
||
renderModalExistingWork();
|
||
hideModalNewWorkForm();
|
||
|
||
// 대시보드 데이터도 새로고침
|
||
await loadDashboardData();
|
||
|
||
} catch (error) {
|
||
console.error('작업 저장 오류:', error);
|
||
showToast(`작업 저장 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteModalWork(workId) {
|
||
if (!confirm('이 작업을 삭제하시겠습니까?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await window.apiCall(`${window.API}/daily-work-reports/${workId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
showToast('작업이 성공적으로 삭제되었습니다.', 'success');
|
||
|
||
// 데이터 새로고침
|
||
await loadModalExistingWork();
|
||
updateModalSummary();
|
||
renderModalExistingWork();
|
||
|
||
// 대시보드 데이터도 새로고침
|
||
await loadDashboardData();
|
||
|
||
} catch (error) {
|
||
console.error('작업 삭제 오류:', error);
|
||
showToast(`작업 삭제 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
async function handleModalVacation(vacationType) {
|
||
const vacationTypeMap = {
|
||
'full': { code: 'ANNUAL_FULL', name: '연차', hours: 8 },
|
||
'half': { code: 'ANNUAL_HALF', name: '반차', hours: 4 },
|
||
'half-half': { code: 'ANNUAL_QUARTER', name: '반반차', hours: 2 }
|
||
};
|
||
|
||
const vacation = vacationTypeMap[vacationType];
|
||
if (!vacation) return;
|
||
|
||
if (!confirm(`${vacation.name} 처리하시겠습니까?\n(${vacation.hours}시간으로 자동 입력됩니다)`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 새로운 근태 관리 API 사용
|
||
const vacationData = {
|
||
worker_id: currentModalWorker.id,
|
||
date: currentModalWorker.date,
|
||
vacation_type: vacation.code
|
||
};
|
||
|
||
const response = await window.apiCall('/attendance/vacation', 'POST', vacationData);
|
||
|
||
if (response.success) {
|
||
showToast(`${vacation.name} 처리가 완료되었습니다.`, 'success');
|
||
|
||
// 데이터 새로고침
|
||
await loadModalExistingWork();
|
||
updateModalSummary();
|
||
renderModalExistingWork();
|
||
|
||
// 대시보드 데이터도 새로고침
|
||
await loadDashboardData();
|
||
} else {
|
||
throw new Error(response.message || '휴가 처리에 실패했습니다.');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('휴가 처리 오류:', error);
|
||
showToast(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
// ========== 전역 함수 (HTML에서 호출) ========== //
|
||
window.loadDashboardData = loadDashboardData;
|
||
window.showToast = showToast;
|
||
window.updateSummaryCards = updateSummaryCards;
|
||
window.displayWorkers = displayWorkers;
|
||
window.openWorkerModal = openWorkerModal;
|
||
window.hideWorkerModal = hideWorkerModal;
|
||
window.deleteModalWork = deleteModalWork;
|
||
window.handleVacation = handleVacation;
|
||
window.confirmOvertime = confirmOvertime;
|