Phase 1: tksafety에 출입신청/체크리스트 API·웹 추가, tkfb 안전 코드 삭제
Phase 2: 사용자 관리 페이지 삭제, API 축소, 알림 수신자 tkuser 이관
Phase 3: tkuser 권한 페이지 정의 업데이트
Phase 4: 전체 34개 페이지 Tailwind CSS + tkfb-core.js 전환,
미사용 CSS 20개·인프라 JS 10개·템플릿·컴포넌트 삭제
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
217 lines
10 KiB
JavaScript
217 lines
10 KiB
JavaScript
/* ===== Visit Management (출입 관리 - 관리자) ===== */
|
|
let allRequests = [];
|
|
let actionRequestId = null;
|
|
|
|
/* ===== Status badge for visit requests ===== */
|
|
function vrStatusBadge(s) {
|
|
const m = {
|
|
pending: ['badge-amber', '대기중'],
|
|
approved: ['badge-green', '승인됨'],
|
|
rejected: ['badge-red', '반려됨'],
|
|
training_completed: ['badge-blue', '교육완료']
|
|
};
|
|
const [cls, label] = m[s] || ['badge-gray', s];
|
|
return `<span class="badge ${cls}">${label}</span>`;
|
|
}
|
|
|
|
/* ===== Load requests ===== */
|
|
async function loadRequests() {
|
|
try {
|
|
const params = new URLSearchParams();
|
|
const status = document.getElementById('filterStatus').value;
|
|
const dateFrom = document.getElementById('filterDateFrom').value;
|
|
const dateTo = document.getElementById('filterDateTo').value;
|
|
if (status) params.set('status', status);
|
|
if (dateFrom) params.set('start_date', dateFrom);
|
|
if (dateTo) params.set('end_date', dateTo);
|
|
|
|
const res = await api('/visit-requests/requests?' + params.toString());
|
|
allRequests = res.data || [];
|
|
renderStats();
|
|
renderRequestsTable();
|
|
} catch (e) {
|
|
showToast('데이터 로드 실패: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
function renderStats() {
|
|
const counts = { pending: 0, approved: 0, rejected: 0, training_completed: 0 };
|
|
allRequests.forEach(r => { if (counts[r.status] !== undefined) counts[r.status]++; });
|
|
document.getElementById('statPending').textContent = counts.pending;
|
|
document.getElementById('statApproved').textContent = counts.approved;
|
|
document.getElementById('statRejected').textContent = counts.rejected;
|
|
document.getElementById('statTrainingDone').textContent = counts.training_completed;
|
|
}
|
|
|
|
function renderRequestsTable() {
|
|
const tbody = document.getElementById('requestsTableBody');
|
|
if (!allRequests.length) {
|
|
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-gray-400 py-8">신청 내역이 없습니다</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = allRequests.map(r => {
|
|
let actions = '';
|
|
if (r.status === 'pending') {
|
|
actions = `
|
|
<button onclick="openApproveModal(${r.request_id})" class="text-green-600 hover:text-green-800 text-xs px-2 py-1 border border-green-200 rounded hover:bg-green-50" title="승인">
|
|
<i class="fas fa-check"></i>
|
|
</button>
|
|
<button onclick="openRejectModal(${r.request_id})" class="text-red-600 hover:text-red-800 text-xs px-2 py-1 border border-red-200 rounded hover:bg-red-50 ml-1" title="반려">
|
|
<i class="fas fa-times"></i>
|
|
</button>`;
|
|
}
|
|
actions += ` <button onclick="openDetailModal(${r.request_id})" class="text-gray-400 hover:text-gray-600 text-xs ml-1" title="상세"><i class="fas fa-eye"></i></button>`;
|
|
if (r.status === 'pending') {
|
|
actions += ` <button onclick="doDeleteRequest(${r.request_id})" class="text-gray-400 hover:text-red-500 text-xs ml-1" title="삭제"><i class="fas fa-trash"></i></button>`;
|
|
}
|
|
|
|
return `<tr>
|
|
<td>${formatDate(r.created_at)}</td>
|
|
<td>${escapeHtml(r.requester_full_name || r.requester_name || '-')}</td>
|
|
<td>${escapeHtml(r.visitor_company)}</td>
|
|
<td class="text-center">${r.visitor_count}</td>
|
|
<td>${escapeHtml(r.workplace_name || '-')}</td>
|
|
<td>${formatDate(r.visit_date)}</td>
|
|
<td class="hide-mobile">${r.visit_time ? String(r.visit_time).substring(0, 5) : '-'}</td>
|
|
<td>${escapeHtml(r.purpose_name || '-')}</td>
|
|
<td>${vrStatusBadge(r.status)}</td>
|
|
<td class="text-right whitespace-nowrap">${actions}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
/* ===== Approve Modal ===== */
|
|
function openApproveModal(id) {
|
|
const r = allRequests.find(x => x.request_id === id);
|
|
if (!r) return;
|
|
actionRequestId = id;
|
|
document.getElementById('approveDetail').innerHTML = `
|
|
<p><strong>업체:</strong> ${escapeHtml(r.visitor_company)}</p>
|
|
<p><strong>방문일:</strong> ${formatDate(r.visit_date)} ${r.visit_time ? String(r.visit_time).substring(0, 5) : ''}</p>
|
|
<p><strong>작업장:</strong> ${escapeHtml(r.workplace_name || '-')}</p>
|
|
<p><strong>인원:</strong> ${r.visitor_count}명</p>
|
|
<p class="mt-2">이 출입 신청을 승인하시겠습니까?</p>
|
|
`;
|
|
document.getElementById('approveModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeApproveModal() {
|
|
document.getElementById('approveModal').classList.add('hidden');
|
|
actionRequestId = null;
|
|
}
|
|
|
|
async function confirmApprove() {
|
|
if (!actionRequestId) return;
|
|
try {
|
|
await api('/visit-requests/requests/' + actionRequestId + '/approve', {
|
|
method: 'PUT', body: JSON.stringify({})
|
|
});
|
|
showToast('승인되었습니다');
|
|
closeApproveModal();
|
|
await loadRequests();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
}
|
|
|
|
/* ===== Reject Modal ===== */
|
|
function openRejectModal(id) {
|
|
const r = allRequests.find(x => x.request_id === id);
|
|
if (!r) return;
|
|
actionRequestId = id;
|
|
document.getElementById('rejectDetail').innerHTML = `
|
|
<p><strong>업체:</strong> ${escapeHtml(r.visitor_company)}</p>
|
|
<p><strong>방문일:</strong> ${formatDate(r.visit_date)} ${r.visit_time ? String(r.visit_time).substring(0, 5) : ''}</p>
|
|
<p><strong>작업장:</strong> ${escapeHtml(r.workplace_name || '-')}</p>
|
|
`;
|
|
document.getElementById('rejectionReason').value = '';
|
|
document.getElementById('rejectModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeRejectModal() {
|
|
document.getElementById('rejectModal').classList.add('hidden');
|
|
actionRequestId = null;
|
|
}
|
|
|
|
async function confirmReject() {
|
|
if (!actionRequestId) return;
|
|
const reason = document.getElementById('rejectionReason').value.trim();
|
|
if (!reason) {
|
|
showToast('반려 사유를 입력해주세요', 'error');
|
|
return;
|
|
}
|
|
try {
|
|
await api('/visit-requests/requests/' + actionRequestId + '/reject', {
|
|
method: 'PUT', body: JSON.stringify({ rejection_reason: reason })
|
|
});
|
|
showToast('반려되었습니다');
|
|
closeRejectModal();
|
|
await loadRequests();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
}
|
|
|
|
/* ===== Detail Modal ===== */
|
|
function openDetailModal(id) {
|
|
const r = allRequests.find(x => x.request_id === id);
|
|
if (!r) return;
|
|
document.getElementById('detailContent').innerHTML = `
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div><span class="text-gray-500">신청자:</span> <span class="font-medium">${escapeHtml(r.requester_full_name || r.requester_name || '-')}</span></div>
|
|
<div><span class="text-gray-500">업체:</span> <span class="font-medium">${escapeHtml(r.visitor_company)}</span></div>
|
|
<div><span class="text-gray-500">인원:</span> <span class="font-medium">${r.visitor_count}명</span></div>
|
|
<div><span class="text-gray-500">분류:</span> <span class="font-medium">${escapeHtml(r.category_name || '-')}</span></div>
|
|
<div><span class="text-gray-500">작업장:</span> <span class="font-medium">${escapeHtml(r.workplace_name || '-')}</span></div>
|
|
<div><span class="text-gray-500">방문일:</span> <span class="font-medium">${formatDate(r.visit_date)}</span></div>
|
|
<div><span class="text-gray-500">방문시간:</span> <span class="font-medium">${r.visit_time ? String(r.visit_time).substring(0, 5) : '-'}</span></div>
|
|
<div><span class="text-gray-500">목적:</span> <span class="font-medium">${escapeHtml(r.purpose_name || '-')}</span></div>
|
|
<div><span class="text-gray-500">상태:</span> ${vrStatusBadge(r.status)}</div>
|
|
<div><span class="text-gray-500">신청일:</span> <span class="font-medium">${formatDateTime(r.created_at)}</span></div>
|
|
${r.approver_name ? `<div><span class="text-gray-500">처리자:</span> <span class="font-medium">${escapeHtml(r.approver_name)}</span></div>` : ''}
|
|
${r.approved_at ? `<div><span class="text-gray-500">처리일:</span> <span class="font-medium">${formatDateTime(r.approved_at)}</span></div>` : ''}
|
|
${r.rejection_reason ? `<div class="col-span-2"><span class="text-gray-500">반려사유:</span> <span class="font-medium text-red-600">${escapeHtml(r.rejection_reason)}</span></div>` : ''}
|
|
${r.notes ? `<div class="col-span-2"><span class="text-gray-500">비고:</span> <span class="font-medium">${escapeHtml(r.notes)}</span></div>` : ''}
|
|
</div>
|
|
`;
|
|
document.getElementById('detailModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeDetailModal() {
|
|
document.getElementById('detailModal').classList.add('hidden');
|
|
}
|
|
|
|
/* ===== Delete request ===== */
|
|
async function doDeleteRequest(id) {
|
|
if (!confirm('이 신청을 삭제하시겠습니까?')) return;
|
|
try {
|
|
await api('/visit-requests/requests/' + id, { method: 'DELETE' });
|
|
showToast('삭제되었습니다');
|
|
await loadRequests();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
}
|
|
|
|
/* ===== Init ===== */
|
|
function initVisitManagementPage() {
|
|
if (!initAuth()) return;
|
|
|
|
// Check admin
|
|
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role);
|
|
if (!isAdmin) {
|
|
document.querySelector('.flex-1.min-w-0').innerHTML = `
|
|
<div class="bg-white rounded-xl shadow-sm p-10 text-center">
|
|
<i class="fas fa-lock text-4xl text-gray-300 mb-4"></i>
|
|
<p class="text-gray-500">관리자 권한이 필요합니다</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
document.getElementById('filterStatus').addEventListener('change', loadRequests);
|
|
document.getElementById('filterDateFrom').addEventListener('change', loadRequests);
|
|
document.getElementById('filterDateTo').addEventListener('change', loadRequests);
|
|
|
|
loadRequests();
|
|
}
|