// 안전관리 대시보드 JavaScript let currentStatus = 'pending'; let requests = []; let currentRejectRequestId = null; // ==================== Toast 알림 ==================== function showToast(message, type = 'info', duration = 3000) { const toastContainer = document.getElementById('toastContainer') || createToastContainer(); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const iconMap = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; toast.innerHTML = ` ${iconMap[type] || 'ℹ️'} ${message} `; toastContainer.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } function createToastContainer() { const container = document.createElement('div'); container.id = 'toastContainer'; container.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 10px; `; document.body.appendChild(container); if (!document.getElementById('toastStyles')) { const style = document.createElement('style'); style.id = 'toastStyles'; style.textContent = ` .toast { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); opacity: 0; transform: translateX(100px); transition: all 0.3s ease; min-width: 250px; max-width: 400px; } .toast.show { opacity: 1; transform: translateX(0); } .toast-success { border-left: 4px solid #10b981; } .toast-error { border-left: 4px solid #ef4444; } .toast-warning { border-left: 4px solid #f59e0b; } .toast-info { border-left: 4px solid #3b82f6; } .toast-icon { font-size: 20px; } .toast-message { font-size: 14px; color: #374151; } `; document.head.appendChild(style); } return container; } // ==================== 초기화 ==================== document.addEventListener('DOMContentLoaded', async () => { await loadRequests(); updateStats(); }); // ==================== 데이터 로드 ==================== /** * 출입 신청 목록 로드 */ async function loadRequests() { try { const filters = currentStatus === 'all' ? {} : { status: currentStatus }; const queryString = new URLSearchParams(filters).toString(); const response = await window.apiCall(`/workplace-visits/requests?${queryString}`, 'GET'); if (response && response.success) { requests = response.data || []; renderRequestTable(); updateStats(); } } catch (error) { console.error('출입 신청 목록 로드 오류:', error); showToast('출입 신청 목록을 불러오는데 실패했습니다.', 'error'); } } /** * 통계 업데이트 */ async function updateStats() { try { const response = await window.apiCall('/workplace-visits/requests', 'GET'); if (response && response.success) { const allRequests = response.data || []; const stats = { pending: allRequests.filter(r => r.status === 'pending').length, approved: allRequests.filter(r => r.status === 'approved').length, training_completed: allRequests.filter(r => r.status === 'training_completed').length, rejected: allRequests.filter(r => r.status === 'rejected').length }; document.getElementById('statPending').textContent = stats.pending; document.getElementById('statApproved').textContent = stats.approved; document.getElementById('statTrainingCompleted').textContent = stats.training_completed; document.getElementById('statRejected').textContent = stats.rejected; } } catch (error) { console.error('통계 업데이트 오류:', error); } } /** * 테이블 렌더링 */ function renderRequestTable() { const container = document.getElementById('requestTableContainer'); if (requests.length === 0) { container.innerHTML = `
📭

출입 신청이 없습니다

현재 ${getStatusText(currentStatus)} 상태의 신청이 없습니다.

