/** * my-monthly-confirm.js — 작업자 월간 근무 확인 (모바일 캘린더) */ var DAYS_KR = ['일', '월', '화', '수', '목', '금', '토']; var currentYear, currentMonth; var isProcessing = false; var selectedCell = null; var currentConfStatus = null; // 현재 confirmation 상태 var pendingChanges = {}; // 수정 내역 { 'YYYY-MM-DD': { from: '반차', to: '정시', hours: 8 } } var loadedRecords = []; // 로드된 daily_records // ===== Init ===== document.addEventListener('DOMContentLoaded', function() { var now = new Date(); currentYear = now.getFullYear(); currentMonth = now.getMonth() + 1; var params = new URLSearchParams(location.search); if (params.get('year')) currentYear = parseInt(params.get('year')); if (params.get('month')) currentMonth = parseInt(params.get('month')); setTimeout(function() { var user = typeof getCurrentUser === 'function' ? getCurrentUser() : window.currentUser; if (!user) return; window._mmcUser = user; updateMonthLabel(); loadData(); }, 500); }); function updateMonthLabel() { document.getElementById('monthLabel').textContent = currentYear + '년 ' + currentMonth + '월'; } function changeMonth(delta) { currentMonth += delta; if (currentMonth > 12) { currentMonth = 1; currentYear++; } if (currentMonth < 1) { currentMonth = 12; currentYear--; } selectedCell = null; updateMonthLabel(); loadData(); } // ===== Data Load ===== async function loadData() { var calWrap = document.getElementById('tableWrap'); calWrap.innerHTML = '
'; try { var user = window._mmcUser || (typeof getCurrentUser === 'function' ? getCurrentUser() : null) || {}; var userId = user.user_id || user.id; var [recordsRes, balanceRes] = await Promise.all([ window.apiCall('/monthly-comparison/my-records?year=' + currentYear + '&month=' + currentMonth), window.apiCall('/vacation-balances/worker/' + userId + '/year/' + currentYear).catch(function() { return { success: true, data: [] }; }) ]); if (!recordsRes || !recordsRes.success) { calWrap.innerHTML = '

데이터가 없습니다

'; document.getElementById('bottomActions').classList.add('hidden'); return; } var data = recordsRes.data; renderUserInfo(data.user); renderCalendar(data.daily_records || []); renderSummaryCards(data.daily_records || []); loadedRecords = data.daily_records || []; currentConfStatus = data.confirmation ? data.confirmation.status : 'pending'; pendingChanges = {}; renderVacationBalance(balanceRes.data || []); renderConfirmStatus(data.confirmation); } catch (e) { calWrap.innerHTML = '

네트워크 오류

'; } } // ===== Render ===== function renderUserInfo(user) { if (!user) return; document.getElementById('userName').textContent = user.worker_name || user.name || '-'; document.getElementById('userDept').textContent = (user.job_type ? user.job_type + ' · ' : '') + (user.department_name || ''); } // 셀 텍스트 판정 // 8h 기준 고정 (scheduled_hours 미존재 — 단축근무 미대응) function getCellInfo(r) { var hrs = r.attendance ? parseFloat(r.attendance.total_work_hours) || 0 : 0; var vacType = r.attendance ? r.attendance.vacation_type : null; var isHoliday = r.is_holiday; if (vacType) return { text: vacType, cls: 'vac', detail: vacType }; if (isHoliday && hrs <= 0) return { text: '휴무', cls: 'off', detail: r.holiday_name || '휴무' }; if (isHoliday && hrs > 0) return { text: '특 ' + hrs + 'h', cls: 'special', detail: '특근 ' + hrs + '시간' }; if (hrs === 8) return { text: '정시', cls: 'normal', detail: '정시근로 8시간' }; if (hrs > 8) return { text: '+' + (hrs - 8) + 'h', cls: 'overtime', detail: '연장근로 ' + hrs + '시간 (+' + (hrs - 8) + ')' }; if (hrs > 0) return { text: hrs + 'h', cls: 'partial', detail: hrs + '시간 근무' }; return { text: '-', cls: 'none', detail: '미입력' }; } function renderCalendar(records) { var el = document.getElementById('tableWrap'); if (!records.length) { el.innerHTML = '

