프론트엔드: - Step 1: 날짜 선택 → 전체 작업자 목록 (완료/미입력/휴가 구분) - Step 2: 선택 작업자 일괄 편집 (프로젝트/공종/시간/부적합/비고) - 연차=선택불가, 반차=4h, 반반차=6h 기본값 백엔드: - POST /api/proxy-input UPSERT 방식 (409 제거) - 신규: TBM 세션 자동 생성 + 작업보고서 INSERT - 기존: 작업보고서 UPDATE - 부적합: work_report_defects INSERT (기존 defect 있으면 SKIP) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
255 lines
9.6 KiB
JavaScript
255 lines
9.6 KiB
JavaScript
/**
|
|
* proxy-input.js — 대리입력 리뉴얼
|
|
* Step 1: 날짜 선택 → 작업자 목록 (체크박스)
|
|
* Step 2: 선택 작업자 일괄 편집 → 저장 (TBM 자동 생성)
|
|
*/
|
|
|
|
let currentDate = '';
|
|
let allWorkers = [];
|
|
let selectedIds = new Set();
|
|
let projects = [];
|
|
let workTypes = [];
|
|
let editData = {}; // { userId: { project_id, work_type_id, work_hours, defect_hours, note } }
|
|
|
|
// ===== Init =====
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const now = new Date();
|
|
currentDate = now.toISOString().substring(0, 10);
|
|
document.getElementById('dateInput').value = currentDate;
|
|
|
|
setTimeout(async () => {
|
|
await loadDropdownData();
|
|
await loadWorkers();
|
|
}, 500);
|
|
});
|
|
|
|
async function loadDropdownData() {
|
|
try {
|
|
const [pRes, wRes] = await Promise.all([
|
|
window.apiCall('/projects'),
|
|
window.apiCall('/daily-work-reports/work-types')
|
|
]);
|
|
projects = (pRes.data || pRes || []).filter(p => p.is_active !== 0);
|
|
workTypes = (wRes.data || wRes || []).filter(w => w.is_active !== 0);
|
|
} catch (e) { console.warn('드롭다운 로드 실패:', e); }
|
|
}
|
|
|
|
// ===== Step 1: Worker List =====
|
|
async function loadWorkers() {
|
|
currentDate = document.getElementById('dateInput').value;
|
|
if (!currentDate) return;
|
|
|
|
const list = document.getElementById('workerList');
|
|
list.innerHTML = '<div class="pi-skeleton"></div><div class="pi-skeleton"></div>';
|
|
selectedIds.clear();
|
|
updateEditButton();
|
|
|
|
try {
|
|
const res = await window.apiCall('/proxy-input/daily-status?date=' + currentDate);
|
|
if (!res.success) throw new Error(res.message);
|
|
|
|
allWorkers = res.data.workers || [];
|
|
const s = res.data.summary || {};
|
|
document.getElementById('totalNum').textContent = s.total_active_workers || 0;
|
|
document.getElementById('doneNum').textContent = s.report_completed || 0;
|
|
document.getElementById('missingNum').textContent = s.report_missing || 0;
|
|
document.getElementById('vacNum').textContent = allWorkers.filter(w => w.vacation_type_code === 'ANNUAL_FULL').length;
|
|
|
|
renderWorkerList();
|
|
} catch (e) {
|
|
list.innerHTML = '<div class="pi-empty"><i class="fas fa-exclamation-triangle text-2xl text-red-300"></i><p>데이터 로드 실패</p></div>';
|
|
}
|
|
}
|
|
|
|
function renderWorkerList() {
|
|
const list = document.getElementById('workerList');
|
|
if (!allWorkers.length) {
|
|
list.innerHTML = '<div class="pi-empty"><p>작업자가 없습니다</p></div>';
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = allWorkers.map(w => {
|
|
const isFullVac = w.vacation_type_code === 'ANNUAL_FULL';
|
|
const hasVac = !!w.vacation_type_code;
|
|
const vacBadge = isFullVac ? '<span class="pi-badge vac">연차</span>'
|
|
: hasVac ? `<span class="pi-badge vac-half">${esc(w.vacation_type_name)}</span>` : '';
|
|
const doneBadge = w.has_report ? `<span class="pi-badge done">${w.total_report_hours}h</span>` : '<span class="pi-badge missing">미입력</span>';
|
|
|
|
return `
|
|
<label class="pi-worker ${isFullVac ? 'disabled' : ''}" data-uid="${w.user_id}">
|
|
<input type="checkbox" class="pi-check" value="${w.user_id}"
|
|
${isFullVac ? 'disabled' : ''}
|
|
onchange="onWorkerCheck(${w.user_id}, this.checked)">
|
|
<div class="pi-worker-info">
|
|
<span class="pi-worker-name">${esc(w.worker_name)}</span>
|
|
<span class="pi-worker-job">${esc(w.job_type || '')}</span>
|
|
</div>
|
|
<div class="pi-worker-badges">
|
|
${vacBadge}${doneBadge}
|
|
</div>
|
|
</label>`;
|
|
}).join('');
|
|
}
|
|
|
|
function onWorkerCheck(userId, checked) {
|
|
if (checked) selectedIds.add(userId);
|
|
else selectedIds.delete(userId);
|
|
updateEditButton();
|
|
}
|
|
|
|
function toggleSelectAll(checked) {
|
|
allWorkers.forEach(w => {
|
|
if (w.vacation_type_code === 'ANNUAL_FULL') return;
|
|
const cb = document.querySelector(`.pi-check[value="${w.user_id}"]`);
|
|
if (cb) { cb.checked = checked; onWorkerCheck(w.user_id, checked); }
|
|
});
|
|
}
|
|
|
|
function updateEditButton() {
|
|
const btn = document.getElementById('editBtn');
|
|
const n = selectedIds.size;
|
|
btn.disabled = n === 0;
|
|
document.getElementById('editBtnText').textContent = n > 0 ? `선택 작업자 편집 (${n}명)` : '작업자를 선택하세요';
|
|
}
|
|
|
|
// ===== Step 2: Edit Mode =====
|
|
function openEditMode() {
|
|
if (selectedIds.size === 0) return;
|
|
|
|
const selected = allWorkers.filter(w => selectedIds.has(w.user_id));
|
|
editData = {};
|
|
|
|
// 기본값 설정
|
|
selected.forEach(w => {
|
|
const isHalfVac = w.vacation_type_code === 'ANNUAL_HALF';
|
|
const isQuarterVac = w.vacation_type_code === 'ANNUAL_QUARTER';
|
|
editData[w.user_id] = {
|
|
project_id: '',
|
|
work_type_id: '',
|
|
work_hours: isHalfVac ? 4 : isQuarterVac ? 6 : 8,
|
|
defect_hours: 0,
|
|
note: '',
|
|
start_time: '08:00',
|
|
end_time: isHalfVac ? '12:00' : isQuarterVac ? '14:00' : '17:00',
|
|
work_status_id: 1
|
|
};
|
|
});
|
|
|
|
document.getElementById('editTitle').textContent = `일괄 편집 (${selected.length}명)`;
|
|
|
|
const projOpts = projects.map(p => `<option value="${p.project_id}">${esc(p.project_name)}</option>`).join('');
|
|
const typeOpts = workTypes.map(t => `<option value="${t.work_type_id}">${esc(t.work_type_name)}</option>`).join('');
|
|
|
|
document.getElementById('editList').innerHTML = selected.map(w => {
|
|
const d = editData[w.user_id];
|
|
const vacLabel = w.vacation_type_name ? ` <span class="pi-badge vac-half">${esc(w.vacation_type_name)}</span>` : '';
|
|
return `
|
|
<div class="pi-edit-card" data-uid="${w.user_id}">
|
|
<div class="pi-edit-header">
|
|
<strong>${esc(w.worker_name)}</strong>
|
|
<span class="pi-edit-job">${esc(w.job_type || '')}</span>
|
|
${vacLabel}
|
|
</div>
|
|
<div class="pi-edit-fields">
|
|
<div class="pi-edit-row">
|
|
<select id="proj_${w.user_id}" class="pi-select" required>
|
|
<option value="">프로젝트 *</option>${projOpts}
|
|
</select>
|
|
<select id="wtype_${w.user_id}" class="pi-select" required>
|
|
<option value="">공종 *</option>${typeOpts}
|
|
</select>
|
|
</div>
|
|
<div class="pi-edit-row">
|
|
<label class="pi-field"><span>시간</span><input type="number" id="hours_${w.user_id}" value="${d.work_hours}" step="0.5" min="0" max="24" class="pi-input"></label>
|
|
<label class="pi-field"><span>부적합</span><input type="number" id="defect_${w.user_id}" value="0" step="0.5" min="0" max="24" class="pi-input"></label>
|
|
</div>
|
|
<input type="text" id="note_${w.user_id}" placeholder="비고" class="pi-note-input" value="">
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
document.getElementById('step1').classList.add('hidden');
|
|
document.getElementById('step2').classList.remove('hidden');
|
|
}
|
|
|
|
function closeEditMode() {
|
|
document.getElementById('step2').classList.add('hidden');
|
|
document.getElementById('step1').classList.remove('hidden');
|
|
}
|
|
|
|
// ===== Save =====
|
|
async function saveAll() {
|
|
const btn = document.getElementById('saveBtn');
|
|
btn.disabled = true;
|
|
document.getElementById('saveBtnText').textContent = '저장 중...';
|
|
|
|
const entries = [];
|
|
let hasError = false;
|
|
|
|
for (const uid of selectedIds) {
|
|
const projEl = document.getElementById('proj_' + uid);
|
|
const wtypeEl = document.getElementById('wtype_' + uid);
|
|
const hoursEl = document.getElementById('hours_' + uid);
|
|
const defectEl = document.getElementById('defect_' + uid);
|
|
const noteEl = document.getElementById('note_' + uid);
|
|
|
|
if (!projEl?.value || !wtypeEl?.value) {
|
|
showToast(`프로젝트/공종을 선택하세요`, 'error');
|
|
projEl?.focus();
|
|
hasError = true;
|
|
break;
|
|
}
|
|
|
|
const workHours = parseFloat(hoursEl?.value) || 0;
|
|
const defectHours = parseFloat(defectEl?.value) || 0;
|
|
|
|
if (defectHours > workHours) {
|
|
showToast('부적합 시간이 근무시간을 초과합니다', 'error');
|
|
hasError = true;
|
|
break;
|
|
}
|
|
|
|
entries.push({
|
|
user_id: uid,
|
|
project_id: parseInt(projEl.value),
|
|
work_type_id: parseInt(wtypeEl.value),
|
|
work_hours: workHours,
|
|
defect_hours: defectHours,
|
|
note: noteEl?.value || '',
|
|
start_time: '08:00',
|
|
end_time: '17:00',
|
|
work_status_id: defectHours > 0 ? 2 : 1
|
|
});
|
|
}
|
|
|
|
if (hasError) {
|
|
btn.disabled = false;
|
|
document.getElementById('saveBtnText').textContent = '전체 저장';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await window.apiCall('/proxy-input', 'POST', {
|
|
session_date: currentDate,
|
|
entries
|
|
});
|
|
|
|
if (res.success) {
|
|
showToast(res.message || `${entries.length}명 저장 완료`, 'success');
|
|
closeEditMode();
|
|
selectedIds.clear();
|
|
updateEditButton();
|
|
await loadWorkers();
|
|
} else {
|
|
showToast(res.message || '저장 실패', 'error');
|
|
}
|
|
} catch (e) {
|
|
showToast('저장 실패: ' + (e.message || e), 'error');
|
|
}
|
|
|
|
btn.disabled = false;
|
|
document.getElementById('saveBtnText').textContent = '전체 저장';
|
|
}
|
|
|
|
function esc(s) { return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|