- 공통 유틸리티 추출 (common/utils.js, common/base-state.js) - TBM 모바일 인라인 JS/CSS 외부 파일로 분리 (tbm-mobile.js, tbm-mobile.css) - 미사용 코드 삭제 (index.js, work-report-*.js 등 5개 파일) - TBM/작업보고 state.js, utils.js를 공통 모듈 기반으로 전환 - 작업보고서 SSO 인증 호환 수정 (token/user 함수) - tbmModel.js: incomplete-reports 쿼리에서 users→sso_users 조인 수정, leader_name 조인 추가 - docker-compose.yml: system1-web 볼륨 마운트 추가 - 모바일 인계(handover) 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1330 lines
52 KiB
JavaScript
1330 lines
52 KiB
JavaScript
/**
|
|
* 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 =
|
|
'<div class="m-empty"><div class="m-empty-icon">⚠</div><div class="m-empty-text">서버 연결에 실패했습니다</div><div class="m-empty-sub">페이지를 새로고침해 주세요</div></div>';
|
|
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 =
|
|
'<div class="m-empty"><div class="m-empty-text">데이터를 불러올 수 없습니다</div></div>';
|
|
}
|
|
}
|
|
|
|
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 =
|
|
'<div class="m-empty">' +
|
|
'<div class="m-empty-icon">📝</div>' +
|
|
'<div class="m-empty-text">' + emptyMsg + '</div>' +
|
|
(currentTab === 'all' ? '<div class="m-empty-sub">최근 ' + loadedDays + '일 기준</div>' : '') +
|
|
'</div>';
|
|
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 += '<div class="m-date-group"><span class="m-date-label">' + label + '</span></div>';
|
|
|
|
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 = '<span class="m-detail-badge incomplete">세부 미입력</span>';
|
|
} else {
|
|
badge = '<span class="m-detail-badge complete">입력 완료</span>';
|
|
}
|
|
}
|
|
|
|
// 이동 뱃지
|
|
var transferBadge = '';
|
|
if (transferCount > 0) {
|
|
transferBadge = '<span class="m-transfer-badge">' + transferCount + '건 이동</span>';
|
|
}
|
|
|
|
// 당일 탭에서 다른 반장의 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 += '<span class="m-leader-badge">타 반장</span>';
|
|
}
|
|
|
|
html += '<div class="m-tbm-row' + myTbmClass + '" data-sid="' + sid + '" onclick="' + clickAction + '">' +
|
|
'<div class="m-row-status ' + status + '" title="' + statusLabel + '"></div>' +
|
|
'<div class="m-row-body">' +
|
|
'<div class="m-row-main">' + leaderDisplay + badge + transferBadge + '</div>' +
|
|
'<div class="m-row-sub">' + esc(subText) + '</div>' +
|
|
'</div>' +
|
|
'<div class="m-row-right">' +
|
|
'<div class="m-row-count">' + memberCount + '<span class="m-row-count-label">명</span></div>' +
|
|
(createdTime ? '<div class="m-row-time">' + createdTime + '</div>' : '') +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
if (status !== 'draft') {
|
|
var taskName = s.task_name || '';
|
|
var workplaceName = s.work_location || '';
|
|
html += '<div class="m-tbm-detail" id="detail_' + sid + '">' +
|
|
'<div class="m-detail-row"><span class="m-detail-label">상태</span><span class="m-detail-value">' + statusLabel + '</span></div>' +
|
|
'<div class="m-detail-row"><span class="m-detail-label">입력자</span><span class="m-detail-value">' + esc(leaderName) + '</span></div>' +
|
|
(taskName ? '<div class="m-detail-row"><span class="m-detail-label">작업</span><span class="m-detail-value">' + esc(taskName) + '</span></div>' : '') +
|
|
(workplaceName ? '<div class="m-detail-row"><span class="m-detail-label">장소</span><span class="m-detail-value">' + esc(workplaceName) + '</span></div>' : '') +
|
|
'<div class="m-detail-row"><span class="m-detail-label">인원</span><span class="m-detail-value">' + esc(memberNames || '없음') + ' (' + memberCount + '명)</span></div>' +
|
|
'<div class="m-detail-actions">' +
|
|
'<button type="button" class="m-detail-btn" onclick="event.stopPropagation(); openDetailEditSheet(' + sid + ');">상세보기</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
});
|
|
});
|
|
|
|
if (currentTab === 'all') {
|
|
html += '<button type="button" class="m-load-more" onclick="loadMore()">이전 기록 더 보기</button>';
|
|
}
|
|
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
|
|
? '<span class="de-worker-status ok">입력완료</span>'
|
|
: '<span class="de-worker-status missing">미입력</span>';
|
|
|
|
// work_hours 표시
|
|
var workHoursTag = '';
|
|
if (m.work_hours !== null && m.work_hours !== undefined) {
|
|
workHoursTag = '<span class="m-work-hours-tag">' + parseFloat(m.work_hours) + 'h</span>';
|
|
}
|
|
// 분할 항목이면 프로젝트명 표시
|
|
var projectTag = '';
|
|
if (m.split_seq > 0 && m.project_name) {
|
|
projectTag = '<span style="font-size:0.625rem; background:#dbeafe; color:#1e40af; padding:0.0625rem 0.375rem; border-radius:0.25rem; margin-left:0.25rem;">' + esc(m.project_name) + '</span>';
|
|
} else if (m.project_name && m.project_id !== deSession.project_id) {
|
|
projectTag = '<span style="font-size:0.625rem; background:#fef3c7; color:#92400e; padding:0.0625rem 0.375rem; border-radius:0.25rem; margin-left:0.25rem;">' + esc(m.project_name) + '</span>';
|
|
}
|
|
|
|
var taskOptions = '<option value="">작업 선택...</option>';
|
|
deTasks.forEach(function(t) {
|
|
var sel = (m.task_id && m.task_id == t.task_id) ? ' selected' : '';
|
|
taskOptions += '<option value="' + t.task_id + '"' + sel + '>' + esc(t.task_name) + '</option>';
|
|
});
|
|
|
|
var currentCatId = m.workplace_category_id || '';
|
|
var catOptions = '<option value="">분류...</option>';
|
|
deWpCats.forEach(function(c) {
|
|
var sel = (currentCatId && currentCatId == c.category_id) ? ' selected' : '';
|
|
catOptions += '<option value="' + c.category_id + '"' + sel + '>' + esc(c.category_name) + '</option>';
|
|
});
|
|
|
|
var wpOptions = '<option value="">장소...</option>';
|
|
if (currentCatId && deWpMap[currentCatId]) {
|
|
deWpMap[currentCatId].forEach(function(wp) {
|
|
var sel = (m.workplace_id && m.workplace_id == wp.workplace_id) ? ' selected' : '';
|
|
wpOptions += '<option value="' + wp.workplace_id + '"' + sel + '>' + esc(wp.workplace_name) + '</option>';
|
|
});
|
|
}
|
|
|
|
html += '<div class="de-worker-card ' + cardClass + '" id="de_card_' + i + '">' +
|
|
'<div style="display:flex; align-items:center;">' +
|
|
'<input type="checkbox" class="de-worker-check" id="de_check_' + i + '" onchange="onWorkerCheck(' + i + ')">' +
|
|
'<span class="de-worker-name">' + esc(m.worker_name) + '</span> ' +
|
|
'<span class="de-worker-job">' + esc(m.job_type || '') + '</span>' +
|
|
workHoursTag +
|
|
projectTag +
|
|
statusHtml +
|
|
'<button type="button" class="de-split-btn" onclick="event.stopPropagation(); openSplitSheet(' + i + ')">분할</button>' +
|
|
'</div>' +
|
|
'<div class="de-worker-fields">' +
|
|
'<div class="de-field-row">' +
|
|
'<span class="de-field-label">작업</span>' +
|
|
'<select id="de_task_' + i + '" onchange="updateCardStatus(' + i + ')">' + taskOptions + '</select>' +
|
|
'</div>' +
|
|
'<div class="de-field-row">' +
|
|
'<span class="de-field-label">장소</span>' +
|
|
'<select id="de_wpcat_' + i + '" onchange="onDeWpCatChange(' + i + ')">' + catOptions + '</select>' +
|
|
'<select id="de_wp_' + i + '" onchange="updateCardStatus(' + i + ')">' + wpOptions + '</select>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
});
|
|
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 = '<option value="">장소...</option>';
|
|
if (catId && deWpMap[catId]) {
|
|
deWpMap[catId].forEach(function(wp) {
|
|
wpSel.innerHTML += '<option value="' + wp.workplace_id + '">' + esc(wp.workplace_name) + '</option>';
|
|
});
|
|
}
|
|
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 += '<div class="picker-item" onclick="pickTask(' + t.task_id + ')">' +
|
|
esc(t.task_name) +
|
|
'</div>';
|
|
});
|
|
if (deTasks.length === 0) {
|
|
html = '<div style="padding:1.5rem; text-align:center; color:#9ca3af;">등록된 작업이 없습니다</div>';
|
|
}
|
|
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 += '<div class="picker-item" onclick="pickWpCategory(' + c.category_id + ')">' +
|
|
esc(c.category_name) +
|
|
'<span class="picker-item-sub">' + count + '개 장소</span>' +
|
|
'</div>';
|
|
});
|
|
if (deWpCats.length === 0) {
|
|
html = '<div style="padding:1.5rem; text-align:center; color:#9ca3af;">등록된 분류가 없습니다</div>';
|
|
}
|
|
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 = '<div class="picker-item" style="color:#6b7280;" onclick="renderWorkplaceCatPicker()">← 분류 다시 선택</div>';
|
|
workplaces.forEach(function(wp) {
|
|
html += '<div class="picker-item" onclick="pickWorkplace(' + catId + ',' + wp.workplace_id + ')">' +
|
|
esc(wp.workplace_name) +
|
|
'</div>';
|
|
});
|
|
if (workplaces.length === 0) {
|
|
html += '<div style="padding:1rem; text-align:center; color:#9ca3af;">등록된 장소가 없습니다</div>';
|
|
}
|
|
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 = '<option value="">장소...</option>';
|
|
if (deWpMap[catId]) {
|
|
deWpMap[catId].forEach(function(wp) {
|
|
wpSel.innerHTML += '<option value="' + wp.workplace_id + '">' + esc(wp.workplace_name) + '</option>';
|
|
});
|
|
}
|
|
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 += '<div style="padding:0.625rem 0; border-bottom:1px solid #f3f4f6;">' +
|
|
'<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.375rem;">' +
|
|
'<div><strong style="font-size:0.875rem;">' + esc(m.worker_name) + '</strong> <span style="font-size:0.75rem; color:#6b7280;">(' + esc(m.job_type || '') + ')</span></div>' +
|
|
'</div>' +
|
|
'<div style="display:flex; gap:0.375rem; align-items:center; flex-wrap:wrap;">' +
|
|
'<select id="att_type_' + i + '" onchange="onAttTypeChange(' + i + ')" style="flex:1; min-width:120px; padding:0.5rem; border:1px solid #d1d5db; border-radius:0.375rem; font-size:0.8125rem; background:white;">' +
|
|
'<option value="regular">정시근로 (8h)</option>' +
|
|
'<option value="overtime">연장근무 (8h+)</option>' +
|
|
'<option value="annual">연차 (휴무)</option>' +
|
|
'<option value="half">반차 (4h)</option>' +
|
|
'<option value="quarter">반반차 (6h)</option>' +
|
|
'<option value="early">조퇴</option>' +
|
|
'</select>' +
|
|
'<input type="number" id="att_hours_' + i + '" placeholder="추가시간" step="0.5" min="0" max="8" style="display:none; width:70px; padding:0.5rem; border:1px solid #d1d5db; border-radius:0.375rem; font-size:0.8125rem; text-align:center;">' +
|
|
'<span id="att_hint_' + i + '" style="font-size:0.75rem; color:#6b7280;"></span>' +
|
|
'</div>' +
|
|
'</div>';
|
|
});
|
|
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 = '<option value="">원래 프로젝트 유지</option>';
|
|
(cachedProjects || []).forEach(function(p) {
|
|
html += '<option value="' + p.project_id + '"' + (p.project_id == currentProjectId ? ' selected' : '') + '>' + esc(p.project_name) + '</option>';
|
|
});
|
|
sel.innerHTML = html;
|
|
}
|
|
|
|
function populateWorkTypeSelect(selectId, currentWorkTypeId) {
|
|
var sel = document.getElementById(selectId);
|
|
var html = '<option value="">원래 공정 유지</option>';
|
|
(cachedWorkTypes || []).forEach(function(wt) {
|
|
html += '<option value="' + wt.id + '"' + (wt.id == currentWorkTypeId ? ' selected' : '') + '>' + esc(wt.name) + '</option>';
|
|
});
|
|
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 += '<div class="split-session-item" data-sid="' + s.session_id + '" onclick="selectSplitSession(' + s.session_id + ')">' +
|
|
esc(leaderName) + (workType ? ' - ' + esc(workType) : '') +
|
|
' <span style="font-size:0.6875rem; color:#9ca3af;">(' + (parseInt(s.team_member_count)||0) + '명)</span>' +
|
|
'</div>';
|
|
});
|
|
if (!html) html = '<div style="padding:0.75rem; font-size:0.8125rem; color:#9ca3af; text-align:center;">다른 TBM이 없습니다</div>';
|
|
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 = '<button type="button" class="pull-btn" disabled title="내 TBM이 없음">내 TBM 없음</button>';
|
|
} else {
|
|
btnHtml = '<button type="button" class="pull-btn" onclick="event.stopPropagation(); startPull(' + m.worker_id + ', \'' + esc(m.worker_name).replace(/'/g, "\\'") + '\', ' + hours + ')">빼오기</button>';
|
|
}
|
|
|
|
html += '<div class="pull-member-item">' +
|
|
'<div class="pull-member-info">' +
|
|
'<div class="pull-member-name">' + esc(m.worker_name) + ' <span class="m-work-hours-tag">' + hoursText + '</span></div>' +
|
|
'<div class="pull-member-sub">' + esc(m.job_type || '') + '</div>' +
|
|
'</div>' +
|
|
btnHtml +
|
|
'</div>';
|
|
});
|
|
|
|
if (pullMembers.length === 0) {
|
|
html = '<div style="padding:1.5rem; text-align:center; color:#9ca3af;">팀원이 없습니다</div>';
|
|
}
|
|
|
|
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 = '<option value="">반장 선택...</option>' +
|
|
leaders.map(function(w) {
|
|
return '<option value="' + w.worker_id + '">' + esc(w.worker_name) + ' (' + (w.job_type || '') + ')</option>';
|
|
}).join('');
|
|
|
|
// 인계할 팀원 체크리스트
|
|
var listEl = document.getElementById('handoverWorkerList');
|
|
if (team.length === 0) {
|
|
listEl.innerHTML = '<p style="padding:1rem; color:#6b7280; text-align:center;">팀원이 없습니다.</p>';
|
|
} else {
|
|
listEl.innerHTML = team.map(function(m) {
|
|
return '<label style="display:flex; align-items:center; gap:0.5rem; padding:0.5rem; cursor:pointer;">' +
|
|
'<input type="checkbox" class="handover-worker-cb" value="' + m.worker_id + '" checked style="width:16px; height:16px;">' +
|
|
'<span style="font-weight:500; font-size:0.875rem;">' + esc(m.worker_name) + '</span>' +
|
|
'<span style="font-size:0.75rem; color:#6b7280; margin-left:auto;">' + (m.job_type || '') + '</span>' +
|
|
'</label>';
|
|
}).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 = '인계 요청';
|
|
}
|
|
};
|
|
})();
|