해당 월 데이터가 없습니다

'; return; } // 날짜별 맵 var recMap = {}; records.forEach(function(r) { recMap[parseInt(r.date.substring(8))] = r; }); var firstDay = new Date(currentYear, currentMonth - 1, 1).getDay(); var daysInMonth = new Date(currentYear, currentMonth, 0).getDate(); // 헤더 var html = '
'; html += '
'; DAYS_KR.forEach(function(d, i) { var cls = i === 0 ? ' sun' : i === 6 ? ' sat' : ''; html += '
' + d + '
'; }); html += '
'; // 셀 html += '
'; // 빈 셀 (월 시작 전) for (var i = 0; i < firstDay; i++) { html += '
'; } for (var day = 1; day <= daysInMonth; day++) { var r = recMap[day]; var info = r ? getCellInfo(r) : { text: '-', cls: 'none', detail: '데이터 없음' }; var dow = (firstDay + day - 1) % 7; var dowCls = dow === 0 ? ' sun' : dow === 6 ? ' sat' : ''; html += '
'; html += '' + day + ''; html += '' + escHtml(info.text) + ''; html += '
'; } html += '
'; // 상세 영역 html += '
'; el.innerHTML = html; } function selectDay(day) { selectedCell = day; var el = document.getElementById('calDetail'); var cells = document.querySelectorAll('.cal-cell'); cells.forEach(function(c) { c.classList.remove('selected'); }); var allCells = document.querySelectorAll('.cal-cell:not(.empty)'); if (allCells[day - 1]) allCells[day - 1].classList.add('selected'); var dateStr = currentYear + '-' + String(currentMonth).padStart(2, '0') + '-' + String(day).padStart(2, '0'); var d = new Date(currentYear, currentMonth - 1, day); var dow = DAYS_KR[d.getDay()]; var record = loadedRecords.find(function(r) { return parseInt(r.date.substring(8)) === day; }); var currentVal = record ? getCellInfo(record).text : '-'; var html = '
'; html += '' + currentMonth + '/' + day + ' (' + dow + ') — ' + escHtml(currentVal); // review_sent 상태에서만 수정 드롭다운 표시 if (currentConfStatus === 'review_sent') { var changed = pendingChanges[dateStr]; html += '
'; html += ''; if (changed) html += ' 수정'; html += '
'; } html += '
'; el.innerHTML = html; el.style.display = 'block'; updateChangeRequestBtn(); } function onCellChange(day) { var dateStr = currentYear + '-' + String(currentMonth).padStart(2, '0') + '-' + String(day).padStart(2, '0'); var sel = document.getElementById('editType-' + day); var newType = sel ? sel.value : ''; var record = loadedRecords.find(function(r) { return parseInt(r.date.substring(8)) === day; }); var currentType = record ? getCellInfo(record).text : '-'; if (newType && newType !== currentType) { var hoursMap = { '정시': 8, '연차': 0, '반차': 4, '반반차': 6, '조퇴': 2, '휴무': 0 }; pendingChanges[dateStr] = { from: currentType, to: newType, hours: hoursMap[newType] || 0 }; // 셀에 수정 뱃지 var allCells = document.querySelectorAll('.cal-cell:not(.empty)'); if (allCells[day - 1]) allCells[day - 1].classList.add('changed'); } else { delete pendingChanges[dateStr]; var allCells2 = document.querySelectorAll('.cal-cell:not(.empty)'); if (allCells2[day - 1]) allCells2[day - 1].classList.remove('changed'); } updateChangeRequestBtn(); // 상세 영역 재렌더 selectDay(day); } function updateChangeRequestBtn() { var rejectBtn = document.getElementById('rejectBtn'); if (!rejectBtn) return; var changeCount = Object.keys(pendingChanges).length; if (currentConfStatus === 'review_sent' && changeCount > 0) { rejectBtn.disabled = false; rejectBtn.innerHTML = '수정요청 (' + changeCount + '건)'; } else if (currentConfStatus === 'review_sent') { rejectBtn.disabled = true; rejectBtn.innerHTML = '수정요청'; } } function renderSummaryCards(records) { var workDays = 0, overtimeHours = 0, vacDays = 0; records.forEach(function(r) { var hrs = r.attendance ? parseFloat(r.attendance.total_work_hours) || 0 : 0; var vacType = r.attendance ? r.attendance.vacation_type : null; var isHoliday = r.is_holiday; if (!isHoliday && (hrs > 0 || vacType)) workDays++; if (hrs > 8) overtimeHours += (hrs - 8); if (vacType) { var vd = r.attendance.vacation_days ? parseFloat(r.attendance.vacation_days) : 0; if (vd > 0) { vacDays += vd; } else { // fallback: vacation_type 이름으로 차감일수 매핑 var deductMap = { '연차': 1, '반차': 0.5, '반반차': 0.25, '조퇴': 0.75, '병가': 1 }; vacDays += deductMap[vacType] || 1; } } }); var el = document.getElementById('summaryCards'); if (!el) return; el.innerHTML = '
' + workDays + '
근무일
' + '
' + fmtNum(overtimeHours) + 'h
연장근로
' + '
' + fmtNum(vacDays) + '일
연차
'; } function renderVacationBalance(balances) { var el = document.getElementById('vacationCards'); var total = 0, used = 0; if (Array.isArray(balances)) { balances.forEach(function(b) { total += parseFloat(b.total_days || 0); used += parseFloat(b.used_days || 0); }); } var remaining = total - used; el.innerHTML = '
연차 현황
' + '
' + '
' + fmtNum(total) + '
부여
' + '
' + fmtNum(used) + '
사용
' + '
' + fmtNum(remaining) + '
잔여
' + '
'; } function renderConfirmStatus(conf) { var actions = document.getElementById('bottomActions'); var statusEl = document.getElementById('confirmedStatus'); var badge = document.getElementById('statusBadge'); var confirmBtn = document.getElementById('confirmBtn'); var rejectBtn = document.getElementById('rejectBtn'); var status = conf ? conf.status : 'pending'; // 기본: 버튼 숨김 + 상태 숨김 actions.classList.add('hidden'); statusEl.classList.add('hidden'); if (status === 'pending') { badge.textContent = '검토대기'; badge.className = 'mmc-status-badge pending'; statusEl.classList.remove('hidden'); document.getElementById('confirmedText').textContent = '관리자 검토 대기 중입니다'; } else if (status === 'review_sent') { badge.textContent = '확인요청'; badge.className = 'mmc-status-badge review_sent'; actions.classList.remove('hidden'); confirmBtn.innerHTML = '확인 완료'; rejectBtn.innerHTML = '수정요청'; rejectBtn.disabled = true; // 수정 내역 없으면 비활성화 rejectBtn.onclick = function() { submitChangeRequest(); }; } else if (status === 'confirmed') { badge.textContent = '확인완료'; badge.className = 'mmc-status-badge confirmed'; statusEl.classList.remove('hidden'); var dt = conf.confirmed_at ? new Date(conf.confirmed_at).toLocaleDateString('ko') : ''; document.getElementById('confirmedText').textContent = dt + ' 확인 완료'; } else if (status === 'change_request') { badge.textContent = '수정요청'; badge.className = 'mmc-status-badge change_request'; statusEl.classList.remove('hidden'); document.getElementById('confirmedText').textContent = '수정요청이 제출되었습니다. 관리자 확인 대기 중'; } else if (status === 'rejected') { badge.textContent = '반려'; badge.className = 'mmc-status-badge rejected'; actions.classList.remove('hidden'); confirmBtn.innerHTML = '동의(재확인)'; rejectBtn.classList.add('hidden'); statusEl.classList.remove('hidden'); document.getElementById('confirmedText').textContent = '반려 사유: ' + (conf.reject_reason || '-') + '\n반려 사유를 확인하고 동의하시면 확인 완료 버튼을 눌러주세요.'; } } function openChangeRequestModal() { document.getElementById('rejectReason').value = ''; document.getElementById('rejectModal').classList.remove('hidden'); // 모달 제목/버튼 수정요청용으로 변경 var header = document.querySelector('.mmc-modal-header span'); if (header) header.innerHTML = '수정요청'; var submitBtn = document.querySelector('.mmc-modal-submit'); if (submitBtn) submitBtn.textContent = '수정요청 제출'; var desc = document.querySelector('.mmc-modal-desc'); if (desc) desc.textContent = '수정이 필요한 내용을 입력해주세요:'; var note = document.querySelector('.mmc-modal-note'); if (note) note.innerHTML = '수정요청 시 관리자에게 알림이 전달됩니다.'; } // ===== Actions ===== async function confirmMonth() { if (isProcessing) return; if (!confirm(currentYear + '년 ' + currentMonth + '월 근무 내역을 확인하시겠습니까?')) return; isProcessing = true; try { var res = await window.apiCall('/monthly-comparison/confirm', 'POST', { year: currentYear, month: currentMonth, status: 'confirmed' }); if (res && res.success) { showToast(res.message || '확인 완료', 'success'); loadData(); } else { showToast(res && res.message || '처리 실패', 'error'); } } catch (e) { showToast('네트워크 오류', 'error'); } finally { isProcessing = false; } } async function submitChangeRequest() { if (isProcessing) return; var changeCount = Object.keys(pendingChanges).length; if (changeCount === 0) { showToast('수정 내역이 없습니다', 'error'); return; } if (!confirm(changeCount + '건의 수정요청을 제출하시겠습니까?')) return; isProcessing = true; try { var changes = Object.keys(pendingChanges).map(function(date) { return { date: date, from: pendingChanges[date].from, to: pendingChanges[date].to }; }); var res = await window.apiCall('/monthly-comparison/confirm', 'POST', { year: currentYear, month: currentMonth, status: 'change_request', change_details: { changes: changes } }); if (res && res.success) { showToast(res.message || '수정요청 완료', 'success'); loadData(); } else { showToast(res && res.message || '처리 실패', 'error'); } } catch (e) { showToast('네트워크 오류', 'error'); } finally { isProcessing = false; } } function openRejectModal() { document.getElementById('rejectReason').value = ''; document.getElementById('rejectModal').classList.remove('hidden'); } function closeRejectModal() { document.getElementById('rejectModal').classList.add('hidden'); } async function submitReject() { if (isProcessing) return; var reason = document.getElementById('rejectReason').value.trim(); if (!reason) { showToast('수정 내용을 입력해주세요', 'error'); return; } isProcessing = true; try { var res = await window.apiCall('/monthly-comparison/confirm', 'POST', { year: currentYear, month: currentMonth, status: 'change_request', change_details: { description: reason } }); if (res && res.success) { showToast(res.message || '수정요청 완료', 'success'); closeRejectModal(); loadData(); } else { showToast(res && res.message || '처리 실패', 'error'); } } catch (e) { showToast('네트워크 오류', 'error'); } finally { isProcessing = false; } } // ===== Helpers ===== function fmtNum(v) { var n = parseFloat(v) || 0; return n % 1 === 0 ? n.toString() : n.toFixed(1); } function escHtml(s) { return (s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // showToast — tkfb-core.js의 전역 showToast 사용 (재정의 불필요) function handleEscKey(e) { if (e.key === 'Escape') closeRejectModal(); } document.addEventListener('keydown', handleEscKey); window.addEventListener('beforeunload', function() { document.removeEventListener('keydown', handleEscKey); });