Files
tk-factory-services/tkpurchase/web/static/js/tkpurchase-schedule.js
Hyungi Ahn b14448fc54 feat(tkpurchase): 체크인 worker_names 배열 저장 + 구매팀 체크인 관리 기능
- doCheckIn()에서 worker_names를 콤마 split 배열로 전송 (DB에 JSON 배열로 저장)
- 구매팀 일정 페이지에 체크인 조회/수정/삭제 모달 추가
- DELETE /checkins/:id endpoint + 트랜잭션 삭제 (reports cascade)
- PUT /checkins/:id에 requirePage 권한 guard 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:53:46 +09:00

387 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* tkpurchase-schedule.js - Schedule management */
let schedulePage = 1;
const scheduleLimit = 20;
let companySearchTimer = null;
let projectList = [];
async function loadProjects() {
try {
const r = await api('/projects/active');
projectList = r.data || [];
} catch(e) {
console.warn('Load projects error:', e);
projectList = [];
}
}
function populateProjectDropdown(selectId, selectedId) {
const select = document.getElementById(selectId);
select.innerHTML = '<option value="">선택 안함</option>';
projectList.forEach(p => {
const label = p.job_no ? `[${p.job_no}] ${p.project_name}` : p.project_name;
select.innerHTML += `<option value="${p.project_id}" ${p.project_id == selectedId ? 'selected' : ''}>${escapeHtml(label)}</option>`;
});
}
function formatDateRange(startDate, endDate) {
const s = formatDate(startDate);
const e = formatDate(endDate);
if (s === e) return s;
// 같은 연도이면 월/일만 표시
const sd = new Date(startDate);
const ed = new Date(endDate);
if (sd.getFullYear() === ed.getFullYear()) {
return `${sd.getMonth()+1}/${sd.getDate()} ~ ${ed.getMonth()+1}/${ed.getDate()}`;
}
return `${s} ~ ${e}`;
}
async function loadSchedules() {
const company = document.getElementById('filterCompany').value.trim();
const dateFrom = document.getElementById('filterDateFrom').value;
const dateTo = document.getElementById('filterDateTo').value;
const status = document.getElementById('filterStatus').value;
let query = `?page=${schedulePage}&limit=${scheduleLimit}`;
if (company) query += '&company=' + encodeURIComponent(company);
if (dateFrom) query += '&date_from=' + dateFrom;
if (dateTo) query += '&date_to=' + dateTo;
if (status) query += '&status=' + status;
try {
const r = await api('/schedules' + query);
renderScheduleTable(r.data || [], r.total || 0);
} catch(e) {
console.warn('Schedule load error:', e);
document.getElementById('scheduleTableBody').innerHTML = '<tr><td colspan="8" class="text-center text-red-400 py-8">로딩 실패</td></tr>';
}
}
function renderScheduleTable(list, total) {
const tbody = document.getElementById('scheduleTableBody');
if (!list.length) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-8">일정이 없습니다</td></tr>';
document.getElementById('schedulePagination').innerHTML = '';
return;
}
const statusMap = {
scheduled: ['badge-amber', '예정'],
in_progress: ['badge-green', '진행중'],
completed: ['badge-blue', '완료'],
cancelled: ['badge-gray', '취소']
};
tbody.innerHTML = list.map(s => {
const [cls, label] = statusMap[s.status] || ['badge-gray', s.status];
const canEdit = s.status === 'scheduled';
const projectLabel = s.project_name ? (s.job_no ? `[${s.job_no}] ${s.project_name}` : s.project_name) : '';
return `<tr>
<td class="font-medium">${escapeHtml(s.company_name || '')}</td>
<td class="whitespace-nowrap">${formatDateRange(s.start_date, s.end_date)}</td>
<td class="text-xs text-gray-500">${escapeHtml(projectLabel)}</td>
<td class="max-w-xs truncate">${escapeHtml(s.work_description || '')}</td>
<td class="hide-mobile">${escapeHtml(s.workplace_name || '')}</td>
<td class="text-center">${s.expected_workers || 0}명</td>
<td><span class="badge ${cls}">${label}</span></td>
<td class="text-right">
${(s.status === 'in_progress' || s.status === 'completed') ? `<button onclick="viewCheckins(${s.id})" class="text-emerald-600 hover:text-emerald-800 text-xs mr-1" title="체크인 현황"><i class="fas fa-clipboard-check"></i></button>` : ''}
${canEdit ? `<button onclick="openEditSchedule(${s.id})" class="text-blue-600 hover:text-blue-800 text-xs mr-1" title="수정"><i class="fas fa-edit"></i></button>
<button onclick="deleteSchedule(${s.id})" class="text-red-500 hover:text-red-700 text-xs" title="삭제"><i class="fas fa-trash"></i></button>` : ''}
</td>
</tr>`;
}).join('');
// Pagination
const totalPages = Math.ceil(total / scheduleLimit);
renderSchedulePagination(totalPages);
}
function renderSchedulePagination(totalPages) {
const container = document.getElementById('schedulePagination');
if (totalPages <= 1) { container.innerHTML = ''; return; }
let html = '';
if (schedulePage > 1) {
html += `<button onclick="goToSchedulePage(${schedulePage - 1})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">&laquo;</button>`;
}
for (let i = 1; i <= totalPages; i++) {
if (i === schedulePage) {
html += `<button class="px-3 py-1 bg-emerald-600 text-white rounded text-sm">${i}</button>`;
} else if (Math.abs(i - schedulePage) <= 2 || i === 1 || i === totalPages) {
html += `<button onclick="goToSchedulePage(${i})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">${i}</button>`;
} else if (Math.abs(i - schedulePage) === 3) {
html += '<span class="text-gray-400">...</span>';
}
}
if (schedulePage < totalPages) {
html += `<button onclick="goToSchedulePage(${schedulePage + 1})" class="px-3 py-1 border rounded text-sm hover:bg-gray-50">&raquo;</button>`;
}
container.innerHTML = html;
}
function goToSchedulePage(p) {
schedulePage = p;
loadSchedules();
}
/* ===== Company Autocomplete ===== */
function setupCompanyAutocomplete(inputId, dropdownId, hiddenId) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
const hidden = document.getElementById(hiddenId);
input.addEventListener('input', function() {
hidden.value = '';
clearTimeout(companySearchTimer);
const q = this.value.trim();
if (q.length < 1) { dropdown.classList.add('hidden'); return; }
companySearchTimer = setTimeout(async () => {
try {
const r = await api('/partners/search?q=' + encodeURIComponent(q));
const list = r.data || [];
if (!list.length) {
dropdown.innerHTML = '<div class="px-3 py-2 text-sm text-gray-400">결과 없음</div>';
} else {
dropdown.innerHTML = list.map(c =>
`<div class="px-3 py-2 text-sm hover:bg-emerald-50 cursor-pointer" onclick="selectCompany('${inputId}','${hiddenId}','${dropdownId}',${c.id},'${escapeHtml(c.company_name)}')">${escapeHtml(c.company_name)}</div>`
).join('');
}
dropdown.classList.remove('hidden');
} catch(e) {
dropdown.classList.add('hidden');
}
}, 300);
});
input.addEventListener('blur', function() {
setTimeout(() => dropdown.classList.add('hidden'), 200);
});
}
function selectCompany(inputId, hiddenId, dropdownId, id, name) {
document.getElementById(inputId).value = name;
document.getElementById(hiddenId).value = id;
document.getElementById(dropdownId).classList.add('hidden');
}
/* ===== Add Schedule ===== */
async function openAddSchedule() {
document.getElementById('addScheduleForm').reset();
document.getElementById('addCompanyId').value = '';
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = tomorrow.toISOString().substring(0, 10);
document.getElementById('addStartDate').value = tomorrowStr;
document.getElementById('addEndDate').value = tomorrowStr;
await loadProjects();
populateProjectDropdown('addProject', '');
document.getElementById('addScheduleModal').classList.remove('hidden');
}
function closeAddSchedule() {
document.getElementById('addScheduleModal').classList.add('hidden');
}
async function submitAddSchedule(e) {
e.preventDefault();
const companyId = document.getElementById('addCompanyId').value;
if (!companyId) { showToast('업체를 선택하세요', 'error'); return; }
const startDate = document.getElementById('addStartDate').value;
const endDate = document.getElementById('addEndDate').value;
if (endDate < startDate) { showToast('종료일은 시작일 이후여야 합니다', 'error'); return; }
const projectId = document.getElementById('addProject').value;
const body = {
company_id: parseInt(companyId),
start_date: startDate,
end_date: endDate,
project_id: projectId ? parseInt(projectId) : null,
work_description: document.getElementById('addWorkDescription').value.trim(),
workplace_name: document.getElementById('addWorkplaceName').value.trim(),
expected_workers: parseInt(document.getElementById('addExpectedWorkers').value) || 0,
notes: document.getElementById('addNotes').value.trim()
};
try {
await api('/schedules', { method: 'POST', body: JSON.stringify(body) });
showToast('일정이 등록되었습니다');
closeAddSchedule();
loadSchedules();
} catch(e) {
showToast(e.message || '등록 실패', 'error');
}
}
/* ===== Edit Schedule ===== */
let scheduleCache = {};
async function openEditSchedule(id) {
try {
const r = await api('/schedules/' + id);
const s = r.data || r;
scheduleCache[id] = s;
document.getElementById('editScheduleId').value = id;
document.getElementById('editCompanySearch').value = s.company_name || '';
document.getElementById('editCompanyId').value = s.company_id || '';
document.getElementById('editStartDate').value = formatDate(s.start_date);
document.getElementById('editEndDate').value = formatDate(s.end_date);
document.getElementById('editWorkDescription').value = s.work_description || '';
document.getElementById('editWorkplaceName').value = s.workplace_name || '';
document.getElementById('editExpectedWorkers').value = s.expected_workers || 0;
document.getElementById('editNotes').value = s.notes || '';
await loadProjects();
populateProjectDropdown('editProject', s.project_id || '');
document.getElementById('editScheduleModal').classList.remove('hidden');
} catch(e) {
showToast('일정 정보를 불러올 수 없습니다', 'error');
}
}
function closeEditSchedule() {
document.getElementById('editScheduleModal').classList.add('hidden');
}
async function submitEditSchedule(e) {
e.preventDefault();
const id = document.getElementById('editScheduleId').value;
const companyId = document.getElementById('editCompanyId').value;
if (!companyId) { showToast('업체를 선택하세요', 'error'); return; }
const startDate = document.getElementById('editStartDate').value;
const endDate = document.getElementById('editEndDate').value;
if (endDate < startDate) { showToast('종료일은 시작일 이후여야 합니다', 'error'); return; }
const projectId = document.getElementById('editProject').value;
const body = {
company_id: parseInt(companyId),
start_date: startDate,
end_date: endDate,
project_id: projectId ? parseInt(projectId) : null,
work_description: document.getElementById('editWorkDescription').value.trim(),
workplace_name: document.getElementById('editWorkplaceName').value.trim(),
expected_workers: parseInt(document.getElementById('editExpectedWorkers').value) || 0,
notes: document.getElementById('editNotes').value.trim()
};
try {
await api('/schedules/' + id, { method: 'PUT', body: JSON.stringify(body) });
showToast('일정이 수정되었습니다');
closeEditSchedule();
loadSchedules();
} catch(e) {
showToast(e.message || '수정 실패', 'error');
}
}
/* ===== Delete Schedule ===== */
async function deleteSchedule(id) {
if (!confirm('이 일정을 삭제하시겠습니까?')) return;
try {
await api('/schedules/' + id, { method: 'DELETE' });
showToast('일정이 삭제되었습니다');
loadSchedules();
} catch(e) {
showToast(e.message || '삭제 실패', 'error');
}
}
/* ===== Checkin Management ===== */
async function viewCheckins(scheduleId) {
document.getElementById('checkinModal').classList.remove('hidden');
const body = document.getElementById('checkinModalBody');
body.innerHTML = '<p class="text-gray-400 text-center py-4">로딩 중...</p>';
try {
const r = await api('/checkins/schedule/' + scheduleId);
const list = r.data || [];
if (!list.length) {
body.innerHTML = '<p class="text-gray-400 text-center py-4">체크인 기록이 없습니다</p>';
return;
}
body.innerHTML = list.map(c => {
let names = '';
if (c.worker_names) {
try {
const parsed = JSON.parse(c.worker_names);
names = Array.isArray(parsed) ? parsed.join(', ') : parsed;
} catch { names = c.worker_names; }
}
const checkinTime = c.check_in_time ? new Date(c.check_in_time).toLocaleString('ko-KR', { month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit' }) : '-';
const checkoutTime = c.check_out_time ? new Date(c.check_out_time).toLocaleString('ko-KR', { hour:'2-digit', minute:'2-digit' }) : '작업중';
return `<div class="border rounded-lg p-3" data-checkin-id="${c.id}">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="text-sm font-medium">${escapeHtml(c.company_name || '')} <span class="text-gray-400 text-xs">${checkinTime} ~ ${checkoutTime}</span></div>
<div class="text-xs text-gray-500 mt-1">인원: ${c.actual_worker_count || 0}명</div>
<div class="text-xs text-gray-600 mt-1" id="checkinNames_${c.id}">작업자: ${escapeHtml(names) || '-'}</div>
</div>
<div class="flex gap-1 ml-2">
<button onclick="editCheckinWorkers(${c.id})" class="text-blue-500 hover:text-blue-700 text-xs px-1" title="수정"><i class="fas fa-edit"></i></button>
<button onclick="deleteCheckin(${c.id},${scheduleId})" class="text-red-500 hover:text-red-700 text-xs px-1" title="삭제"><i class="fas fa-trash"></i></button>
</div>
</div>
</div>`;
}).join('');
} catch(e) {
body.innerHTML = '<p class="text-red-400 text-center py-4">로딩 실패</p>';
}
}
function closeCheckinModal() {
document.getElementById('checkinModal').classList.add('hidden');
}
async function deleteCheckin(checkinId, scheduleId) {
if (!confirm('이 체크인을 삭제하시겠습니까?\n관련 업무현황도 함께 삭제됩니다.')) return;
try {
await api('/checkins/' + checkinId, { method: 'DELETE' });
showToast('체크인이 삭제되었습니다');
viewCheckins(scheduleId);
loadSchedules();
} catch(e) {
showToast(e.message || '삭제 실패', 'error');
}
}
async function editCheckinWorkers(checkinId) {
const el = document.getElementById('checkinNames_' + checkinId);
const current = el.textContent.replace('작업자: ', '').trim();
const input = prompt('작업자 명단 (콤마로 구분)', current === '-' ? '' : current);
if (input === null) return;
const names = input.split(/[,]\s*/).map(n => n.trim()).filter(Boolean);
try {
await api('/checkins/' + checkinId, {
method: 'PUT',
body: JSON.stringify({ worker_names: names.length ? names : null })
});
el.textContent = '작업자: ' + (names.length ? names.join(', ') : '-');
showToast('작업자 명단이 수정되었습니다');
} catch(e) {
showToast(e.message || '수정 실패', 'error');
}
}
/* ===== Init ===== */
function initSchedulePage() {
if (!initAuth()) return;
// Set default date range
const now = new Date();
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
document.getElementById('filterDateFrom').value = firstDay.toISOString().substring(0, 10);
document.getElementById('filterDateTo').value = lastDay.toISOString().substring(0, 10);
// Setup autocomplete for both modals
setupCompanyAutocomplete('addCompanySearch', 'addCompanyDropdown', 'addCompanyId');
setupCompanyAutocomplete('editCompanySearch', 'editCompanyDropdown', 'editCompanyId');
loadSchedules();
}