/** * TBM Mobile - Main UI Logic * tbm-mobile.html에서 추출된 인라인 JS (로직 변경 없음) */ (function() { 'use strict'; var currentTab = 'today'; var allSessions = []; var todaySessions = []; var currentUser = null; var loadedDays = 7; var esc = window.escapeHtml || function(s) { return s || ''; }; var todayAssignments = []; // 당일 배정 현황 // 세부 편집 상태 var deSessionId = null; var deSession = null; var deMembers = []; var deTasks = []; var deWpCats = []; var deWpMap = {}; // category_id -> [workplaces] var deSelected = {}; // index -> boolean (그룹 선택용) // 피커 상태 var pickerMode = ''; // 'task' | 'workplace' var pickerWpStep = 'category'; // 'category' | 'place' var pickerSelectedCatId = null; // busy guard - 비동기 함수 중복 호출 방지 var _busy = {}; function isBusy(key) { return !!_busy[key]; } function setBusy(key) { _busy[key] = true; } function clearBusy(key) { delete _busy[key]; } function showLoading(msg) { var el = document.getElementById('loadingOverlay'); if (el) { document.getElementById('loadingText').textContent = msg || '불러오는 중...'; el.classList.add('active'); } } function hideLoading() { var el = document.getElementById('loadingOverlay'); if (el) el.classList.remove('active'); } // 초기화 document.addEventListener('DOMContentLoaded', async function() { var now = new Date(); var days = ['일','월','화','수','목','금','토']; var dateEl = document.getElementById('headerDate'); if (dateEl) { dateEl.textContent = now.getFullYear() + '.' + String(now.getMonth()+1).padStart(2,'0') + '.' + String(now.getDate()).padStart(2,'0') + ' (' + days[now.getDay()] + ')'; } try { await window.waitForApi(8000); } catch(e) { document.getElementById('tbmContent').innerHTML = '
서버 연결에 실패했습니다
페이지를 새로고침해 주세요
'; return; } currentUser = JSON.parse(localStorage.getItem('sso_user') || '{}'); await loadData(); }); function getTodayStr() { var now = new Date(); return now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0'); } async function loadData() { try { var today = new Date(); var todayStr = getTodayStr(); var dates = []; for (var i = 0; i < loadedDays; i++) { var d = new Date(today); d.setDate(d.getDate() - i); dates.push(d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0')); } var API = window.TbmAPI; var promises = dates.map(function(date) { return API.fetchSessionsByDate(date); }); var results = await Promise.all(promises); allSessions = []; results.forEach(function(sessions) { if (sessions && sessions.length > 0) { allSessions = allSessions.concat(sessions); } }); // 당일 세션 = 오늘 날짜만 todaySessions = allSessions.filter(function(s) { var sDate = s.session_date ? s.session_date.split('T')[0] : ''; return sDate === todayStr; }); document.getElementById('todayCount').textContent = todaySessions.length; document.getElementById('allCount').textContent = allSessions.length; renderList(); } catch (error) { console.error('TBM 로드 오류:', error); document.getElementById('tbmContent').innerHTML = '
데이터를 불러올 수 없습니다
'; } } window.switchTab = function(tab) { currentTab = tab; document.querySelectorAll('.m-tab').forEach(function(el) { el.classList.toggle('active', el.dataset.tab === tab); }); renderList(); }; function isMySession(s) { var userId = currentUser.user_id; var workerId = currentUser.worker_id; var userName = currentUser.name; return (userId && String(s.created_by) === String(userId)) || (workerId && String(s.leader_id) === String(workerId)) || (userName && s.created_by_name === userName); } function renderList() { var sessions = currentTab === 'today' ? todaySessions : allSessions; var content = document.getElementById('tbmContent'); if (sessions.length === 0) { var emptyMsg = currentTab === 'today' ? '오늘 등록된 TBM이 없습니다' : '등록된 TBM이 없습니다'; content.innerHTML = '
' + '
📝
' + '
' + emptyMsg + '
' + (currentTab === 'all' ? '
최근 ' + loadedDays + '일 기준
' : '') + '
'; return; } var grouped = {}; sessions.forEach(function(s) { var date = s.session_date ? s.session_date.split('T')[0] : ''; if (/^\d{4}-\d{2}-\d{2}$/.test(date)) { /* ok */ } else if (s.session_date) { date = new Date(s.session_date).toISOString().split('T')[0]; } if (!grouped[date]) grouped[date] = []; grouped[date].push(s); }); var sortedDates = Object.keys(grouped).sort().reverse(); var todayStr = getTodayStr(); var yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0]; var html = ''; sortedDates.forEach(function(date) { var label = date; if (date === todayStr) label = '오늘'; else if (date === yesterday) label = '어제'; else { var parts = date.split('-'); var dayNames = ['일','월','화','수','목','금','토']; var dObj = new Date(date + 'T00:00:00'); label = parseInt(parts[1]) + '/' + parseInt(parts[2]) + ' (' + dayNames[dObj.getDay()] + ')'; } html += '
' + label + '
'; grouped[date].forEach(function(s) { var sid = s.session_id; var status = s.status || 'draft'; var leaderName = s.leader_name || s.created_by_name || '미지정'; var memberCount = (parseInt(s.team_member_count) || 0); var memberNames = s.team_member_names || ''; var subText = memberNames || '팀원 없음'; var isMine = isMySession(s); var transferCount = parseInt(s.transfer_count) || 0; var createdTime = ''; if (s.created_at) { try { var t = new Date(s.created_at); createdTime = String(t.getHours()).padStart(2,'0') + ':' + String(t.getMinutes()).padStart(2,'0'); } catch(e) {} } var statusLabel = status === 'completed' ? '완료' : (status === 'cancelled' ? '취소' : '진행'); var badge = ''; if (status === 'draft') { if (!s.task_id) { badge = '세부 미입력'; } else { badge = '입력 완료'; } } // 이동 뱃지 var transferBadge = ''; if (transferCount > 0) { transferBadge = '' + transferCount + '건 이동'; } // 당일 탭에서 다른 반장의 draft TBM 클릭 → 빼오기 시트 var clickAction; if (isMine && status === 'draft') { clickAction = 'openDetailEditSheet(' + sid + ')'; } else if (!isMine && status === 'draft' && currentTab === 'today') { clickAction = 'openPullSheet(' + sid + ')'; } else if (status !== 'draft') { clickAction = 'toggleDetail(' + sid + ')'; } else { clickAction = 'toggleDetail(' + sid + ')'; } var myTbmClass = isMine ? ' my-tbm' : ''; var leaderDisplay = esc(leaderName); if (!isMine && currentTab === 'today') { leaderDisplay += '타 반장'; } html += '
' + '
' + '
' + '
' + leaderDisplay + badge + transferBadge + '
' + '
' + esc(subText) + '
' + '
' + '
' + '
' + memberCount + '
' + (createdTime ? '
' + createdTime + '
' : '') + '
' + '
'; if (status !== 'draft') { var taskName = s.task_name || ''; var workplaceName = s.work_location || ''; html += '
' + '
상태' + statusLabel + '
' + '
입력자' + esc(leaderName) + '
' + (taskName ? '
작업' + esc(taskName) + '
' : '') + (workplaceName ? '
장소' + esc(workplaceName) + '
' : '') + '
인원' + esc(memberNames || '없음') + ' (' + memberCount + '명)
' + '
' + '' + '
' + '
'; } }); }); if (currentTab === 'all') { html += ''; } content.innerHTML = html; } window.toggleDetail = function(sid) { var row = document.querySelector('.m-tbm-row[data-sid="' + sid + '"]'); if (!row) return; document.querySelectorAll('.m-tbm-row.expanded').forEach(function(el) { if (el !== row) el.classList.remove('expanded'); }); row.classList.toggle('expanded'); }; window.loadMore = function() { loadedDays += 7; loadData(); }; // ─── 세부 편집 바텀시트 ─── window.openDetailEditSheet = async function(sid) { if (isBusy('detailEdit')) return; setBusy('detailEdit'); showLoading('불러오는 중...'); deSessionId = sid; deSelected = {}; try { var API = window.TbmAPI; var results = await Promise.all([ API.getSession(sid).catch(function() { return null; }), API.getTeamMembers(sid).catch(function() { return []; }), API.loadTasks().catch(function() { return []; }), API.loadWorkplaceCategories().catch(function() { return []; }), API.loadActiveWorkplacesList().catch(function() { return []; }) ]); deSession = results[0]; deMembers = results[1]; deTasks = results[2] || window.TbmState.allTasks || []; deWpCats = results[3] || window.TbmState.allWorkplaceCategories || []; var allWorkplaces = results[4]; if (!deSession) { window.showToast('TBM 정보를 불러올 수 없습니다.', 'error'); return; } if (deMembers.length === 0) { window.showToast('팀원이 없습니다.', 'error'); return; } // work_type 필터 var workTypeId = deSession.work_type_id || (deMembers[0] && deMembers[0].work_type_id); if (workTypeId) { deTasks = deTasks.filter(function(t) { return t.work_type_id == workTypeId; }); } // 작업장소 맵 (category_id 기준) deWpMap = {}; allWorkplaces.forEach(function(wp) { var catId = wp.category_id || 0; if (!deWpMap[catId]) deWpMap[catId] = []; deWpMap[catId].push(wp); }); renderDetailEditSheet(); document.getElementById('deSelectAll').checked = false; updateGroupBar(); document.getElementById('detailEditOverlay').style.display = 'block'; document.getElementById('detailEditSheet').style.display = 'block'; } catch(e) { console.error('세부 편집 로드 오류:', e); window.showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error'); } finally { hideLoading(); clearBusy('detailEdit'); } }; function renderDetailEditSheet() { var html = ''; deMembers.forEach(function(m, i) { var hasBoth = m.task_id && m.workplace_id; var cardClass = hasBoth ? 'filled' : 'unfilled'; var statusHtml = hasBoth ? '입력완료' : '미입력'; // work_hours 표시 var workHoursTag = ''; if (m.work_hours !== null && m.work_hours !== undefined) { workHoursTag = '' + parseFloat(m.work_hours) + 'h'; } // 분할 항목이면 프로젝트명 표시 var projectTag = ''; if (m.split_seq > 0 && m.project_name) { projectTag = '' + esc(m.project_name) + ''; } else if (m.project_name && m.project_id !== deSession.project_id) { projectTag = '' + esc(m.project_name) + ''; } var taskOptions = ''; deTasks.forEach(function(t) { var sel = (m.task_id && m.task_id == t.task_id) ? ' selected' : ''; taskOptions += ''; }); var currentCatId = m.workplace_category_id || ''; var catOptions = ''; deWpCats.forEach(function(c) { var sel = (currentCatId && currentCatId == c.category_id) ? ' selected' : ''; catOptions += ''; }); var wpOptions = ''; if (currentCatId && deWpMap[currentCatId]) { deWpMap[currentCatId].forEach(function(wp) { var sel = (m.workplace_id && m.workplace_id == wp.workplace_id) ? ' selected' : ''; wpOptions += ''; }); } html += '
' + '
' + '' + '' + esc(m.worker_name) + ' ' + '' + esc(m.job_type || '') + '' + workHoursTag + projectTag + statusHtml + '' + '
' + '
' + '
' + '작업' + '' + '
' + '
' + '장소' + '' + '' + '
' + '
' + '
'; }); document.getElementById('deWorkerList').innerHTML = html; } window.updateCardStatus = function(idx) { var card = document.getElementById('de_card_' + idx); var taskVal = document.getElementById('de_task_' + idx).value; var wpVal = document.getElementById('de_wp_' + idx).value; var statusEl = card.querySelector('.de-worker-status'); if (taskVal && wpVal) { card.className = 'de-worker-card filled'; statusEl.className = 'de-worker-status ok'; statusEl.textContent = '입력완료'; } else { card.className = 'de-worker-card unfilled'; statusEl.className = 'de-worker-status missing'; statusEl.textContent = '미입력'; } }; window.onDeWpCatChange = function(idx) { var catId = document.getElementById('de_wpcat_' + idx).value; var wpSel = document.getElementById('de_wp_' + idx); wpSel.innerHTML = ''; if (catId && deWpMap[catId]) { deWpMap[catId].forEach(function(wp) { wpSel.innerHTML += ''; }); } updateCardStatus(idx); }; // ─── 그룹 선택 ─── window.onWorkerCheck = function(idx) { deSelected[idx] = document.getElementById('de_check_' + idx).checked; var allChecked = true; for (var i = 0; i < deMembers.length; i++) { if (!deSelected[i]) { allChecked = false; break; } } document.getElementById('deSelectAll').checked = allChecked; updateGroupBar(); }; window.toggleSelectAll = function() { var checked = document.getElementById('deSelectAll').checked; for (var i = 0; i < deMembers.length; i++) { deSelected[i] = checked; document.getElementById('de_check_' + i).checked = checked; } updateGroupBar(); }; function getSelectedIndices() { var arr = []; for (var i = 0; i < deMembers.length; i++) { if (deSelected[i]) arr.push(i); } return arr; } function updateGroupBar() { var indices = getSelectedIndices(); var bar = document.getElementById('deGroupBar'); var countEl = document.getElementById('deSelectedCount'); var labelEl = document.getElementById('deGroupLabel'); if (indices.length > 0) { bar.className = 'de-group-bar visible'; labelEl.textContent = indices.length + '명 선택'; countEl.textContent = indices.length + '명'; } else { bar.className = 'de-group-bar'; countEl.textContent = ''; } } // ─── 피커 (작업/장소 선택 팝업) ─── window.openPicker = function(mode) { var indices = getSelectedIndices(); if (indices.length === 0) { window.showToast('작업자를 먼저 선택하세요.', 'error'); return; } pickerMode = mode; pickerWpStep = 'category'; pickerSelectedCatId = null; if (mode === 'task') { renderTaskPicker(); } else { renderWorkplaceCatPicker(); } document.getElementById('pickerOverlay').style.display = 'block'; document.getElementById('pickerSheet').style.display = 'block'; }; window.closePicker = function() { document.getElementById('pickerOverlay').style.display = 'none'; document.getElementById('pickerSheet').style.display = 'none'; }; function renderTaskPicker() { document.getElementById('pickerTitle').textContent = '작업 선택'; var listEl = document.getElementById('pickerList'); var html = ''; deTasks.forEach(function(t) { html += '
' + esc(t.task_name) + '
'; }); if (deTasks.length === 0) { html = '
등록된 작업이 없습니다
'; } listEl.innerHTML = html; // 새 작업 추가 영역 var addRow = document.getElementById('pickerAddRow'); addRow.style.display = 'flex'; document.getElementById('pickerAddInput').placeholder = '새 작업명 입력...'; document.getElementById('pickerAddInput').value = ''; document.getElementById('pickerAddBtn').onclick = function() { addNewTask(); }; } function renderWorkplaceCatPicker() { pickerWpStep = 'category'; document.getElementById('pickerTitle').textContent = '장소 분류 선택'; var listEl = document.getElementById('pickerList'); var html = ''; deWpCats.forEach(function(c) { var count = deWpMap[c.category_id] ? deWpMap[c.category_id].length : 0; html += '
' + esc(c.category_name) + '' + count + '개 장소' + '
'; }); if (deWpCats.length === 0) { html = '
등록된 분류가 없습니다
'; } listEl.innerHTML = html; document.getElementById('pickerAddRow').style.display = 'none'; } function renderWorkplacePicker(catId) { pickerWpStep = 'place'; pickerSelectedCatId = catId; var catName = ''; deWpCats.forEach(function(c) { if (c.category_id == catId) catName = c.category_name; }); document.getElementById('pickerTitle').textContent = esc(catName) + ' - 장소 선택'; var listEl = document.getElementById('pickerList'); var workplaces = deWpMap[catId] || []; var html = '
← 분류 다시 선택
'; workplaces.forEach(function(wp) { html += '
' + esc(wp.workplace_name) + '
'; }); if (workplaces.length === 0) { html += '
등록된 장소가 없습니다
'; } listEl.innerHTML = html; document.getElementById('pickerAddRow').style.display = 'none'; } window.pickTask = function(taskId) { var indices = getSelectedIndices(); indices.forEach(function(i) { document.getElementById('de_task_' + i).value = taskId; updateCardStatus(i); }); closePicker(); window.showToast(indices.length + '명에게 작업 적용', 'success'); }; window.pickWpCategory = function(catId) { renderWorkplacePicker(catId); }; window.pickWorkplace = function(catId, wpId) { var indices = getSelectedIndices(); indices.forEach(function(i) { // 분류 설정 document.getElementById('de_wpcat_' + i).value = catId; // 장소 옵션 갱신 var wpSel = document.getElementById('de_wp_' + i); wpSel.innerHTML = ''; if (deWpMap[catId]) { deWpMap[catId].forEach(function(wp) { wpSel.innerHTML += ''; }); } wpSel.value = wpId; updateCardStatus(i); }); closePicker(); window.showToast(indices.length + '명에게 장소 적용', 'success'); }; // ─── 새 작업/공정 추가 ─── async function addNewTask() { var name = document.getElementById('pickerAddInput').value.trim(); if (!name) { window.showToast('작업명을 입력하세요.', 'error'); return; } var workTypeId = deSession.work_type_id || (deMembers[0] && deMembers[0].work_type_id) || null; try { var res = await window.TbmAPI.createTask({ task_name: name, work_type_id: workTypeId }); if (res && res.success) { var newId = res.data.task_id; // deTasks에 추가 deTasks.push({ task_id: newId, task_name: name, work_type_id: workTypeId }); // 모든 작업자 드롭다운 갱신 for (var i = 0; i < deMembers.length; i++) { var sel = document.getElementById('de_task_' + i); var opt = document.createElement('option'); opt.value = newId; opt.textContent = name; sel.appendChild(opt); } // 피커 다시 렌더링 renderTaskPicker(); window.showToast('작업 "' + name + '" 추가됨', 'success'); } else { window.showToast('작업 추가 실패', 'error'); } } catch(e) { console.error(e); window.showToast('오류가 발생했습니다.', 'error'); } } window.closeDetailEditSheet = function() { document.getElementById('detailEditOverlay').style.display = 'none'; document.getElementById('detailEditSheet').style.display = 'none'; clearBusy('detailEdit'); }; // 저장 (부분 입력도 허용) window.saveDetailEdit = async function() { var members = []; for (var i = 0; i < deMembers.length; i++) { var m = deMembers[i]; var taskId = document.getElementById('de_task_' + i).value || null; var wpCatId = document.getElementById('de_wpcat_' + i).value || null; var wpId = document.getElementById('de_wp_' + i).value || null; members.push({ worker_id: m.worker_id, project_id: m.project_id || deSession.project_id || null, work_type_id: m.work_type_id || deSession.work_type_id || null, task_id: taskId ? parseInt(taskId) : null, workplace_category_id: wpCatId ? parseInt(wpCatId) : null, workplace_id: wpId ? parseInt(wpId) : null, work_detail: m.work_detail || null }); } var btn = document.getElementById('deSaveBtn'); btn.disabled = true; btn.textContent = '저장 중...'; try { await window.TbmAPI.clearTeamMembers(deSessionId); var res = await window.TbmAPI.addTeamMembers(deSessionId, members); if (res && res.success) { closeDetailEditSheet(); window.showToast('세부 내역이 저장되었습니다.', 'success'); await loadData(); } else { window.showToast('저장에 실패했습니다.', 'error'); } } catch(e) { console.error('세부 편집 저장 오류:', e); window.showToast('오류가 발생했습니다.', 'error'); } finally { btn.disabled = false; btn.textContent = '저장'; } }; // 완료 (미입력 있으면 차단) window.completeFromDetailSheet = function() { var incomplete = []; for (var i = 0; i < deMembers.length; i++) { var taskVal = document.getElementById('de_task_' + i).value; var wpVal = document.getElementById('de_wp_' + i).value; if (!taskVal || !wpVal) { incomplete.push(deMembers[i].worker_name); } } if (incomplete.length > 0) { window.showToast('미입력: ' + incomplete.join(', '), 'error'); return; } var sid = deSessionId; saveDetailEdit().then(function() { window.completeTbm(sid); }); }; window.deleteFromDetailSheet = function() { var sid = deSessionId; closeDetailEditSheet(); window.deleteTbm(sid); }; // ─── TBM 완료 바텀시트 ─── var completeSessionId = null; var completeTeamMembers = []; window.completeTbm = async function(sid) { if (isBusy('complete')) return; setBusy('complete'); showLoading('확인 중...'); completeSessionId = sid; try { completeTeamMembers = await window.TbmAPI.getTeamMembers(sid).catch(function() { return []; }); if (completeTeamMembers.length === 0) { window.showToast('팀원이 없습니다.', 'error'); return; } // 세부 미입력 작업자 체크 var incomplete = completeTeamMembers.filter(function(m) { return !m.task_id || !m.workplace_id; }); if (incomplete.length > 0) { var names = incomplete.map(function(m) { return m.worker_name; }).join(', '); window.showToast('세부 미입력: ' + names + ' - 세부 내역을 먼저 입력하세요.', 'error'); return; } renderCompleteSheet(); document.getElementById('completeOverlay').style.display = 'block'; document.getElementById('completeSheet').style.display = 'block'; } catch(e) { console.error(e); window.showToast('팀원 조회 중 오류가 발생했습니다.', 'error'); } finally { hideLoading(); clearBusy('complete'); } }; function renderCompleteSheet() { var html = ''; completeTeamMembers.forEach(function(m, i) { html += '
' + '
' + '
' + esc(m.worker_name) + ' (' + esc(m.job_type || '') + ')
' + '
' + '
' + '' + '' + '' + '
' + '
'; }); document.getElementById('completeWorkerList').innerHTML = html; } window.onAttTypeChange = function(idx) { var sel = document.getElementById('att_type_' + idx); var inp = document.getElementById('att_hours_' + idx); var hint = document.getElementById('att_hint_' + idx); var val = sel.value; if (val === 'overtime') { inp.style.display = 'block'; inp.placeholder = '+시간'; inp.value = ''; hint.textContent = ''; } else if (val === 'early') { inp.style.display = 'block'; inp.placeholder = '근무시간'; inp.value = ''; hint.textContent = ''; } else { inp.style.display = 'none'; inp.value = ''; var labels = { regular:'8h', annual:'연차 자동처리', half:'4h', quarter:'6h' }; hint.textContent = labels[val] || ''; } }; window.closeCompleteSheet = function() { document.getElementById('completeOverlay').style.display = 'none'; document.getElementById('completeSheet').style.display = 'none'; }; window.submitCompleteSheet = async function() { var attendanceData = []; for (var i = 0; i < completeTeamMembers.length; i++) { var type = document.getElementById('att_type_' + i).value; var hoursVal = document.getElementById('att_hours_' + i).value; var hours = hoursVal ? parseFloat(hoursVal) : null; if (type === 'overtime' && (!hours || hours <= 0)) { window.showToast(esc(completeTeamMembers[i].worker_name) + '의 추가 시간을 입력해주세요.', 'error'); return; } if (type === 'early' && (!hours || hours <= 0)) { window.showToast(esc(completeTeamMembers[i].worker_name) + '의 근무 시간을 입력해주세요.', 'error'); return; } attendanceData.push({ worker_id: completeTeamMembers[i].worker_id, attendance_type: type, attendance_hours: hours }); } var btn = document.getElementById('completeSheetBtn'); btn.disabled = true; btn.textContent = '처리 중...'; try { var now = new Date(); var endTime = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0'); var res = await window.apiCall('/tbm/sessions/' + completeSessionId + '/complete', 'POST', { end_time: endTime, attendance_data: attendanceData }); if (res && res.success) { closeCompleteSheet(); window.showToast('TBM이 완료 처리되었습니다.', 'success'); await loadData(); } else { window.showToast('완료 처리에 실패했습니다.', 'error'); } } catch(e) { console.error(e); window.showToast('오류가 발생했습니다.', 'error'); } finally { btn.disabled = false; btn.textContent = '완료 처리'; } }; window.deleteTbm = async function(sid) { if (!confirm('이 TBM을 삭제하시겠습니까?')) return; try { var res = await window.TbmAPI.deleteSession(sid); if (res && res.success) { window.showToast('TBM이 삭제되었습니다.', 'success'); await loadData(); } else { window.showToast('삭제에 실패했습니다.', 'error'); } } catch(e) { window.showToast('오류가 발생했습니다.', 'error'); } }; // ─── 분할 기능 ─── var splitMemberIdx = null; var splitOption = 'keep'; // 'keep' | 'send' var splitTargetSessionId = null; var cachedProjects = null; var cachedWorkTypes = null; // 프로젝트/공정 목록 로딩 (캐시) async function loadProjectsAndWorkTypes() { if (!cachedProjects) { try { cachedProjects = await window.TbmAPI.loadProjects() || []; } catch(e) { cachedProjects = []; } } if (!cachedWorkTypes) { try { cachedWorkTypes = await window.TbmAPI.loadWorkTypes() || []; } catch(e) { cachedWorkTypes = []; } } } function populateProjectSelect(selectId, currentProjectId) { var sel = document.getElementById(selectId); var html = ''; (cachedProjects || []).forEach(function(p) { html += ''; }); sel.innerHTML = html; } function populateWorkTypeSelect(selectId, currentWorkTypeId) { var sel = document.getElementById(selectId); var html = ''; (cachedWorkTypes || []).forEach(function(wt) { html += ''; }); sel.innerHTML = html; } window.openSplitSheet = async function(memberIdx) { if (isBusy('split')) return; setBusy('split'); showLoading('불러오는 중...'); splitMemberIdx = memberIdx; splitOption = 'keep'; splitTargetSessionId = null; var m = deMembers[memberIdx]; var currentHours = m.work_hours === null || m.work_hours === undefined ? 8 : parseFloat(m.work_hours); document.getElementById('splitTitle').textContent = esc(m.worker_name) + ' 작업 분할'; document.getElementById('splitSubtitle').textContent = '현재 ' + currentHours + 'h 배정'; document.getElementById('splitHours').value = ''; document.getElementById('splitHours').max = currentHours - 0.5; document.getElementById('splitRemainder').textContent = ''; document.getElementById('splitOptKeep').className = 'split-radio-item active'; document.getElementById('splitOptSend').className = 'split-radio-item'; document.getElementById('splitSessionPicker').style.display = 'none'; // 시간 입력 시 나머지 자동 계산 document.getElementById('splitHours').oninput = function() { var val = parseFloat(this.value); if (val && val > 0 && val < currentHours) { document.getElementById('splitRemainder').textContent = '나머지: ' + (currentHours - val) + 'h'; } else { document.getElementById('splitRemainder').textContent = ''; } }; // 프로젝트/공정 목록 로드 + 드롭다운 채우기 await loadProjectsAndWorkTypes(); populateProjectSelect('splitProjectId', null); populateWorkTypeSelect('splitWorkTypeId', null); // 다른 세션 목록 로드 (당일) loadSplitSessionList(); document.getElementById('splitOverlay').style.display = 'block'; document.getElementById('splitSheet').style.display = 'block'; hideLoading(); clearBusy('split'); }; async function loadSplitSessionList() { var todayStr = getTodayStr(); try { var sessions = await window.TbmAPI.fetchSessionsByDate(todayStr); if (sessions && sessions.length > 0) { var html = ''; sessions.forEach(function(s) { if (s.session_id === deSessionId) return; // 현재 세션 제외 if (s.status !== 'draft') return; // draft만 var leaderName = s.leader_name || s.created_by_name || '미지정'; var workType = s.work_type_name || ''; html += '
' + esc(leaderName) + (workType ? ' - ' + esc(workType) : '') + ' (' + (parseInt(s.team_member_count)||0) + '명)' + '
'; }); if (!html) html = '
다른 TBM이 없습니다
'; document.getElementById('splitSessionList').innerHTML = html; } } catch(e) { console.error(e); } } window.setSplitOption = function(opt) { splitOption = opt; splitTargetSessionId = null; document.getElementById('splitOptKeep').className = 'split-radio-item' + (opt === 'keep' ? ' active' : ''); document.getElementById('splitOptSend').className = 'split-radio-item' + (opt === 'send' ? ' active' : ''); document.getElementById('splitSessionPicker').style.display = opt === 'send' ? 'block' : 'none'; // 세션 선택 초기화 document.querySelectorAll('.split-session-item').forEach(function(el) { el.classList.remove('active'); }); }; window.selectSplitSession = function(sid) { splitTargetSessionId = sid; document.querySelectorAll('.split-session-item').forEach(function(el) { el.classList.toggle('active', parseInt(el.dataset.sid) === sid); }); }; window.closeSplitSheet = function() { document.getElementById('splitOverlay').style.display = 'none'; document.getElementById('splitSheet').style.display = 'none'; clearBusy('split'); }; window.saveSplit = async function() { var m = deMembers[splitMemberIdx]; var currentHours = m.work_hours === null || m.work_hours === undefined ? 8 : parseFloat(m.work_hours); var splitHours = parseFloat(document.getElementById('splitHours').value); if (!splitHours || splitHours <= 0 || splitHours >= currentHours) { window.showToast('올바른 시간을 입력하세요 (0 < 시간 < ' + currentHours + ')', 'error'); return; } var btn = document.getElementById('splitSaveBtn'); btn.disabled = true; btn.textContent = '처리 중...'; try { // 프로젝트/공정 선택값 var selProjectId = document.getElementById('splitProjectId').value; var selWorkTypeId = document.getElementById('splitWorkTypeId').value; if (splitOption === 'keep') { var remainHoursKeep = currentHours - splitHours; var newProjectId = selProjectId ? parseInt(selProjectId) : (m.project_id || null); var newWorkTypeId = selWorkTypeId ? parseInt(selWorkTypeId) : (m.work_type_id || null); // 1) 기존 항목: 시간만 줄이기 (프로젝트/공정 유지) await window.TbmAPI.updateTeamMember(deSessionId, { worker_id: m.worker_id, project_id: m.project_id || null, work_type_id: m.work_type_id || null, task_id: m.task_id || null, workplace_category_id: m.workplace_category_id || null, workplace_id: m.workplace_id || null, work_detail: m.work_detail || null, is_present: true, work_hours: splitHours }); // 2) 나머지 시간으로 새 항목 추가 (프로젝트/공정 변경 가능) await window.TbmAPI.splitAssignment(deSessionId, { worker_id: m.worker_id, work_hours: remainHoursKeep, project_id: newProjectId, work_type_id: newWorkTypeId }); closeSplitSheet(); // 세부 편집 데이터 다시 로드 deMembers = await window.TbmAPI.getTeamMembers(deSessionId).catch(function() { return deMembers; }); renderDetailEditSheet(); window.showToast('분할 완료: ' + splitHours + 'h + ' + remainHoursKeep + 'h', 'success'); } else if (splitOption === 'send') { if (!splitTargetSessionId) { window.showToast('이동할 TBM을 선택하세요.', 'error'); btn.disabled = false; btn.textContent = '분할 저장'; return; } var remainHours = currentHours - splitHours; var destProjectId = selProjectId ? parseInt(selProjectId) : (m.project_id || null); var destWorkTypeId = selWorkTypeId ? parseInt(selWorkTypeId) : (m.work_type_id || null); // transfer API 호출 var res = await window.TbmAPI.transfer({ transfer_type: 'send', worker_id: m.worker_id, source_session_id: deSessionId, dest_session_id: splitTargetSessionId, hours: remainHours, project_id: destProjectId, work_type_id: destWorkTypeId }); if (res && res.success) { closeSplitSheet(); closeDetailEditSheet(); window.showToast('이동 완료' + (res.data && res.data.warning ? ' (' + res.data.warning + ')' : ''), 'success'); await loadData(); } else { window.showToast(res?.message || '이동 실패', 'error'); } } } catch(e) { console.error('분할 오류:', e); window.showToast('오류가 발생했습니다.', 'error'); } finally { btn.disabled = false; btn.textContent = '분할 저장'; } }; // ─── 빼오기 기능 ─── var pullSessionId = null; var pullMembers = []; var pullWorker = null; // 빼오기 대상 var myDraftSession = null; // 내 draft TBM window.openPullSheet = async function(sid) { if (isBusy('pull')) return; setBusy('pull'); showLoading('불러오는 중...'); pullSessionId = sid; try { pullMembers = await window.TbmAPI.getTeamMembers(sid).catch(function() { return []; }); var session = await window.TbmAPI.getSession(sid).catch(function() { return null; }); var leaderName = session ? (session.leader_name || session.created_by_name || '미지정') : '미지정'; document.getElementById('pullTitle').textContent = esc(leaderName) + ' 반장 팀'; document.getElementById('pullSubtitle').textContent = pullMembers.length + '명 배정'; // 내 draft TBM 확인 myDraftSession = todaySessions.find(function(s) { return isMySession(s) && s.status === 'draft'; }); var html = ''; pullMembers.forEach(function(m) { var hours = m.work_hours === null || m.work_hours === undefined ? 8 : parseFloat(m.work_hours); var hoursText = hours + 'h'; var btnHtml = ''; if (!myDraftSession) { btnHtml = ''; } else { btnHtml = ''; } html += '
' + '
' + '
' + esc(m.worker_name) + ' ' + hoursText + '
' + '
' + esc(m.job_type || '') + '
' + '
' + btnHtml + '
'; }); if (pullMembers.length === 0) { html = '
팀원이 없습니다
'; } document.getElementById('pullMemberList').innerHTML = html; document.getElementById('pullOverlay').style.display = 'block'; document.getElementById('pullSheet').style.display = 'block'; } catch(e) { console.error('빼오기 로드 오류:', e); window.showToast('데이터를 불러올 수 없습니다.', 'error'); } finally { hideLoading(); clearBusy('pull'); } }; window.closePullSheet = function() { document.getElementById('pullOverlay').style.display = 'none'; document.getElementById('pullSheet').style.display = 'none'; clearBusy('pull'); }; window.startPull = async function(workerId, workerName, maxHours) { pullWorker = { worker_id: workerId, worker_name: workerName, max_hours: maxHours }; document.getElementById('pullHoursTitle').textContent = esc(workerName) + ' 빼오기'; document.getElementById('pullHoursSubtitle').textContent = '최대 ' + maxHours + 'h 가능'; document.getElementById('pullHoursInput').value = maxHours; document.getElementById('pullHoursInput').max = maxHours; // 프로젝트/공정 드롭다운 채우기 await loadProjectsAndWorkTypes(); var myProject = myDraftSession ? myDraftSession.project_id : null; var myWorkType = myDraftSession ? myDraftSession.work_type_id : null; populateProjectSelect('pullProjectId', myProject); populateWorkTypeSelect('pullWorkTypeId', myWorkType); document.getElementById('pullHoursOverlay').style.display = 'block'; document.getElementById('pullHoursSheet').style.display = 'block'; }; window.closePullHoursModal = function() { document.getElementById('pullHoursOverlay').style.display = 'none'; document.getElementById('pullHoursSheet').style.display = 'none'; }; window.confirmPull = async function() { var hours = parseFloat(document.getElementById('pullHoursInput').value); if (!hours || hours <= 0 || hours > pullWorker.max_hours) { window.showToast('올바른 시간을 입력하세요 (0 < 시간 <= ' + pullWorker.max_hours + ')', 'error'); return; } var btn = document.getElementById('pullHoursSaveBtn'); btn.disabled = true; btn.textContent = '처리 중...'; try { var pullProjectId = document.getElementById('pullProjectId').value || null; var pullWorkTypeId = document.getElementById('pullWorkTypeId').value || null; var res = await window.TbmAPI.transfer({ transfer_type: 'pull', worker_id: pullWorker.worker_id, source_session_id: pullSessionId, dest_session_id: myDraftSession.session_id, hours: hours, project_id: pullProjectId ? parseInt(pullProjectId) : null, work_type_id: pullWorkTypeId ? parseInt(pullWorkTypeId) : null }); if (res && res.success) { closePullHoursModal(); closePullSheet(); window.showToast(esc(pullWorker.worker_name) + ' ' + hours + 'h 빼오기 완료' + (res.data && res.data.warning ? ' (' + res.data.warning + ')' : ''), 'success'); await loadData(); } else { window.showToast(res?.message || '빼오기 실패', 'error'); } } catch(e) { console.error('빼오기 오류:', e); window.showToast('오류가 발생했습니다.', 'error'); } finally { btn.disabled = false; btn.textContent = '빼오기 실행'; } }; // ─── 인계 바텀시트 ─── var handoverSessionId = null; var handoverSession = null; window.handoverFromDetailSheet = function() { var sid = deSessionId; closeDetailEditSheet(); openHandoverSheet(sid); }; async function openHandoverSheet(sid) { if (isBusy('handover')) return; setBusy('handover'); showLoading('인계 정보 불러오는 중...'); handoverSessionId = sid; try { var API = window.TbmAPI; var results = await Promise.all([ API.getSession(sid).catch(function() { return null; }), API.getTeamMembers(sid).catch(function() { return []; }), API.loadWorkers().catch(function() { return []; }) ]); handoverSession = results[0]; var team = results[1]; var workers = results[2]; if (!handoverSession) { window.showToast('세션 정보를 불러올 수 없습니다.', 'error'); return; } // 현재 세션 리더를 제외한 반장/그룹장 목록 var leaders = workers.filter(function(w) { return (w.job_type === 'leader' || w.job_type === '그룹장' || w.job_type === 'admin') && w.worker_id !== handoverSession.leader_id; }); var leaderSelect = document.getElementById('handoverLeaderId'); leaderSelect.innerHTML = '' + leaders.map(function(w) { return ''; }).join(''); // 인계할 팀원 체크리스트 var listEl = document.getElementById('handoverWorkerList'); if (team.length === 0) { listEl.innerHTML = '

팀원이 없습니다.

'; } else { listEl.innerHTML = team.map(function(m) { return ''; }).join(''); } document.getElementById('handoverNotes').value = ''; document.getElementById('handoverOverlay').style.display = 'block'; document.getElementById('handoverSheet').style.display = 'block'; } catch(e) { console.error('인계 시트 열기 오류:', e); window.showToast('인계 정보를 불러오는 중 오류가 발생했습니다.', 'error'); } finally { hideLoading(); clearBusy('handover'); } } window.openHandoverSheet = openHandoverSheet; window.closeHandoverSheet = function() { document.getElementById('handoverOverlay').style.display = 'none'; document.getElementById('handoverSheet').style.display = 'none'; }; window.submitHandover = async function() { var toLeaderId = parseInt(document.getElementById('handoverLeaderId').value); var notes = document.getElementById('handoverNotes').value; if (!toLeaderId) { window.showToast('인계 대상 반장을 선택해주세요.', 'error'); return; } var workerIds = []; document.querySelectorAll('.handover-worker-cb:checked').forEach(function(cb) { workerIds.push(parseInt(cb.value)); }); if (workerIds.length === 0) { window.showToast('인계할 팀원을 최소 1명 선택해주세요.', 'error'); return; } var btn = document.querySelector('#handoverSheet .split-btn'); btn.disabled = true; btn.textContent = '처리 중...'; try { var today = getTodayStr(); var now = new Date().toTimeString().slice(0, 5); var handoverData = { session_id: handoverSessionId, from_leader_id: handoverSession.leader_id, to_leader_id: toLeaderId, handover_date: today, handover_time: now, reason: '모바일 인계', handover_notes: notes, worker_ids: workerIds }; var res = await window.TbmAPI.saveHandover(handoverData); if (res && res.success) { window.closeHandoverSheet(); window.showToast('작업 인계가 요청되었습니다.', 'success'); await loadData(); } else { window.showToast(res?.message || '인계 요청에 실패했습니다.', 'error'); } } catch(e) { console.error('인계 저장 오류:', e); window.showToast('인계 중 오류가 발생했습니다.', 'error'); } finally { btn.disabled = false; btn.textContent = '인계 요청'; } }; })();