- api-base.js에 4개 전역 유틸리티 추가 (showToast, formatDate, waitForApi, generateUUID) - 24개 파일에서 중복 정의 제거 (-932줄) - showToast: 18곳 중복 → 1곳 통합 (자동 컨테이너/스타일 생성) - waitForApi/waitForApiConfig/waitForApiCall: 5곳 → 1곳 통합 - generateUUID: tbm.js 중복 제거 - tbm/utils.js, workplace-management/utils.js: window 재정의 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
369 lines
11 KiB
JavaScript
369 lines
11 KiB
JavaScript
// 안전관리 대시보드 JavaScript
|
|
|
|
let currentStatus = 'pending';
|
|
let requests = [];
|
|
let currentRejectRequestId = null;
|
|
|
|
// showToast → api-base.js 전역 사용
|
|
|
|
// ==================== 초기화 ====================
|
|
|
|
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 = `
|
|
<div class="empty-state">
|
|
<div style="font-size: 48px; margin-bottom: 16px;">📭</div>
|
|
<h3>출입 신청이 없습니다</h3>
|
|
<p>현재 ${getStatusText(currentStatus)} 상태의 신청이 없습니다.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<table class="request-table">
|
|
<thead>
|
|
<tr>
|
|
<th>신청일</th>
|
|
<th>신청자</th>
|
|
<th>방문자</th>
|
|
<th>인원</th>
|
|
<th>방문 작업장</th>
|
|
<th>방문 일시</th>
|
|
<th>목적</th>
|
|
<th>상태</th>
|
|
<th>작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
requests.forEach(req => {
|
|
const statusText = {
|
|
'pending': '승인 대기',
|
|
'approved': '승인됨',
|
|
'rejected': '반려됨',
|
|
'training_completed': '교육 완료'
|
|
}[req.status] || req.status;
|
|
|
|
html += `
|
|
<tr>
|
|
<td>${new Date(req.created_at).toLocaleDateString()}</td>
|
|
<td>${req.requester_full_name || req.requester_name}</td>
|
|
<td>${req.visitor_company}</td>
|
|
<td>${req.visitor_count}명</td>
|
|
<td>${req.category_name} - ${req.workplace_name}</td>
|
|
<td>${req.visit_date} ${req.visit_time}</td>
|
|
<td>${req.purpose_name}</td>
|
|
<td><span class="status-badge ${req.status}">${statusText}</span></td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm btn-secondary" onclick="viewDetail(${req.request_id})">상세</button>
|
|
${req.status === 'pending' ? `
|
|
<button class="btn btn-sm btn-primary" onclick="approveRequest(${req.request_id})">승인</button>
|
|
<button class="btn btn-sm btn-danger" onclick="openRejectModal(${req.request_id})">반려</button>
|
|
` : ''}
|
|
${req.status === 'approved' ? `
|
|
<button class="btn btn-sm btn-primary" onclick="startTraining(${req.request_id})">교육 진행</button>
|
|
` : ''}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
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 = `
|
|
<div class="detail-grid">
|
|
<div class="detail-label">신청 번호</div>
|
|
<div class="detail-value">#${req.request_id}</div>
|
|
|
|
<div class="detail-label">신청일</div>
|
|
<div class="detail-value">${new Date(req.created_at).toLocaleString()}</div>
|
|
|
|
<div class="detail-label">신청자</div>
|
|
<div class="detail-value">${req.requester_full_name || req.requester_name}</div>
|
|
|
|
<div class="detail-label">방문자 소속</div>
|
|
<div class="detail-value">${req.visitor_company}</div>
|
|
|
|
<div class="detail-label">방문 인원</div>
|
|
<div class="detail-value">${req.visitor_count}명</div>
|
|
|
|
<div class="detail-label">방문 구역</div>
|
|
<div class="detail-value">${req.category_name}</div>
|
|
|
|
<div class="detail-label">방문 작업장</div>
|
|
<div class="detail-value">${req.workplace_name}</div>
|
|
|
|
<div class="detail-label">방문 날짜</div>
|
|
<div class="detail-value">${req.visit_date}</div>
|
|
|
|
<div class="detail-label">방문 시간</div>
|
|
<div class="detail-value">${req.visit_time}</div>
|
|
|
|
<div class="detail-label">방문 목적</div>
|
|
<div class="detail-value">${req.purpose_name}</div>
|
|
|
|
<div class="detail-label">상태</div>
|
|
<div class="detail-value"><span class="status-badge ${req.status}">${statusText}</span></div>
|
|
</div>
|
|
`;
|
|
|
|
if (req.notes) {
|
|
html += `
|
|
<div style="margin-top: 16px; padding: 12px; background: var(--gray-50); border-radius: var(--radius-md);">
|
|
<strong>비고:</strong><br>
|
|
${req.notes}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (req.rejection_reason) {
|
|
html += `
|
|
<div style="margin-top: 16px; padding: 12px; background: var(--red-50); border-radius: var(--radius-md); color: var(--red-700);">
|
|
<strong>반려 사유:</strong><br>
|
|
${req.rejection_reason}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (req.approved_by) {
|
|
html += `
|
|
<div style="margin-top: 16px; padding: 12px; background: var(--blue-50); border-radius: var(--radius-md);">
|
|
<strong>처리 정보:</strong><br>
|
|
처리자: ${req.approver_name || 'Unknown'}<br>
|
|
처리 시간: ${new Date(req.approved_at).toLocaleString()}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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.switchTab = switchTab;
|
|
window.viewDetail = viewDetail;
|
|
window.closeDetailModal = closeDetailModal;
|
|
window.approveRequest = approveRequest;
|
|
window.openRejectModal = openRejectModal;
|
|
window.closeRejectModal = closeRejectModal;
|
|
window.confirmReject = confirmReject;
|
|
window.startTraining = startTraining;
|