- 공통 유틸리티 추출 (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>
1575 lines
62 KiB
JavaScript
1575 lines
62 KiB
JavaScript
/**
|
||
* Daily Work Report - Mobile UI Logic
|
||
* 모바일 전용 작업보고서 UI 로직
|
||
*
|
||
* 재사용 모듈: state.js, api.js, utils.js, api-base.js
|
||
*/
|
||
|
||
const MobileReport = (function() {
|
||
'use strict';
|
||
|
||
// ===== 내부 상태 =====
|
||
let currentTab = 'tbm';
|
||
let manualCounter = 0;
|
||
let manualCards = {}; // { id: { data } }
|
||
|
||
// 시간 선택 상태
|
||
let timePickerCallback = null;
|
||
let timePickerValue = 0;
|
||
|
||
// 부적합 시트 상태
|
||
let defectSheetIndex = null;
|
||
let defectSheetType = null; // 'tbm' | 'manual'
|
||
let defectSheetTempData = []; // 현재 편집중인 부적합 데이터
|
||
|
||
// 작업장소 시트 상태
|
||
let wpSheetManualId = null;
|
||
let wpSheetStep = 'category'; // 'category' | 'list'
|
||
let wpSelectedCategory = null;
|
||
let wpSelectedCategoryName = null;
|
||
let wpSelectedId = null;
|
||
let wpSelectedName = null;
|
||
|
||
// 수정 시트 상태
|
||
let editReportId = null;
|
||
|
||
// ===== 초기화 =====
|
||
async function init() {
|
||
console.log('[Mobile] 초기화 시작');
|
||
showMessage('데이터를 불러오는 중...', 'loading');
|
||
|
||
try {
|
||
const api = window.DailyWorkReportAPI;
|
||
const state = window.DailyWorkReportState;
|
||
|
||
await api.loadAllData();
|
||
|
||
// 부적합 카테고리/아이템 로드 확인
|
||
console.log('[Mobile] issueCategories:', (state.issueCategories || []).length, '개');
|
||
console.log('[Mobile] issueItems:', (state.issueItems || []).length, '개');
|
||
|
||
// TBM 데이터 로드
|
||
await api.loadIncompleteTbms();
|
||
await api.loadDailyIssuesForTbms();
|
||
|
||
console.log('[Mobile] incompleteTbms:', (state.incompleteTbms || []).length, '개');
|
||
console.log('[Mobile] dailyIssuesCache:', Object.keys(state.dailyIssuesCache || {}).length, '날짜');
|
||
|
||
hideMessage();
|
||
renderTbmCards();
|
||
|
||
// 완료 탭 날짜 초기화
|
||
const today = getKoreaToday();
|
||
const dateInput = document.getElementById('completedDate');
|
||
if (dateInput) dateInput.value = today;
|
||
|
||
console.log('[Mobile] 초기화 완료');
|
||
} catch (error) {
|
||
console.error('[Mobile] 초기화 오류:', error);
|
||
showMessage('데이터 로드 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
}
|
||
|
||
// ===== 유틸리티 (CommonUtils 위임) =====
|
||
var CU = window.CommonUtils;
|
||
function getKoreaToday() { return CU.getTodayKST(); }
|
||
function formatDateForApi(date) { return CU.formatDate(date) || null; }
|
||
|
||
function formatDate(dateString) {
|
||
if (!dateString) return '';
|
||
const d = new Date(dateString);
|
||
return `${d.getMonth() + 1}/${d.getDate()}`;
|
||
}
|
||
|
||
function getDayOfWeek(dateString) { return CU.getDayOfWeek(dateString); }
|
||
|
||
function formatHours(val) {
|
||
if (!val || val <= 0) return '선택';
|
||
if (val === Math.floor(val)) return val + '시간';
|
||
const hours = Math.floor(val);
|
||
const mins = Math.round((val - hours) * 60);
|
||
if (hours === 0) return mins + '분';
|
||
return hours + '시간 ' + mins + '분';
|
||
}
|
||
|
||
function esc(str) {
|
||
return window.escapeHtml ? window.escapeHtml(String(str || '')) : String(str || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||
}
|
||
|
||
// ===== 메시지 / 토스트 =====
|
||
function showMessage(msg, type) {
|
||
const el = document.getElementById('mMessage');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.className = 'm-message show ' + (type || 'info');
|
||
if (type === 'success') setTimeout(hideMessage, 3000);
|
||
}
|
||
|
||
function hideMessage() {
|
||
const el = document.getElementById('mMessage');
|
||
if (el) el.className = 'm-message';
|
||
}
|
||
|
||
function showToast(msg, type, duration) {
|
||
const el = document.getElementById('mToast');
|
||
if (!el) return;
|
||
el.textContent = msg;
|
||
el.className = 'm-toast show ' + (type || '');
|
||
clearTimeout(el._timer);
|
||
el._timer = setTimeout(() => { el.className = 'm-toast'; }, duration || 2500);
|
||
}
|
||
|
||
function showResult(type, title, message, details) {
|
||
const overlay = document.getElementById('mResultOverlay');
|
||
const icons = { success: '✅', error: '❌', warning: '⚠️' };
|
||
document.getElementById('mResultIcon').textContent = icons[type] || 'ℹ️';
|
||
document.getElementById('mResultTitle').textContent = title;
|
||
document.getElementById('mResultMessage').textContent = message;
|
||
|
||
const detailsEl = document.getElementById('mResultDetails');
|
||
if (details && Array.isArray(details) && details.length > 0) {
|
||
detailsEl.innerHTML = '<ul>' + details.map(d => '<li>' + esc(d) + '</li>').join('') + '</ul>';
|
||
detailsEl.style.display = 'block';
|
||
} else if (typeof details === 'string' && details) {
|
||
detailsEl.innerHTML = '<p>' + esc(details) + '</p>';
|
||
detailsEl.style.display = 'block';
|
||
} else {
|
||
detailsEl.style.display = 'none';
|
||
}
|
||
|
||
overlay.classList.add('show');
|
||
}
|
||
|
||
function closeResult() {
|
||
document.getElementById('mResultOverlay').classList.remove('show');
|
||
}
|
||
|
||
function showConfirm(message, onConfirm, isDanger) {
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'm-confirm-overlay';
|
||
overlay.innerHTML = `
|
||
<div class="m-confirm-box">
|
||
<div class="m-confirm-message">${esc(message)}</div>
|
||
<div class="m-confirm-actions">
|
||
<button class="m-confirm-cancel" id="mConfirmCancel">취소</button>
|
||
<button class="m-confirm-ok ${isDanger ? 'danger' : ''}" id="mConfirmOk">확인</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(overlay);
|
||
|
||
overlay.querySelector('#mConfirmCancel').onclick = () => { overlay.remove(); };
|
||
overlay.querySelector('#mConfirmOk').onclick = () => { overlay.remove(); onConfirm(); };
|
||
}
|
||
|
||
// ===== 탭 관리 =====
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
|
||
// 탭 버튼 상태
|
||
document.querySelectorAll('.m-tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||
});
|
||
|
||
// 탭 콘텐츠
|
||
document.querySelectorAll('.m-tab-content').forEach(el => {
|
||
el.classList.remove('active');
|
||
});
|
||
|
||
const tabMap = { tbm: 'tabTbm', manual: 'tabManual', completed: 'tabCompleted' };
|
||
const targetEl = document.getElementById(tabMap[tab]);
|
||
if (targetEl) targetEl.classList.add('active');
|
||
|
||
// 수동추가 버튼 표시/숨기기
|
||
const addBtn = document.getElementById('btnAddManual');
|
||
if (addBtn) addBtn.style.display = (tab === 'manual') ? 'block' : 'none';
|
||
|
||
// 완료 탭 진입 시 자동 조회
|
||
if (tab === 'completed') {
|
||
loadCompletedReports();
|
||
}
|
||
}
|
||
|
||
// ===== TBM 카드 렌더링 =====
|
||
function renderTbmCards() {
|
||
const state = window.DailyWorkReportState;
|
||
const container = document.getElementById('tbmCardList');
|
||
const tbms = state.incompleteTbms || [];
|
||
|
||
// 탭 카운트 업데이트
|
||
const countEl = document.getElementById('tbmCount');
|
||
if (countEl) countEl.textContent = tbms.length;
|
||
|
||
if (tbms.length === 0) {
|
||
container.innerHTML = '<div class="m-empty"><div class="m-empty-icon">✅</div><div>미완료 TBM 작업이 없습니다</div></div>';
|
||
return;
|
||
}
|
||
|
||
// 날짜별 그룹화
|
||
const byDate = {};
|
||
tbms.forEach((tbm, index) => {
|
||
const dateStr = formatDateForApi(tbm.session_date);
|
||
if (!byDate[dateStr]) {
|
||
byDate[dateStr] = { date: tbm.session_date, sessions: {} };
|
||
}
|
||
const sessionKey = `${tbm.session_id}_${dateStr}`;
|
||
if (!byDate[dateStr].sessions[sessionKey]) {
|
||
byDate[dateStr].sessions[sessionKey] = {
|
||
session_id: tbm.session_id,
|
||
created_by_name: tbm.created_by_name,
|
||
items: []
|
||
};
|
||
}
|
||
byDate[dateStr].sessions[sessionKey].items.push({ ...tbm, originalIndex: index });
|
||
});
|
||
|
||
const sortedDates = Object.keys(byDate).sort((a, b) => new Date(b) - new Date(a));
|
||
const today = getKoreaToday();
|
||
let html = '';
|
||
|
||
sortedDates.forEach((dateStr, dateIndex) => {
|
||
const dateData = byDate[dateStr];
|
||
const sessions = Object.values(dateData.sessions);
|
||
const totalWorkers = sessions.reduce((sum, s) => sum + s.items.length, 0);
|
||
const isToday = dateStr === today;
|
||
const isExpanded = isToday || dateIndex === 0;
|
||
const dayOfWeek = getDayOfWeek(dateStr);
|
||
|
||
// 당일 신고
|
||
const issues = state.dailyIssuesCache[dateStr] || [];
|
||
const nonconfCount = issues.filter(i => i.category_type === 'nonconformity').length;
|
||
|
||
html += `
|
||
<div class="m-date-group ${isExpanded ? 'expanded' : ''}" data-date="${dateStr}">
|
||
<div class="m-date-header" onclick="MobileReport.toggleDateGroup('${dateStr}')">
|
||
<div class="m-date-left">
|
||
<span class="m-date-toggle">▶</span>
|
||
<span class="m-date-title">${formatDate(dateData.date)} (${dayOfWeek})</span>
|
||
${isToday ? '<span class="m-today-badge">오늘</span>' : ''}
|
||
</div>
|
||
<div class="m-date-right">
|
||
${nonconfCount > 0 ? '<span class="m-issue-type-badge nonconformity">부적합 ' + nonconfCount + '</span>' : ''}
|
||
<span>세션${sessions.length} · ${totalWorkers}명</span>
|
||
</div>
|
||
</div>
|
||
<div class="m-date-content">
|
||
`;
|
||
|
||
// 이슈 리마인더
|
||
if (issues.length > 0) {
|
||
html += `
|
||
<div class="m-issue-reminder">
|
||
<div class="m-issue-reminder-header">
|
||
<span>⚠️</span>
|
||
<span>당일 신고 ${issues.length}건</span>
|
||
</div>
|
||
${issues.slice(0, 3).map(issue => {
|
||
const itemText = issue.issue_item_name || '';
|
||
const desc = issue.additional_description ? (itemText ? itemText + ' - ' + issue.additional_description : issue.additional_description) : itemText;
|
||
return `
|
||
<div class="m-issue-reminder-item">
|
||
<span class="m-issue-type-badge ${issue.category_type === 'safety' ? 'safety' : 'nonconformity'}">${issue.category_type === 'safety' ? '안전' : '부적합'}</span>
|
||
<span>${esc(issue.issue_category_name || '')} ${esc(desc || '-')}</span>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
${issues.length > 3 ? '<div style="font-size:0.6875rem;color:#92400e;padding-top:0.25rem;">외 ' + (issues.length - 3) + '건</div>' : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 세션별 카드
|
||
sessions.forEach(group => {
|
||
const sessionKey = `${group.session_id}_${dateStr}`;
|
||
|
||
html += `
|
||
<div class="m-session-header">
|
||
<span class="m-session-badge">TBM</span>
|
||
<span class="m-session-info">작성자: ${esc(group.created_by_name)} · ${group.items.length}명</span>
|
||
</div>
|
||
`;
|
||
|
||
group.items.forEach(tbm => {
|
||
const idx = tbm.originalIndex;
|
||
const defects = state.tempDefects[idx] || [];
|
||
const totalDefectHours = defects.reduce((s, d) => s + (parseFloat(d.defect_hours) || 0), 0);
|
||
const totalHoursVal = document.getElementById('m_totalHours_' + idx)?.value || '';
|
||
const hasRelatedIssue = issues.some(i => {
|
||
if (i.category_type !== 'nonconformity') return false;
|
||
if (tbm.workplace_id && i.workplace_id) return tbm.workplace_id === i.workplace_id;
|
||
return false;
|
||
});
|
||
|
||
html += renderWorkerCard(tbm, idx, totalHoursVal, defects, totalDefectHours, hasRelatedIssue);
|
||
});
|
||
|
||
// 일괄 제출 버튼
|
||
if (group.items.length > 1) {
|
||
html += `
|
||
<button class="m-batch-btn" onclick="MobileReport.batchSubmitSession('${sessionKey}')">
|
||
이 세션 일괄 제출 (${group.items.length}건)
|
||
</button>
|
||
`;
|
||
}
|
||
});
|
||
|
||
html += '</div></div>';
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function getDefaultHours(tbm) {
|
||
// work_hours가 있으면 (분할 배정) 해당 값 우선 사용
|
||
if (tbm.work_hours != null && parseFloat(tbm.work_hours) > 0) {
|
||
return parseFloat(tbm.work_hours);
|
||
}
|
||
switch (tbm.attendance_type) {
|
||
case 'overtime': return 8 + (parseFloat(tbm.attendance_hours) || 0);
|
||
case 'regular': return 8;
|
||
case 'half': return 4;
|
||
case 'quarter': return 6;
|
||
case 'early': return parseFloat(tbm.attendance_hours) || 0;
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
function getAttendanceLabel(type) {
|
||
const labels = { overtime: '연장근무', regular: '정시근로', annual: '연차', half: '반차', quarter: '반반차', early: '조퇴' };
|
||
return labels[type] || '';
|
||
}
|
||
|
||
function getAttendanceBadgeColor(type) {
|
||
const colors = { overtime: '#7c3aed', regular: '#2563eb', annual: '#ef4444', half: '#f59e0b', quarter: '#f97316', early: '#6b7280' };
|
||
return colors[type] || '#6b7280';
|
||
}
|
||
|
||
function renderWorkerCard(tbm, idx, totalHoursVal, defects, totalDefectHours, hasRelatedIssue) {
|
||
const state = window.DailyWorkReportState;
|
||
const defectCount = defects.filter(d => d.defect_hours > 0).length;
|
||
const defectText = defectCount > 0 ? totalDefectHours + 'h / ' + defectCount + '건' : '없음';
|
||
|
||
// 근태 기반 자동 시간 채움
|
||
const defaultHours = tbm.attendance_type ? getDefaultHours(tbm) : 0;
|
||
const effectiveHours = totalHoursVal || (defaultHours > 0 ? String(defaultHours) : '');
|
||
const timeDisplay = effectiveHours ? formatHours(parseFloat(effectiveHours)) : '선택';
|
||
const attendanceBadge = tbm.attendance_type ?
|
||
`<span style="display:inline-block; padding:0.125rem 0.375rem; border-radius:0.25rem; font-size:0.625rem; font-weight:700; color:white; background:${getAttendanceBadgeColor(tbm.attendance_type)}; margin-left:0.375rem;">${getAttendanceLabel(tbm.attendance_type)}</span>` : '';
|
||
|
||
return `
|
||
<div class="m-worker-card" data-index="${idx}" data-type="tbm">
|
||
<input type="hidden" id="m_totalHours_${idx}" value="${esc(effectiveHours)}">
|
||
<div class="m-card-top">
|
||
<div>
|
||
<div class="m-worker-name">${esc(tbm.worker_name || '작업자')}${attendanceBadge}</div>
|
||
<div class="m-worker-job">${esc(tbm.job_type || '-')}</div>
|
||
</div>
|
||
</div>
|
||
<div class="m-card-info">
|
||
<div class="m-info-row"><span class="m-info-label">프로젝트</span><span class="m-info-value">${esc(tbm.project_name || '-')}</span></div>
|
||
<div class="m-info-row"><span class="m-info-label">공정/작업</span><span class="m-info-value">${esc(tbm.work_type_name || '-')} / ${esc(tbm.task_name || '-')}</span></div>
|
||
<div class="m-info-row"><span class="m-info-label">작업장소</span><span class="m-info-value">${esc(tbm.category_name || '')} > ${esc(tbm.workplace_name || '-')}</span></div>
|
||
</div>
|
||
<div class="m-card-actions">
|
||
<div class="m-action-btn ${effectiveHours ? 'has-value' : ''}" onclick="MobileReport.openTimePicker(${idx}, 'tbm')">
|
||
<span class="m-action-label">작업시간</span>
|
||
<span class="m-action-value" id="m_timeDisplay_${idx}">${timeDisplay}</span>
|
||
</div>
|
||
<div class="m-action-btn ${defectCount > 0 ? 'has-value' : ''} ${hasRelatedIssue ? 'has-related-issue' : ''}" onclick="MobileReport.openDefectSheet(${idx}, 'tbm')">
|
||
<span class="m-action-label">부적합</span>
|
||
<span class="m-action-value" id="m_defectDisplay_${idx}">${defectText}</span>
|
||
</div>
|
||
</div>
|
||
<button class="m-submit-btn primary" onclick="MobileReport.submitTbmReport(${idx})">제출</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function toggleDateGroup(dateStr) {
|
||
const group = document.querySelector(`.m-date-group[data-date="${dateStr}"]`);
|
||
if (!group) return;
|
||
group.classList.toggle('expanded');
|
||
}
|
||
|
||
// ===== 시간 선택 =====
|
||
function openTimePicker(index, type) {
|
||
timePickerValue = 0;
|
||
const overlay = document.getElementById('mTimeOverlay');
|
||
|
||
if (type === 'tbm') {
|
||
const existing = document.getElementById('m_totalHours_' + index);
|
||
if (existing && existing.value) timePickerValue = parseFloat(existing.value) || 0;
|
||
document.getElementById('mTimeTitle').textContent = '작업시간 선택';
|
||
timePickerCallback = function(val) {
|
||
const inp = document.getElementById('m_totalHours_' + index);
|
||
if (inp) inp.value = val;
|
||
const disp = document.getElementById('m_timeDisplay_' + index);
|
||
if (disp) {
|
||
disp.textContent = formatHours(val);
|
||
disp.closest('.m-action-btn').classList.toggle('has-value', val > 0);
|
||
}
|
||
};
|
||
} else if (type === 'manual') {
|
||
const existing = document.getElementById('m_manual_hours_' + index);
|
||
if (existing && existing.value) timePickerValue = parseFloat(existing.value) || 0;
|
||
document.getElementById('mTimeTitle').textContent = '작업시간 선택';
|
||
timePickerCallback = function(val) {
|
||
const inp = document.getElementById('m_manual_hours_' + index);
|
||
if (inp) inp.value = val;
|
||
const disp = document.getElementById('m_manual_timeDisplay_' + index);
|
||
if (disp) {
|
||
disp.textContent = formatHours(val);
|
||
disp.closest('.m-action-btn').classList.toggle('has-value', val > 0);
|
||
}
|
||
};
|
||
} else if (type === 'defect') {
|
||
// 부적합 시트 내 시간 선택
|
||
timePickerValue = 0;
|
||
document.getElementById('mTimeTitle').textContent = '부적합 시간 선택';
|
||
timePickerCallback = function(val) {
|
||
// index is defect index within defectSheetTempData
|
||
if (defectSheetTempData[index]) {
|
||
defectSheetTempData[index].defect_hours = val;
|
||
const timeEl = document.getElementById('m_defect_time_' + index);
|
||
if (timeEl) timeEl.textContent = formatHours(val);
|
||
}
|
||
};
|
||
}
|
||
|
||
updateTimeDisplay();
|
||
highlightTimeButtons();
|
||
overlay.classList.add('show');
|
||
}
|
||
|
||
function setTime(val) {
|
||
timePickerValue = val;
|
||
updateTimeDisplay();
|
||
highlightTimeButtons();
|
||
}
|
||
|
||
function adjustTime(delta) {
|
||
timePickerValue = Math.max(0, Math.round((timePickerValue + delta) * 10) / 10);
|
||
updateTimeDisplay();
|
||
highlightTimeButtons();
|
||
}
|
||
|
||
function updateTimeDisplay() {
|
||
document.getElementById('mTimeCurrent').textContent = formatHours(timePickerValue) || '0시간';
|
||
}
|
||
|
||
function highlightTimeButtons() {
|
||
document.querySelectorAll('.m-time-btn').forEach(btn => {
|
||
btn.classList.remove('selected');
|
||
});
|
||
const quickValues = [0.5, 1, 2, 4, 8];
|
||
const btns = document.querySelectorAll('.m-quick-time-grid .m-time-btn');
|
||
quickValues.forEach((v, i) => {
|
||
if (btns[i] && timePickerValue === v) btns[i].classList.add('selected');
|
||
});
|
||
}
|
||
|
||
function confirmTime() {
|
||
if (timePickerCallback) {
|
||
timePickerCallback(timePickerValue);
|
||
}
|
||
closeTimePicker();
|
||
}
|
||
|
||
function closeTimePicker() {
|
||
document.getElementById('mTimeOverlay').classList.remove('show');
|
||
timePickerCallback = null;
|
||
}
|
||
|
||
// ===== 부적합 바텀시트 =====
|
||
function openDefectSheet(index, type) {
|
||
console.log('[Mobile] openDefectSheet:', index, type);
|
||
defectSheetIndex = index;
|
||
defectSheetType = type;
|
||
const state = window.DailyWorkReportState;
|
||
|
||
// 기존 부적합 데이터 복사
|
||
if (type === 'tbm') {
|
||
if (!state.tempDefects[index]) {
|
||
state.tempDefects[index] = [];
|
||
}
|
||
defectSheetTempData = JSON.parse(JSON.stringify(state.tempDefects[index]));
|
||
} else {
|
||
const mcData = manualCards[index];
|
||
if (mcData && mcData.defects) {
|
||
defectSheetTempData = JSON.parse(JSON.stringify(mcData.defects));
|
||
} else {
|
||
defectSheetTempData = [];
|
||
}
|
||
}
|
||
|
||
console.log('[Mobile] defectSheetTempData:', defectSheetTempData);
|
||
renderDefectSheetContent();
|
||
showBottomSheet('defect');
|
||
}
|
||
|
||
function renderDefectSheetContent() {
|
||
const state = window.DailyWorkReportState;
|
||
const body = document.getElementById('defectSheetBody');
|
||
console.log('[Mobile] renderDefectSheetContent, body:', !!body);
|
||
console.log('[Mobile] issueCategories:', (state.issueCategories || []).length);
|
||
console.log('[Mobile] issueItems:', (state.issueItems || []).length);
|
||
|
||
// 당일 신고된 이슈 가져오기
|
||
let relatedIssues = [];
|
||
if (defectSheetType === 'tbm') {
|
||
const tbm = state.incompleteTbms[defectSheetIndex];
|
||
console.log('[Mobile] tbm:', tbm?.worker_name, 'session_date:', tbm?.session_date, 'workplace_id:', tbm?.workplace_id);
|
||
if (tbm) {
|
||
const dateStr = formatDateForApi(tbm.session_date);
|
||
const allIssues = state.dailyIssuesCache[dateStr] || [];
|
||
console.log('[Mobile] allIssues for', dateStr, ':', allIssues.length);
|
||
relatedIssues = allIssues.filter(i => {
|
||
if (i.category_type !== 'nonconformity') return false;
|
||
if (tbm.workplace_id && i.workplace_id) return tbm.workplace_id === i.workplace_id;
|
||
if (tbm.workplace_name && (i.workplace_name || i.custom_location)) {
|
||
const loc = i.workplace_name || i.custom_location || '';
|
||
return loc.includes(tbm.workplace_name) || tbm.workplace_name.includes(loc);
|
||
}
|
||
return false;
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log('[Mobile] relatedIssues:', relatedIssues.length);
|
||
|
||
let html = '';
|
||
|
||
// 신고 기반 이슈 체크
|
||
if (relatedIssues.length > 0) {
|
||
html += '<div style="font-size:0.8125rem;font-weight:600;color:#374151;margin-bottom:0.5rem;">📋 관련 신고</div>';
|
||
relatedIssues.forEach(issue => {
|
||
const existingIdx = defectSheetTempData.findIndex(d => d.issue_report_id == issue.report_id);
|
||
const isSelected = existingIdx >= 0;
|
||
const defectHours = isSelected ? defectSheetTempData[existingIdx].defect_hours : 0;
|
||
const itemText = issue.issue_item_name || '';
|
||
const desc = issue.additional_description ? (itemText ? itemText + ' - ' + issue.additional_description : issue.additional_description) : itemText;
|
||
|
||
html += `
|
||
<div class="m-defect-issue-item ${isSelected ? 'selected' : ''}" data-report-id="${issue.report_id}" onclick="MobileReport.toggleDefectIssue(${issue.report_id})">
|
||
<div class="m-defect-checkbox"></div>
|
||
<div class="m-defect-issue-info">
|
||
<div class="m-defect-issue-name">${esc(issue.issue_category_name || '부적합')}</div>
|
||
<div class="m-defect-issue-detail">${esc(desc || '-')} · ${esc(issue.workplace_name || issue.custom_location || '')}</div>
|
||
${isSelected ? `
|
||
<div class="m-defect-time-input" onclick="event.stopPropagation()">
|
||
<label>시간:</label>
|
||
<span id="m_defect_issue_time_${issue.report_id}" style="font-weight:600;color:#2563eb;cursor:pointer;" onclick="MobileReport.openDefectTimePicker(${issue.report_id}, 'issue')">${formatHours(defectHours) || '선택'}</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// 수동 부적합 추가 섹션
|
||
html += '<div class="m-defect-manual-section">';
|
||
html += '<div class="m-defect-manual-title">수동 부적합 추가</div>';
|
||
|
||
// 기존 수동 부적합 항목 렌더링
|
||
const manualDefects = defectSheetTempData.filter(d => !d.issue_report_id);
|
||
manualDefects.forEach((defect, i) => {
|
||
const realIdx = defectSheetTempData.indexOf(defect);
|
||
html += renderManualDefectRow(defect, realIdx);
|
||
});
|
||
|
||
html += `
|
||
<button class="m-btn-add" style="width:100%;margin-top:0.5rem;background:#f3f4f6;color:#374151;border:1.5px dashed #d1d5db;" onclick="MobileReport.addManualDefect()">+ 부적합 추가</button>
|
||
</div>`;
|
||
|
||
body.innerHTML = html;
|
||
console.log('[Mobile] defectSheet rendered, html length:', html.length);
|
||
}
|
||
|
||
function renderManualDefectRow(defect, idx) {
|
||
const state = window.DailyWorkReportState;
|
||
const categories = state.issueCategories || [];
|
||
const items = state.issueItems || [];
|
||
const filteredItems = defect.category_id ? items.filter(it => it.category_id == defect.category_id) : [];
|
||
|
||
return `
|
||
<div style="border:1.5px solid #e5e7eb;border-radius:10px;padding:0.625rem;margin-bottom:0.5rem;position:relative;">
|
||
<button style="position:absolute;top:0.25rem;right:0.5rem;background:none;border:none;color:#9ca3af;font-size:1.25rem;cursor:pointer;" onclick="MobileReport.removeManualDefect(${idx})">×</button>
|
||
<div class="m-form-group" style="margin-bottom:0.5rem;">
|
||
<label class="m-form-label">카테고리</label>
|
||
<select class="m-form-select" onchange="MobileReport.updateDefectCategory(${idx}, this.value)" style="font-size:16px;">
|
||
<option value="">선택</option>
|
||
${categories.map(c => `<option value="${c.category_id}" ${defect.category_id == c.category_id ? 'selected' : ''}>${esc(c.category_name)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="m-form-group" style="margin-bottom:0.5rem;">
|
||
<label class="m-form-label">항목</label>
|
||
<select class="m-form-select" id="m_defectItem_${idx}" onchange="MobileReport.updateDefectItem(${idx}, this.value)" style="font-size:16px;">
|
||
<option value="">선택</option>
|
||
${filteredItems.map(it => `<option value="${it.item_id}" ${defect.item_id == it.item_id ? 'selected' : ''}>${esc(it.item_name)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||
<span style="font-size:0.75rem;color:#6b7280;">시간:</span>
|
||
<span id="m_defect_time_${idx}" style="font-weight:600;color:#2563eb;cursor:pointer;font-size:0.875rem;" onclick="MobileReport.openDefectTimePicker(${idx}, 'manual')">${formatHours(defect.defect_hours) || '선택'}</span>
|
||
</div>
|
||
<div class="m-form-group" style="margin-top:0.5rem;margin-bottom:0;">
|
||
<input class="m-form-input" placeholder="비고 (선택)" value="${esc(defect.note || '')}" onchange="MobileReport.updateDefectNote(${idx}, this.value)" style="font-size:16px;">
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function toggleDefectIssue(reportId) {
|
||
const existingIdx = defectSheetTempData.findIndex(d => d.issue_report_id == reportId);
|
||
|
||
if (existingIdx >= 0) {
|
||
// 이미 선택됨 → 해제
|
||
defectSheetTempData.splice(existingIdx, 1);
|
||
} else {
|
||
// 선택 → 추가 (데스크탑과 동일한 구조: 이슈 기반이므로 error_type_id는 null)
|
||
defectSheetTempData.push({
|
||
issue_report_id: reportId,
|
||
error_type_id: null,
|
||
defect_hours: 0,
|
||
note: ''
|
||
});
|
||
}
|
||
renderDefectSheetContent();
|
||
}
|
||
|
||
function addManualDefect() {
|
||
defectSheetTempData.push({
|
||
issue_report_id: null,
|
||
category_id: null,
|
||
item_id: null,
|
||
error_type_id: '', // 레거시 호환 (데스크탑과 동일)
|
||
defect_hours: 0,
|
||
note: ''
|
||
});
|
||
renderDefectSheetContent();
|
||
}
|
||
|
||
function removeManualDefect(idx) {
|
||
defectSheetTempData.splice(idx, 1);
|
||
renderDefectSheetContent();
|
||
}
|
||
|
||
function updateDefectCategory(idx, categoryId) {
|
||
if (defectSheetTempData[idx]) {
|
||
defectSheetTempData[idx].category_id = categoryId ? parseInt(categoryId) : null;
|
||
defectSheetTempData[idx].item_id = null;
|
||
defectSheetTempData[idx].error_type_id = null;
|
||
// 항목 드롭다운 재렌더링
|
||
renderDefectSheetContent();
|
||
}
|
||
}
|
||
|
||
function updateDefectItem(idx, itemId) {
|
||
if (defectSheetTempData[idx]) {
|
||
defectSheetTempData[idx].item_id = itemId ? parseInt(itemId) : null;
|
||
defectSheetTempData[idx].error_type_id = itemId ? parseInt(itemId) : null;
|
||
}
|
||
}
|
||
|
||
function updateDefectNote(idx, note) {
|
||
if (defectSheetTempData[idx]) {
|
||
defectSheetTempData[idx].note = note;
|
||
}
|
||
}
|
||
|
||
function openDefectTimePicker(id, type) {
|
||
if (type === 'issue') {
|
||
// issue_report_id 기반
|
||
const idx = defectSheetTempData.findIndex(d => d.issue_report_id == id);
|
||
if (idx < 0) return;
|
||
timePickerValue = defectSheetTempData[idx].defect_hours || 0;
|
||
timePickerCallback = function(val) {
|
||
defectSheetTempData[idx].defect_hours = val;
|
||
const el = document.getElementById('m_defect_issue_time_' + id);
|
||
if (el) el.textContent = formatHours(val);
|
||
};
|
||
} else {
|
||
// manual index
|
||
timePickerValue = defectSheetTempData[id]?.defect_hours || 0;
|
||
timePickerCallback = function(val) {
|
||
if (defectSheetTempData[id]) {
|
||
defectSheetTempData[id].defect_hours = val;
|
||
const el = document.getElementById('m_defect_time_' + id);
|
||
if (el) el.textContent = formatHours(val);
|
||
}
|
||
};
|
||
}
|
||
updateTimeDisplay();
|
||
highlightTimeButtons();
|
||
document.getElementById('mTimeTitle').textContent = '부적합 시간 선택';
|
||
document.getElementById('mTimeOverlay').classList.add('show');
|
||
}
|
||
|
||
function saveDefects() {
|
||
const state = window.DailyWorkReportState;
|
||
|
||
// 수동 부적합 유효성 검사: category_id 또는 item_id가 있어야 저장
|
||
const manualDefects = defectSheetTempData.filter(d => !d.issue_report_id);
|
||
const invalidManual = manualDefects.filter(d => d.defect_hours > 0 && !d.category_id && !d.item_id);
|
||
if (invalidManual.length > 0) {
|
||
showToast('부적합 카테고리/항목을 선택해주세요', 'error');
|
||
return;
|
||
}
|
||
|
||
// _saved 플래그 설정 (데스크탑 호환)
|
||
defectSheetTempData.forEach(d => { d._saved = true; });
|
||
|
||
if (defectSheetType === 'tbm') {
|
||
state.tempDefects[defectSheetIndex] = defectSheetTempData;
|
||
// 카드 UI 업데이트
|
||
const defects = defectSheetTempData;
|
||
const totalDefectHours = defects.reduce((s, d) => s + (parseFloat(d.defect_hours) || 0), 0);
|
||
const defectCount = defects.filter(d => d.defect_hours > 0).length;
|
||
const defectText = defectCount > 0 ? totalDefectHours + 'h / ' + defectCount + '건' : '없음';
|
||
const disp = document.getElementById('m_defectDisplay_' + defectSheetIndex);
|
||
if (disp) {
|
||
disp.textContent = defectText;
|
||
disp.closest('.m-action-btn').classList.toggle('has-value', defectCount > 0);
|
||
}
|
||
} else if (defectSheetType === 'manual') {
|
||
if (manualCards[defectSheetIndex]) {
|
||
manualCards[defectSheetIndex].defects = defectSheetTempData;
|
||
// UI 업데이트
|
||
const defects = defectSheetTempData;
|
||
const totalDefectHours = defects.reduce((s, d) => s + (parseFloat(d.defect_hours) || 0), 0);
|
||
const defectCount = defects.filter(d => d.defect_hours > 0).length;
|
||
const defectText = defectCount > 0 ? totalDefectHours + 'h / ' + defectCount + '건' : '없음';
|
||
const disp = document.getElementById('m_manual_defectDisplay_' + defectSheetIndex);
|
||
if (disp) {
|
||
disp.textContent = defectText;
|
||
disp.closest('.m-action-btn').classList.toggle('has-value', defectCount > 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
hideDefectSheet();
|
||
showToast('부적합 정보가 저장되었습니다', 'success');
|
||
}
|
||
|
||
function hideDefectSheet() {
|
||
hideBottomSheet('defect');
|
||
defectSheetIndex = null;
|
||
defectSheetType = null;
|
||
}
|
||
|
||
// ===== 제출 =====
|
||
async function submitTbmReport(index) {
|
||
const state = window.DailyWorkReportState;
|
||
const tbm = state.incompleteTbms[index];
|
||
if (!tbm) return;
|
||
|
||
const totalHours = parseFloat(document.getElementById('m_totalHours_' + index)?.value);
|
||
const defects = state.tempDefects[index] || [];
|
||
const errorHours = defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0);
|
||
const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null;
|
||
|
||
// 검증
|
||
if (!totalHours || totalHours <= 0) {
|
||
showToast('작업시간을 선택해주세요', 'error');
|
||
return;
|
||
}
|
||
if (errorHours > totalHours) {
|
||
showToast('부적합 시간이 총 작업시간을 초과합니다', 'error');
|
||
return;
|
||
}
|
||
|
||
const invalidDefects = defects.filter(d => d.defect_hours > 0 && !d.error_type_id && !d.issue_report_id && !d.category_id && !d.item_id);
|
||
if (invalidDefects.length > 0) {
|
||
showToast('부적합 원인을 선택해주세요', 'error');
|
||
return;
|
||
}
|
||
|
||
const reportDate = typeof tbm.session_date === 'string' && tbm.session_date.includes('T')
|
||
? tbm.session_date.split('T')[0]
|
||
: (tbm.session_date instanceof Date ? formatDateForApi(tbm.session_date) : tbm.session_date);
|
||
|
||
const reportData = {
|
||
tbm_assignment_id: tbm.assignment_id,
|
||
tbm_session_id: tbm.session_id,
|
||
worker_id: tbm.worker_id,
|
||
project_id: tbm.project_id,
|
||
work_type_id: tbm.task_id,
|
||
report_date: reportDate,
|
||
start_time: null,
|
||
end_time: null,
|
||
total_hours: totalHours,
|
||
error_hours: errorHours,
|
||
error_type_id: errorTypeId,
|
||
work_status_id: errorHours > 0 ? 2 : 1
|
||
};
|
||
|
||
// 제출 버튼 비활성화
|
||
const card = document.querySelector(`.m-worker-card[data-index="${index}"]`);
|
||
const btn = card?.querySelector('.m-submit-btn');
|
||
if (btn) { btn.disabled = true; btn.textContent = '제출 중...'; }
|
||
|
||
try {
|
||
const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', reportData);
|
||
if (!response.success) throw new Error(response.message || '제출 실패');
|
||
|
||
// 부적합 원인 저장
|
||
if (defects.length > 0 && response.data?.report_id) {
|
||
const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0);
|
||
if (validDefects.length > 0) {
|
||
const defectsToSend = validDefects.map(d => ({
|
||
issue_report_id: d.issue_report_id || null,
|
||
category_id: d.category_id || null,
|
||
item_id: d.item_id || null,
|
||
error_type_id: d.error_type_id || null,
|
||
defect_hours: d.defect_hours,
|
||
note: d.note || ''
|
||
}));
|
||
await window.apiCall(`/daily-work-reports/${response.data.report_id}/defects`, 'PUT', { defects: defectsToSend });
|
||
}
|
||
}
|
||
|
||
// 임시 데이터 삭제
|
||
delete state.tempDefects[index];
|
||
|
||
showResult('success', '제출 완료', `${tbm.worker_name}의 작업보고서가 제출되었습니다.`,
|
||
response.data.tbm_completed ? '모든 팀원의 작업보고서가 제출되어 TBM이 완료되었습니다.' : response.data.completion_status
|
||
);
|
||
|
||
// 목록 새로고침
|
||
await refreshTbmList();
|
||
} catch (error) {
|
||
console.error('[Mobile] TBM 제출 오류:', error);
|
||
showResult('error', '제출 실패', error.message);
|
||
if (btn) { btn.disabled = false; btn.textContent = '제출'; }
|
||
}
|
||
}
|
||
|
||
async function batchSubmitSession(sessionKey) {
|
||
const state = window.DailyWorkReportState;
|
||
const cards = document.querySelectorAll(`.m-worker-card[data-type="tbm"]`);
|
||
|
||
// 세션에 해당하는 카드 찾기
|
||
const tbms = state.incompleteTbms || [];
|
||
const byDate = {};
|
||
tbms.forEach((tbm, index) => {
|
||
const dateStr = formatDateForApi(tbm.session_date);
|
||
const key = `${tbm.session_id}_${dateStr}`;
|
||
if (!byDate[key]) byDate[key] = [];
|
||
byDate[key].push({ tbm, index });
|
||
});
|
||
|
||
const sessionItems = byDate[sessionKey];
|
||
if (!sessionItems || sessionItems.length === 0) {
|
||
showToast('제출할 항목이 없습니다', 'error');
|
||
return;
|
||
}
|
||
|
||
// 검증
|
||
const errors = [];
|
||
const itemsToSubmit = [];
|
||
|
||
sessionItems.forEach(({ tbm, index }) => {
|
||
const totalHours = parseFloat(document.getElementById('m_totalHours_' + index)?.value);
|
||
const defects = state.tempDefects[index] || [];
|
||
const errorHours = defects.reduce((s, d) => s + (parseFloat(d.defect_hours) || 0), 0);
|
||
const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null;
|
||
|
||
if (!totalHours || totalHours <= 0) {
|
||
errors.push(`${tbm.worker_name}: 작업시간 미입력`);
|
||
return;
|
||
}
|
||
if (errorHours > totalHours) {
|
||
errors.push(`${tbm.worker_name}: 부적합 시간 초과`);
|
||
return;
|
||
}
|
||
|
||
const reportDate = typeof tbm.session_date === 'string' && tbm.session_date.includes('T')
|
||
? tbm.session_date.split('T')[0]
|
||
: (tbm.session_date instanceof Date ? formatDateForApi(tbm.session_date) : tbm.session_date);
|
||
|
||
itemsToSubmit.push({
|
||
index,
|
||
tbm,
|
||
defects,
|
||
data: {
|
||
tbm_assignment_id: tbm.assignment_id,
|
||
tbm_session_id: tbm.session_id,
|
||
worker_id: tbm.worker_id,
|
||
project_id: tbm.project_id,
|
||
work_type_id: tbm.task_id,
|
||
report_date: reportDate,
|
||
start_time: null,
|
||
end_time: null,
|
||
total_hours: totalHours,
|
||
error_hours: errorHours,
|
||
error_type_id: errorTypeId,
|
||
work_status_id: errorHours > 0 ? 2 : 1
|
||
}
|
||
});
|
||
});
|
||
|
||
if (errors.length > 0) {
|
||
showResult('error', '일괄제출 검증 실패', '모든 항목이 유효해야 제출할 수 있습니다.', errors);
|
||
return;
|
||
}
|
||
|
||
showMessage('일괄 제출 중...', 'loading');
|
||
const results = { success: [], failed: [] };
|
||
|
||
for (const item of itemsToSubmit) {
|
||
try {
|
||
const response = await window.apiCall('/daily-work-reports/from-tbm', 'POST', item.data);
|
||
if (response.success) {
|
||
// 부적합 저장
|
||
if (item.defects.length > 0 && response.data?.report_id) {
|
||
const validDefects = item.defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0);
|
||
if (validDefects.length > 0) {
|
||
const defectsToSend = validDefects.map(d => ({
|
||
issue_report_id: d.issue_report_id || null,
|
||
category_id: d.category_id || null,
|
||
item_id: d.item_id || null,
|
||
error_type_id: d.error_type_id || null,
|
||
defect_hours: d.defect_hours,
|
||
note: d.note || ''
|
||
}));
|
||
await window.apiCall(`/daily-work-reports/${response.data.report_id}/defects`, 'PUT', { defects: defectsToSend });
|
||
}
|
||
}
|
||
delete state.tempDefects[item.index];
|
||
results.success.push(item.tbm.worker_name);
|
||
} else {
|
||
results.failed.push(`${item.tbm.worker_name}: ${response.message}`);
|
||
}
|
||
} catch (error) {
|
||
results.failed.push(`${item.tbm.worker_name}: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
hideMessage();
|
||
|
||
if (results.failed.length === 0) {
|
||
showResult('success', '일괄제출 완료', `${itemsToSubmit.length}건 모두 성공`, results.success.map(n => '✓ ' + n));
|
||
} else if (results.success.length === 0) {
|
||
showResult('error', '일괄제출 실패', `${itemsToSubmit.length}건 모두 실패`, results.failed);
|
||
} else {
|
||
showResult('warning', '일괄제출 부분 완료', `성공 ${results.success.length}건 / 실패 ${results.failed.length}건`,
|
||
[...results.success.map(n => '✓ ' + n), ...results.failed.map(m => '✗ ' + m)]
|
||
);
|
||
}
|
||
|
||
await refreshTbmList();
|
||
}
|
||
|
||
async function refreshTbmList() {
|
||
const api = window.DailyWorkReportAPI;
|
||
await api.loadIncompleteTbms();
|
||
await api.loadDailyIssuesForTbms();
|
||
renderTbmCards();
|
||
}
|
||
|
||
// ===== 수동 입력 =====
|
||
function addManualCard() {
|
||
const id = 'mc_' + (manualCounter++);
|
||
const state = window.DailyWorkReportState;
|
||
|
||
manualCards[id] = {
|
||
worker_id: null,
|
||
report_date: getKoreaToday(),
|
||
project_id: null,
|
||
work_type_id: null,
|
||
task_id: null,
|
||
workplace_category_id: null,
|
||
workplace_id: null,
|
||
workplace_name: null,
|
||
workplace_category_name: null,
|
||
total_hours: 0,
|
||
defects: []
|
||
};
|
||
|
||
const container = document.getElementById('manualCardList');
|
||
// 빈 상태 메시지 제거
|
||
const empty = container.querySelector('.m-empty');
|
||
if (empty) empty.remove();
|
||
|
||
const cardEl = document.createElement('div');
|
||
cardEl.className = 'm-manual-card';
|
||
cardEl.id = 'm_manual_card_' + id;
|
||
cardEl.setAttribute('data-manual-id', id);
|
||
cardEl.style.position = 'relative';
|
||
|
||
const workers = state.workers || [];
|
||
const projects = state.projects || [];
|
||
const workTypes = state.workTypes || [];
|
||
|
||
cardEl.innerHTML = `
|
||
<button class="m-manual-delete" onclick="MobileReport.removeManualCard('${id}')">×</button>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">작업자</label>
|
||
<select class="m-form-select" id="m_manual_worker_${id}" onchange="MobileReport.updateManualField('${id}','worker_id',this.value)">
|
||
<option value="">작업자 선택</option>
|
||
${workers.map(w => `<option value="${w.worker_id}">${esc(w.worker_name)} (${esc(w.job_type || '-')})</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="m-form-row">
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">날짜</label>
|
||
<input type="date" class="m-form-input" id="m_manual_date_${id}" value="${getKoreaToday()}" onchange="MobileReport.updateManualField('${id}','report_date',this.value)">
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">프로젝트</label>
|
||
<select class="m-form-select" id="m_manual_project_${id}" onchange="MobileReport.updateManualField('${id}','project_id',this.value)">
|
||
<option value="">선택</option>
|
||
${projects.map(p => `<option value="${p.project_id}">${esc(p.project_name)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="m-form-row">
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">공정</label>
|
||
<select class="m-form-select" id="m_manual_workType_${id}" onchange="MobileReport.onWorkTypeChange('${id}', this.value)">
|
||
<option value="">선택</option>
|
||
${workTypes.map(wt => `<option value="${wt.id}">${esc(wt.name)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">작업</label>
|
||
<select class="m-form-select" id="m_manual_task_${id}" disabled>
|
||
<option value="">공정 먼저 선택</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">작업장소</label>
|
||
<div class="m-workplace-box" id="m_manual_wpBox_${id}" onclick="MobileReport.openWorkplaceSheet('${id}')">
|
||
<span class="m-wp-icon">📍</span>
|
||
<span class="m-wp-text" id="m_manual_wpText_${id}">클릭하여 선택</span>
|
||
</div>
|
||
</div>
|
||
<div class="m-card-actions">
|
||
<div class="m-action-btn" onclick="MobileReport.openTimePicker('${id}', 'manual')">
|
||
<span class="m-action-label">작업시간</span>
|
||
<span class="m-action-value" id="m_manual_timeDisplay_${id}">선택</span>
|
||
<input type="hidden" id="m_manual_hours_${id}" value="">
|
||
</div>
|
||
<div class="m-action-btn" onclick="MobileReport.openDefectSheet('${id}', 'manual')">
|
||
<span class="m-action-label">부적합</span>
|
||
<span class="m-action-value" id="m_manual_defectDisplay_${id}">없음</span>
|
||
</div>
|
||
</div>
|
||
<button class="m-submit-btn primary" onclick="MobileReport.submitManualReport('${id}')">제출</button>
|
||
`;
|
||
|
||
container.appendChild(cardEl);
|
||
|
||
// 수동 입력 탭으로 이동
|
||
if (currentTab !== 'manual') {
|
||
switchTab('manual');
|
||
}
|
||
}
|
||
|
||
function removeManualCard(id) {
|
||
const card = document.getElementById('m_manual_card_' + id);
|
||
if (card) card.remove();
|
||
delete manualCards[id];
|
||
|
||
// 카드가 모두 없으면 빈 상태 표시
|
||
if (Object.keys(manualCards).length === 0) {
|
||
document.getElementById('manualCardList').innerHTML = `
|
||
<div class="m-empty">
|
||
<div class="m-empty-icon">📝</div>
|
||
<div>수동으로 작업보고서를 추가하세요</div>
|
||
<button class="m-btn-add" style="margin-top:0.75rem;" onclick="MobileReport.addManualCard()">+ 수동추가</button>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function updateManualField(id, field, value) {
|
||
if (manualCards[id]) {
|
||
manualCards[id][field] = value;
|
||
}
|
||
}
|
||
|
||
async function onWorkTypeChange(id, workTypeId) {
|
||
updateManualField(id, 'work_type_id', workTypeId);
|
||
updateManualField(id, 'task_id', null);
|
||
|
||
const taskSelect = document.getElementById('m_manual_task_' + id);
|
||
if (!workTypeId) {
|
||
taskSelect.disabled = true;
|
||
taskSelect.innerHTML = '<option value="">공정 먼저 선택</option>';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await window.apiCall(`/tasks?work_type_id=${workTypeId}`);
|
||
const tasks = response.success ? response.data : (Array.isArray(response) ? response : []);
|
||
|
||
if (tasks && tasks.length > 0) {
|
||
taskSelect.disabled = false;
|
||
taskSelect.innerHTML = '<option value="">선택</option>' +
|
||
tasks.map(t => `<option value="${t.task_id}">${esc(t.task_name)}</option>`).join('');
|
||
taskSelect.onchange = function() {
|
||
updateManualField(id, 'task_id', this.value);
|
||
};
|
||
} else {
|
||
taskSelect.disabled = true;
|
||
taskSelect.innerHTML = '<option value="">등록된 작업 없음</option>';
|
||
}
|
||
} catch (error) {
|
||
taskSelect.disabled = true;
|
||
taskSelect.innerHTML = '<option value="">로드 실패</option>';
|
||
}
|
||
}
|
||
|
||
async function submitManualReport(id) {
|
||
const mc = manualCards[id];
|
||
if (!mc) return;
|
||
|
||
const workerId = mc.worker_id || document.getElementById('m_manual_worker_' + id)?.value;
|
||
const reportDate = mc.report_date || document.getElementById('m_manual_date_' + id)?.value;
|
||
const projectId = mc.project_id || document.getElementById('m_manual_project_' + id)?.value;
|
||
const taskId = mc.task_id || document.getElementById('m_manual_task_' + id)?.value;
|
||
const totalHours = parseFloat(document.getElementById('m_manual_hours_' + id)?.value);
|
||
const workplaceId = mc.workplace_id;
|
||
const defects = mc.defects || [];
|
||
const errorHours = defects.reduce((s, d) => s + (parseFloat(d.defect_hours) || 0), 0);
|
||
const errorTypeId = defects.length > 0 ? (defects[0].error_type_id || defects[0].item_id || null) : null;
|
||
|
||
// 검증
|
||
if (!workerId) { showToast('작업자를 선택해주세요', 'error'); return; }
|
||
if (!reportDate) { showToast('날짜를 입력해주세요', 'error'); return; }
|
||
if (!projectId) { showToast('프로젝트를 선택해주세요', 'error'); return; }
|
||
if (!taskId) { showToast('작업을 선택해주세요', 'error'); return; }
|
||
if (workplaceId === null || workplaceId === undefined) { showToast('작업장소를 선택해주세요', 'error'); return; }
|
||
if (!totalHours || totalHours <= 0) { showToast('작업시간을 선택해주세요', 'error'); return; }
|
||
if (errorHours > totalHours) { showToast('부적합 시간이 초과합니다', 'error'); return; }
|
||
|
||
const reportData = {
|
||
report_date: reportDate,
|
||
worker_id: parseInt(workerId),
|
||
work_entries: [{
|
||
project_id: parseInt(projectId),
|
||
task_id: parseInt(taskId),
|
||
work_hours: totalHours,
|
||
work_status_id: errorHours > 0 ? 2 : 1,
|
||
error_type_id: errorTypeId ? parseInt(errorTypeId) : null
|
||
}]
|
||
};
|
||
|
||
const btn = document.querySelector(`#m_manual_card_${id} .m-submit-btn`);
|
||
if (btn) { btn.disabled = true; btn.textContent = '제출 중...'; }
|
||
|
||
try {
|
||
let response;
|
||
let retries = 3;
|
||
for (let i = 0; i < retries; i++) {
|
||
try {
|
||
response = await window.apiCall('/daily-work-reports', 'POST', reportData);
|
||
break;
|
||
} catch (err) {
|
||
if ((err.message?.includes('429') || err.message?.includes('너무 많은 요청')) && i < retries - 1) {
|
||
await new Promise(r => setTimeout(r, (i + 1) * 2000));
|
||
continue;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
if (!response.success) throw new Error(response.message || '제출 실패');
|
||
|
||
// 부적합 저장
|
||
const reportId = response.data?.inserted_ids?.[0] || response.data?.workReport_ids?.[0];
|
||
if (defects.length > 0 && reportId) {
|
||
const validDefects = defects.filter(d => (d.issue_report_id || d.category_id || d.item_id || d.error_type_id) && d.defect_hours > 0);
|
||
if (validDefects.length > 0) {
|
||
await window.apiCall(`/daily-work-reports/${reportId}/defects`, 'PUT', { defects: validDefects });
|
||
}
|
||
}
|
||
|
||
removeManualCard(id);
|
||
showToast('작업보고서가 제출되었습니다', 'success');
|
||
} catch (error) {
|
||
console.error('[Mobile] 수동 제출 오류:', error);
|
||
showToast('제출 실패: ' + error.message, 'error');
|
||
if (btn) { btn.disabled = false; btn.textContent = '제출'; }
|
||
}
|
||
}
|
||
|
||
// ===== 작업장소 바텀시트 =====
|
||
function openWorkplaceSheet(manualId) {
|
||
wpSheetManualId = manualId;
|
||
wpSheetStep = 'category';
|
||
wpSelectedCategory = null;
|
||
wpSelectedCategoryName = null;
|
||
wpSelectedId = null;
|
||
wpSelectedName = null;
|
||
|
||
renderWorkplaceCategories();
|
||
showBottomSheet('wp');
|
||
}
|
||
|
||
async function renderWorkplaceCategories() {
|
||
const body = document.getElementById('wpSheetBody');
|
||
document.getElementById('wpSheetTitle').textContent = '작업장소 선택';
|
||
body.innerHTML = '<div class="m-loading"></div>';
|
||
|
||
try {
|
||
const response = await window.apiCall('/workplaces/categories');
|
||
const categories = response.success ? response.data : response;
|
||
|
||
let html = '<div class="m-wp-category-grid">';
|
||
categories.forEach(cat => {
|
||
html += `<button class="m-wp-category-btn" onclick="MobileReport.selectWpCategory(${cat.category_id}, '${esc(cat.category_name)}')">🏭 ${esc(cat.category_name)}</button>`;
|
||
});
|
||
html += `<button class="m-wp-category-btn" onclick="MobileReport.selectExternalWp()" style="border-color:#0ea5e9;background:#f0f9ff;">🌐 외부</button>`;
|
||
html += '</div>';
|
||
|
||
body.innerHTML = html;
|
||
} catch (error) {
|
||
body.innerHTML = '<div class="m-empty">작업장소 로드 실패</div>';
|
||
}
|
||
}
|
||
|
||
async function selectWpCategory(categoryId, categoryName) {
|
||
wpSelectedCategory = categoryId;
|
||
wpSelectedCategoryName = categoryName;
|
||
wpSheetStep = 'list';
|
||
|
||
document.getElementById('wpSheetTitle').textContent = categoryName;
|
||
const body = document.getElementById('wpSheetBody');
|
||
body.innerHTML = '<div class="m-loading"></div>';
|
||
|
||
try {
|
||
const response = await window.apiCall(`/workplaces?category_id=${categoryId}`);
|
||
const workplaces = response.success ? response.data : response;
|
||
|
||
let html = `<button style="display:flex;align-items:center;gap:0.375rem;font-size:0.8125rem;color:#6b7280;background:none;border:none;cursor:pointer;padding:0.375rem 0;margin-bottom:0.5rem;" onclick="MobileReport.renderWorkplaceCategories()">← 뒤로</button>`;
|
||
html += '<div class="m-wp-list">';
|
||
workplaces.forEach(wp => {
|
||
html += `
|
||
<div class="m-wp-item" onclick="MobileReport.selectWpItem(${wp.workplace_id}, '${esc(wp.workplace_name)}')">
|
||
<span class="m-wp-item-icon">📍</span>
|
||
<span class="m-wp-item-name">${esc(wp.workplace_name)}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
|
||
body.innerHTML = html;
|
||
} catch (error) {
|
||
body.innerHTML = '<div class="m-empty">작업장소 로드 실패</div>';
|
||
}
|
||
}
|
||
|
||
function selectWpItem(workplaceId, workplaceName) {
|
||
wpSelectedId = workplaceId;
|
||
wpSelectedName = workplaceName;
|
||
|
||
// 수동 카드에 반영
|
||
if (manualCards[wpSheetManualId]) {
|
||
manualCards[wpSheetManualId].workplace_id = workplaceId;
|
||
manualCards[wpSheetManualId].workplace_name = workplaceName;
|
||
manualCards[wpSheetManualId].workplace_category_id = wpSelectedCategory;
|
||
manualCards[wpSheetManualId].workplace_category_name = wpSelectedCategoryName;
|
||
}
|
||
|
||
const textEl = document.getElementById('m_manual_wpText_' + wpSheetManualId);
|
||
const boxEl = document.getElementById('m_manual_wpBox_' + wpSheetManualId);
|
||
if (textEl) textEl.textContent = `${wpSelectedCategoryName} > ${workplaceName}`;
|
||
if (boxEl) boxEl.classList.add('selected');
|
||
|
||
hideWorkplaceSheet();
|
||
showToast('작업장소 선택됨', 'success');
|
||
}
|
||
|
||
function selectExternalWp() {
|
||
if (manualCards[wpSheetManualId]) {
|
||
manualCards[wpSheetManualId].workplace_id = 0;
|
||
manualCards[wpSheetManualId].workplace_name = '외부 (외근/연차/휴무)';
|
||
manualCards[wpSheetManualId].workplace_category_id = 0;
|
||
manualCards[wpSheetManualId].workplace_category_name = '외부';
|
||
}
|
||
|
||
const textEl = document.getElementById('m_manual_wpText_' + wpSheetManualId);
|
||
const boxEl = document.getElementById('m_manual_wpBox_' + wpSheetManualId);
|
||
if (textEl) textEl.textContent = '🌐 외부 (외근/연차/휴무)';
|
||
if (boxEl) boxEl.classList.add('selected');
|
||
|
||
hideWorkplaceSheet();
|
||
showToast('외부 작업장소 선택됨', 'success');
|
||
}
|
||
|
||
function hideWorkplaceSheet() {
|
||
hideBottomSheet('wp');
|
||
}
|
||
|
||
// ===== 완료 보고서 =====
|
||
async function loadCompletedReports() {
|
||
const dateInput = document.getElementById('completedDate');
|
||
const selectedDate = dateInput?.value;
|
||
if (!selectedDate) return;
|
||
|
||
const container = document.getElementById('completedCardList');
|
||
container.innerHTML = '<div class="m-loading"></div>';
|
||
|
||
try {
|
||
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}`);
|
||
let reports = [];
|
||
if (Array.isArray(response)) {
|
||
reports = response;
|
||
} else if (response.success && response.data) {
|
||
reports = Array.isArray(response.data) ? response.data : [];
|
||
} else if (response.data) {
|
||
reports = Array.isArray(response.data) ? response.data : [];
|
||
}
|
||
|
||
renderCompletedCards(reports);
|
||
} catch (error) {
|
||
console.error('[Mobile] 완료 보고서 로드 오류:', error);
|
||
container.innerHTML = '<div class="m-empty"><div class="m-empty-icon">⚠️</div><div>보고서를 불러올 수 없습니다</div></div>';
|
||
}
|
||
}
|
||
|
||
function renderCompletedCards(reports) {
|
||
const container = document.getElementById('completedCardList');
|
||
|
||
if (!reports || reports.length === 0) {
|
||
container.innerHTML = '<div class="m-empty"><div class="m-empty-icon">📋</div><div>작성된 보고서가 없습니다</div></div>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
reports.forEach(report => {
|
||
const totalHours = parseFloat(report.total_hours || report.work_hours || 0);
|
||
const errorHours = parseFloat(report.error_hours || 0);
|
||
|
||
html += `
|
||
<div class="m-completed-card">
|
||
<div class="m-completed-top">
|
||
<div>
|
||
<div class="m-completed-worker">${esc(report.worker_name || '작업자')}</div>
|
||
<span class="m-completed-badge ${report.tbm_session_id ? 'tbm' : 'manual'}">${report.tbm_session_id ? 'TBM' : '수동'}</span>
|
||
</div>
|
||
</div>
|
||
<div class="m-completed-info">
|
||
<div class="m-info-row"><span class="m-info-label">프로젝트</span><span class="m-info-value">${esc(report.project_name || '-')}</span></div>
|
||
<div class="m-info-row"><span class="m-info-label">공정/작업</span><span class="m-info-value">${esc(report.work_type_name || '-')} / ${esc(report.task_name || '-')}</span></div>
|
||
<div class="m-info-row"><span class="m-info-label">작업시간</span><span class="m-info-value">${totalHours}시간</span></div>
|
||
${errorHours > 0 ? `<div class="m-info-row"><span class="m-info-label">부적합</span><span class="m-info-value" style="color:#dc2626;">${errorHours}시간 (${esc(report.error_type_name || '-')})</span></div>` : ''}
|
||
<div class="m-info-row"><span class="m-info-label">작성자</span><span class="m-info-value">${esc(report.created_by_name || '-')}</span></div>
|
||
</div>
|
||
<div class="m-completed-actions">
|
||
<button class="m-btn-edit" onclick='MobileReport.openEditSheet(${JSON.stringify(report).replace(/'/g, "'")})'>수정</button>
|
||
<button class="m-btn-delete" onclick="MobileReport.deleteReport(${report.id})">삭제</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// ===== 수정 바텀시트 =====
|
||
function openEditSheet(report) {
|
||
editReportId = report.id;
|
||
const state = window.DailyWorkReportState;
|
||
const projects = state.projects || [];
|
||
const workTypes = state.workTypes || [];
|
||
const workStatusTypes = state.workStatusTypes || [];
|
||
|
||
const body = document.getElementById('editSheetBody');
|
||
body.innerHTML = `
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">작업자</label>
|
||
<input class="m-form-input" value="${esc(report.worker_name || '작업자')}" readonly style="background:#f3f4f6;">
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">프로젝트</label>
|
||
<select class="m-form-select" id="m_edit_project">
|
||
${projects.map(p => `<option value="${p.project_id}" ${p.project_id == report.project_id ? 'selected' : ''}>${esc(p.project_name)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">공정</label>
|
||
<select class="m-form-select" id="m_edit_workType" onchange="MobileReport.loadEditTasks()">
|
||
${workTypes.map(wt => `<option value="${wt.id}">${esc(wt.name)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">작업</label>
|
||
<select class="m-form-select" id="m_edit_task">
|
||
<option value="">로딩중...</option>
|
||
</select>
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">작업시간 (시간)</label>
|
||
<input type="number" class="m-form-input" id="m_edit_hours" step="0.5" min="0" max="24" value="${parseFloat(report.work_hours || report.total_hours || 0)}">
|
||
</div>
|
||
<div class="m-form-group">
|
||
<label class="m-form-label">작업상태</label>
|
||
<select class="m-form-select" id="m_edit_status">
|
||
${workStatusTypes.map(ws => `<option value="${ws.id}" ${ws.id == report.work_status_id ? 'selected' : ''}>${esc(ws.name)}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
`;
|
||
|
||
// 작업 목록 로드 후 현재 값 설정
|
||
loadEditTasks().then(() => {
|
||
const taskSelect = document.getElementById('m_edit_task');
|
||
if (report.work_type_id && taskSelect) {
|
||
taskSelect.value = report.work_type_id;
|
||
}
|
||
});
|
||
|
||
showBottomSheet('edit');
|
||
}
|
||
|
||
async function loadEditTasks() {
|
||
const workTypeId = document.getElementById('m_edit_workType')?.value;
|
||
const taskSelect = document.getElementById('m_edit_task');
|
||
if (!workTypeId || !taskSelect) return;
|
||
|
||
try {
|
||
const response = await window.apiCall(`/tasks?work_type_id=${workTypeId}`);
|
||
const tasks = response.data || response || [];
|
||
taskSelect.innerHTML = '<option value="">선택</option>' +
|
||
tasks.map(t => `<option value="${t.task_id}">${esc(t.task_name)}</option>`).join('');
|
||
} catch (error) {
|
||
taskSelect.innerHTML = '<option value="">로드 실패</option>';
|
||
}
|
||
}
|
||
|
||
async function saveEditedReport() {
|
||
const projectId = document.getElementById('m_edit_project')?.value;
|
||
const taskId = document.getElementById('m_edit_task')?.value;
|
||
const workHours = parseFloat(document.getElementById('m_edit_hours')?.value);
|
||
const workStatusId = document.getElementById('m_edit_status')?.value;
|
||
|
||
if (!projectId || !taskId || !workHours) {
|
||
showToast('필수 항목을 입력해주세요', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await window.apiCall(`/daily-work-reports/${editReportId}`, 'PUT', {
|
||
project_id: parseInt(projectId),
|
||
work_type_id: parseInt(taskId),
|
||
work_hours: workHours,
|
||
work_status_id: parseInt(workStatusId)
|
||
});
|
||
|
||
if (response.success) {
|
||
hideEditSheet();
|
||
showToast('수정 완료', 'success');
|
||
loadCompletedReports();
|
||
} else {
|
||
throw new Error(response.message || '수정 실패');
|
||
}
|
||
} catch (error) {
|
||
showToast('수정 실패: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
function hideEditSheet() {
|
||
hideBottomSheet('edit');
|
||
editReportId = null;
|
||
}
|
||
|
||
// ===== 삭제 =====
|
||
function deleteReport(reportId) {
|
||
showConfirm('이 작업보고서를 삭제하시겠습니까?', async () => {
|
||
try {
|
||
const response = await window.apiCall(`/daily-work-reports/${reportId}`, 'DELETE');
|
||
if (response.success) {
|
||
showToast('삭제 완료', 'success');
|
||
loadCompletedReports();
|
||
} else {
|
||
throw new Error(response.message || '삭제 실패');
|
||
}
|
||
} catch (error) {
|
||
showToast('삭제 실패: ' + error.message, 'error');
|
||
}
|
||
}, true);
|
||
}
|
||
|
||
// ===== 바텀시트 공통 =====
|
||
function showBottomSheet(id) {
|
||
const sheetMap = { defect: 'defectSheet', wp: 'wpSheet', edit: 'editSheet' };
|
||
const overlayMap = { defect: 'defectOverlay', wp: 'wpOverlay', edit: 'editOverlay' };
|
||
|
||
const sheet = document.getElementById(sheetMap[id]);
|
||
const overlay = document.getElementById(overlayMap[id]);
|
||
|
||
if (overlay) overlay.classList.add('show');
|
||
if (sheet) {
|
||
sheet.classList.add('show');
|
||
// body 스크롤 방지
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
}
|
||
|
||
function hideBottomSheet(id) {
|
||
const sheetMap = { defect: 'defectSheet', wp: 'wpSheet', edit: 'editSheet' };
|
||
const overlayMap = { defect: 'defectOverlay', wp: 'wpOverlay', edit: 'editOverlay' };
|
||
|
||
const sheet = document.getElementById(sheetMap[id]);
|
||
const overlay = document.getElementById(overlayMap[id]);
|
||
|
||
if (overlay) overlay.classList.remove('show');
|
||
if (sheet) sheet.classList.remove('show');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
// ===== DOM Ready =====
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => { setTimeout(init, 100); });
|
||
} else {
|
||
setTimeout(init, 100);
|
||
}
|
||
|
||
// ===== Public API =====
|
||
return {
|
||
switchTab,
|
||
toggleDateGroup,
|
||
openTimePicker,
|
||
setTime,
|
||
adjustTime,
|
||
confirmTime,
|
||
closeTimePicker,
|
||
openDefectSheet,
|
||
toggleDefectIssue,
|
||
addManualDefect,
|
||
removeManualDefect,
|
||
updateDefectCategory,
|
||
updateDefectItem,
|
||
updateDefectNote,
|
||
openDefectTimePicker,
|
||
saveDefects,
|
||
hideDefectSheet,
|
||
submitTbmReport,
|
||
batchSubmitSession,
|
||
addManualCard,
|
||
removeManualCard,
|
||
updateManualField,
|
||
onWorkTypeChange,
|
||
submitManualReport,
|
||
openWorkplaceSheet,
|
||
renderWorkplaceCategories,
|
||
selectWpCategory,
|
||
selectWpItem,
|
||
selectExternalWp,
|
||
hideWorkplaceSheet,
|
||
loadCompletedReports,
|
||
openEditSheet,
|
||
loadEditTasks,
|
||
saveEditedReport,
|
||
hideEditSheet,
|
||
deleteReport,
|
||
closeResult,
|
||
showToast
|
||
};
|
||
|
||
})();
|