`; return; } let html = ` `; requests.forEach(req => { const statusText = { 'pending': '승인 대기', 'approved': '승인됨', 'rejected': '반려됨', 'training_completed': '교육 완료' }[req.status] || req.status; html += ` `; }); html += `
신청일 신청자 방문자 인원 방문 작업장 방문 일시 목적 상태 작업
${new Date(req.created_at).toLocaleDateString()} ${req.requester_full_name || req.requester_name} ${req.visitor_company} ${req.visitor_count}명 ${req.category_name} - ${req.workplace_name} ${req.visit_date} ${req.visit_time} ${req.purpose_name} ${statusText}
${req.status === 'pending' ? ` ` : ''} ${req.status === 'approved' ? ` ` : ''}
`; container.innerHTML = html; } /** * 상태 텍스트 변환 */ function getStatusText(status) { const map = { 'pending': '승인 대기', 'approved': '승인 완료', 'rejected': '반려', 'training_completed': '교육 완료', 'all': '전체' }; return map[status] || status; } // ==================== 탭 전환 ==================== /** * 탭 전환 */ async function switchTab(status) { currentStatus = status; // 탭 활성화 상태 변경 document.querySelectorAll('.status-tab').forEach(tab => { if (tab.dataset.status === status) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); await loadRequests(); } // ==================== 상세보기 ==================== /** * 상세보기 모달 열기 */ async function viewDetail(requestId) { try { const response = await window.apiCall(`/workplace-visits/requests/${requestId}`, 'GET'); if (response && response.success) { const req = response.data; const statusText = { 'pending': '승인 대기', 'approved': '승인됨', 'rejected': '반려됨', 'training_completed': '교육 완료' }[req.status] || req.status; let html = `
신청 번호
#${req.request_id}
신청일
${new Date(req.created_at).toLocaleString()}
신청자
${req.requester_full_name || req.requester_name}
방문자 소속
${req.visitor_company}
방문 인원
${req.visitor_count}명
방문 구역
${req.category_name}
방문 작업장
${req.workplace_name}
방문 날짜
${req.visit_date}
방문 시간
${req.visit_time}
방문 목적
${req.purpose_name}
상태
${statusText}
`; if (req.notes) { html += `
비고:
${req.notes}
`; } if (req.rejection_reason) { html += `
반려 사유:
${req.rejection_reason}
`; } if (req.approved_by) { html += `
처리 정보:
처리자: ${req.approver_name || 'Unknown'}
처리 시간: ${new Date(req.approved_at).toLocaleString()}
`; } document.getElementById('detailContent').innerHTML = html; document.getElementById('detailModal').style.display = 'flex'; } } catch (error) { console.error('상세 정보 로드 오류:', error); showToast('상세 정보를 불러오는데 실패했습니다.', 'error'); } } /** * 상세보기 모달 닫기 */ function closeDetailModal() { document.getElementById('detailModal').style.display = 'none'; } // ==================== 승인/반려 ==================== /** * 승인 처리 */ async function approveRequest(requestId) { if (!confirm('이 출입 신청을 승인하시겠습니까?')) { return; } try { const response = await window.apiCall(`/workplace-visits/requests/${requestId}/approve`, 'PUT'); if (response && response.success) { showToast('출입 신청이 승인되었습니다.', 'success'); await loadRequests(); updateStats(); } else { throw new Error(response?.message || '승인 실패'); } } catch (error) { console.error('승인 처리 오류:', error); showToast(error.message || '승인 처리 중 오류가 발생했습니다.', 'error'); } } /** * 반려 모달 열기 */ function openRejectModal(requestId) { currentRejectRequestId = requestId; document.getElementById('rejectionReason').value = ''; document.getElementById('rejectModal').style.display = 'flex'; } /** * 반려 모달 닫기 */ function closeRejectModal() { currentRejectRequestId = null; document.getElementById('rejectModal').style.display = 'none'; } /** * 반려 확정 */ async function confirmReject() { const reason = document.getElementById('rejectionReason').value.trim(); if (!reason) { showToast('반려 사유를 입력해주세요.', 'warning'); return; } try { const response = await window.apiCall( `/workplace-visits/requests/${currentRejectRequestId}/reject`, 'PUT', { rejection_reason: reason } ); if (response && response.success) { showToast('출입 신청이 반려되었습니다.', 'success'); closeRejectModal(); await loadRequests(); updateStats(); } else { throw new Error(response?.message || '반려 실패'); } } catch (error) { console.error('반려 처리 오류:', error); showToast(error.message || '반려 처리 중 오류가 발생했습니다.', 'error'); } } // ==================== 안전교육 진행 ==================== /** * 안전교육 진행 페이지로 이동 */ function startTraining(requestId) { window.location.href = `/pages/safety/training-conduct.html?request_id=${requestId}`; } // 전역 함수로 노출 window.showToast = showToast; window.switchTab = switchTab; window.viewDetail = viewDetail; window.closeDetailModal = closeDetailModal; window.approveRequest = approveRequest; window.openRejectModal = openRejectModal; window.closeRejectModal = closeRejectModal; window.confirmReject = confirmReject; window.startTraining = startTraining;