Files
tk-factory-services/tkpurchase/web/static/js/tkpurchase-visit.js
Hyungi Ahn 281f5d35d1 feat: tkpurchase 시스템 Phase 1 - 협력업체 마스터 + 당일 방문 관리
신규 독립 시스템 tkpurchase (구매/방문 관리) 구축:
- 협력업체 CRUD + 소속 작업자 관리 (마스터 데이터 소유)
- 당일 방문 등록/체크인/체크아웃 + 일괄 마감
- 업체 자동완성, CSV 내보내기, 집계 통계
- 자정 자동 체크아웃 (node-cron)
- tkuser 협력업체 읽기 전용 탭 + 권한 그리드(tkpurchase-perms) 추가
- docker-compose에 tkpurchase-api/web 서비스 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:45:37 +09:00

273 lines
12 KiB
JavaScript

/* ===== Visit Management ===== */
let todayVisits = [];
let editingVisitId = null;
async function loadTodayVisits() {
try {
const r = await api('/daily-visits/today');
const { visits, stats } = r.data;
todayVisits = visits;
renderStats(stats);
renderVisitTable(visits);
} catch (e) {
showToast('데이터 로드 실패: ' + e.message, 'error');
}
}
function renderStats(s) {
document.getElementById('statTotal').textContent = s.total || 0;
document.getElementById('statCheckedIn').textContent = s.checked_in || 0;
document.getElementById('statCheckedOut').textContent = s.checked_out || 0;
document.getElementById('statVisitors').textContent = s.total_visitors || 0;
}
function renderVisitTable(visits) {
const tbody = document.getElementById('visitTableBody');
if (!visits.length) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-8">오늘 방문 기록이 없습니다</td></tr>';
return;
}
tbody.innerHTML = visits.map(v => {
const companyName = v.partner_company_name || v.company_name || '-';
const safetyIcon = v.safety_education_yn
? '<i class="fas fa-check-circle text-emerald-500"></i>'
: '<i class="fas fa-exclamation-triangle text-amber-500 safety-warning" title="안전교육 미이수"></i>';
const actions = v.status === 'checked_in'
? `<button onclick="doCheckout(${v.id})" class="text-blue-600 hover:text-blue-800 text-xs px-2 py-1 border border-blue-200 rounded hover:bg-blue-50">체크아웃</button>`
: '';
return `<tr>
<td>${escapeHtml(companyName)}</td>
<td>${escapeHtml(v.visitor_name)}</td>
<td class="text-center">${v.visitor_count}</td>
<td>${purposeBadge(v.purpose)}</td>
<td class="hide-mobile">${safetyIcon}</td>
<td>${formatTime(v.check_in_time)}</td>
<td>${statusBadge(v.status)}</td>
<td class="text-right">
${actions}
<button onclick="openEditVisit(${v.id})" class="text-gray-400 hover:text-gray-600 text-xs ml-1" title="수정"><i class="fas fa-pen"></i></button>
<button onclick="doDeleteVisit(${v.id})" class="text-gray-400 hover:text-red-500 text-xs ml-1" title="삭제"><i class="fas fa-trash"></i></button>
</td>
</tr>`;
}).join('');
}
/* ===== 업체 자동완성 ===== */
let companySearchTimeout = null;
let selectedCompanyId = null;
function initCompanySearch() {
const input = document.getElementById('companySearch');
const dropdown = document.getElementById('companyDropdown');
const manualToggle = document.getElementById('manualCompanyToggle');
const manualInput = document.getElementById('manualCompanyName');
input.addEventListener('input', () => {
clearTimeout(companySearchTimeout);
selectedCompanyId = null;
const q = input.value.trim();
if (q.length < 1) { dropdown.classList.add('hidden'); return; }
companySearchTimeout = setTimeout(async () => {
try {
const r = await api('/partners/search?q=' + encodeURIComponent(q));
const items = r.data || [];
if (items.length === 0) {
dropdown.innerHTML = '<div class="px-3 py-2 text-sm text-gray-400">검색 결과 없음</div>';
} else {
dropdown.innerHTML = items.map(c =>
`<div class="px-3 py-2 text-sm hover:bg-emerald-50 cursor-pointer" onclick="selectCompany(${c.id}, '${escapeHtml(c.company_name).replace(/'/g, "\\'")}')">
<span class="font-medium">${escapeHtml(c.company_name)}</span>
${c.business_number ? `<span class="text-gray-400 text-xs ml-2">${c.business_number}</span>` : ''}
</div>`
).join('');
}
dropdown.classList.remove('hidden');
} catch (e) { dropdown.classList.add('hidden'); }
}, 300);
});
input.addEventListener('blur', () => setTimeout(() => dropdown.classList.add('hidden'), 200));
manualToggle.addEventListener('change', () => {
if (manualToggle.checked) {
input.parentElement.classList.add('hidden');
manualInput.parentElement.classList.remove('hidden');
selectedCompanyId = null;
input.value = '';
} else {
input.parentElement.classList.remove('hidden');
manualInput.parentElement.classList.add('hidden');
manualInput.value = '';
}
});
}
function selectCompany(id, name) {
selectedCompanyId = id;
document.getElementById('companySearch').value = name;
document.getElementById('companyDropdown').classList.add('hidden');
}
/* ===== 인원수 +- ===== */
function initCounterButtons() {
document.getElementById('countMinus').addEventListener('click', () => {
const el = document.getElementById('visitorCount');
const v = parseInt(el.value) || 1;
if (v > 1) el.value = v - 1;
});
document.getElementById('countPlus').addEventListener('click', () => {
const el = document.getElementById('visitorCount');
el.value = (parseInt(el.value) || 0) + 1;
});
}
/* ===== 추가정보 접이식 ===== */
function toggleExtra() {
document.getElementById('extraFields').classList.toggle('open');
const icon = document.getElementById('extraToggleIcon');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-up');
}
/* ===== 방문 등록 ===== */
async function submitVisit(e) {
e.preventDefault();
const manualMode = document.getElementById('manualCompanyToggle').checked;
const company_id = manualMode ? null : selectedCompanyId;
const company_name = manualMode ? document.getElementById('manualCompanyName').value.trim() : null;
if (!company_id && !company_name) {
showToast('업체를 선택하거나 입력해주세요', 'error'); return;
}
const data = {
company_id,
company_name: company_name || document.getElementById('companySearch').value.trim(),
visitor_name: document.getElementById('visitorName').value.trim(),
visitor_count: parseInt(document.getElementById('visitorCount').value) || 1,
purpose: document.getElementById('visitPurpose').value,
purpose_detail: document.getElementById('purposeDetail').value.trim() || null,
workplace_name: document.getElementById('workplaceName').value.trim() || null,
safety_education_yn: document.getElementById('safetyCheck').checked,
vehicle_number: document.getElementById('vehicleNumber').value.trim() || null,
notes: document.getElementById('visitNotes').value.trim() || null,
managing_department: document.getElementById('managingDept').value || null,
};
if (!data.visitor_name) { showToast('방문자명을 입력해주세요', 'error'); return; }
if (!data.purpose) { showToast('방문 목적을 선택해주세요', 'error'); return; }
try {
await api('/daily-visits/', { method: 'POST', body: JSON.stringify(data) });
showToast('방문이 등록되었습니다');
document.getElementById('visitForm').reset();
selectedCompanyId = null;
document.getElementById('manualCompanyToggle').checked = false;
document.getElementById('companySearch').parentElement.classList.remove('hidden');
document.getElementById('manualCompanyName').parentElement.classList.add('hidden');
document.getElementById('visitorCount').value = '1';
await loadTodayVisits();
} catch (e) {
showToast(e.message, 'error');
}
}
/* ===== 체크아웃 ===== */
async function doCheckout(id) {
try {
await api(`/daily-visits/${id}/checkout`, { method: 'PUT', body: JSON.stringify({}) });
showToast('체크아웃 완료');
await loadTodayVisits();
} catch (e) { showToast(e.message, 'error'); }
}
async function doBulkCheckout() {
const checkedIn = todayVisits.filter(v => v.status === 'checked_in');
if (checkedIn.length === 0) { showToast('체크인 중인 방문이 없습니다', 'error'); return; }
if (!confirm(`체크인 중인 ${checkedIn.length}건을 모두 체크아웃 하시겠습니까?`)) return;
try {
const r = await api('/daily-visits/bulk-checkout', { method: 'POST', body: JSON.stringify({}) });
showToast(`${r.data.affected}건 체크아웃 완료`);
await loadTodayVisits();
} catch (e) { showToast(e.message, 'error'); }
}
/* ===== 수정 ===== */
function openEditVisit(id) {
const v = todayVisits.find(x => x.id === id);
if (!v) return;
editingVisitId = id;
document.getElementById('editVisitorName').value = v.visitor_name;
document.getElementById('editVisitorCount').value = v.visitor_count;
document.getElementById('editPurpose').value = v.purpose;
document.getElementById('editPurposeDetail').value = v.purpose_detail || '';
document.getElementById('editWorkplace').value = v.workplace_name || '';
document.getElementById('editSafetyCheck').checked = v.safety_education_yn;
document.getElementById('editVehicle').value = v.vehicle_number || '';
document.getElementById('editNotes').value = v.notes || '';
document.getElementById('editVisitModal').classList.remove('hidden');
}
function closeEditVisit() {
document.getElementById('editVisitModal').classList.add('hidden');
editingVisitId = null;
}
async function submitEditVisit(e) {
e.preventDefault();
if (!editingVisitId) return;
const data = {
visitor_name: document.getElementById('editVisitorName').value.trim(),
visitor_count: parseInt(document.getElementById('editVisitorCount').value) || 1,
purpose: document.getElementById('editPurpose').value,
purpose_detail: document.getElementById('editPurposeDetail').value.trim() || null,
workplace_name: document.getElementById('editWorkplace').value.trim() || null,
safety_education_yn: document.getElementById('editSafetyCheck').checked,
vehicle_number: document.getElementById('editVehicle').value.trim() || null,
notes: document.getElementById('editNotes').value.trim() || null,
};
try {
await api(`/daily-visits/${editingVisitId}`, { method: 'PUT', body: JSON.stringify(data) });
showToast('수정되었습니다');
closeEditVisit();
await loadTodayVisits();
} catch (e) { showToast(e.message, 'error'); }
}
/* ===== 삭제 ===== */
async function doDeleteVisit(id) {
if (!confirm('이 방문 기록을 삭제하시겠습니까?')) return;
try {
await api(`/daily-visits/${id}`, { method: 'DELETE' });
showToast('삭제되었습니다');
await loadTodayVisits();
} catch (e) { showToast(e.message, 'error'); }
}
/* ===== CSV 내보내기 ===== */
async function exportVisits() {
const token = getToken();
const dateFrom = document.getElementById('exportDateFrom')?.value || '';
const dateTo = document.getElementById('exportDateTo')?.value || '';
let url = API_BASE + '/daily-visits/export?';
if (dateFrom) url += 'date_from=' + dateFrom + '&';
if (dateTo) url += 'date_to=' + dateTo + '&';
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } });
if (!res.ok) { showToast('내보내기 실패', 'error'); return; }
const blob = await res.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `visits_${dateFrom || 'all'}_${dateTo || 'all'}.csv`;
a.click();
}
/* ===== Init ===== */
function initVisitPage() {
if (!initAuth()) return;
initCompanySearch();
initCounterButtons();
document.getElementById('visitForm').addEventListener('submit', submitVisit);
document.getElementById('editVisitForm').addEventListener('submit', submitEditVisit);
loadTodayVisits();
}