- DB 마이그레이션: request_type, visitor_name, department_id, check_in/out_time 컬럼 + status ENUM 확장 - 4소스 UNION 대시보드: 방문(외부/내부) + TBM + 협력업체 통합 조회 - 체크인/체크아웃 API + 내부 출입 신고(승인 불필요) 지원 - 통합 출입 현황판 페이지 신규 (entry-dashboard.html) - 출입 신청/관리 페이지에 유형 필터 + 체크인/아웃 버튼 추가 - safety_entry_dashboard 권한 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
4.9 KiB
JavaScript
137 lines
4.9 KiB
JavaScript
/* ===== Entry Dashboard (출입 현황판) ===== */
|
|
let dashboardData = [];
|
|
let currentSourceFilter = '';
|
|
let refreshTimer = null;
|
|
|
|
/* ===== Source/Status badges ===== */
|
|
function sourceBadge(s) {
|
|
const m = {
|
|
tbm: ['badge-blue', 'TBM'],
|
|
partner: ['badge-green', '협력업체'],
|
|
visit: ['badge-amber', '방문']
|
|
};
|
|
const [cls, label] = m[s] || ['badge-gray', s];
|
|
return `<span class="badge ${cls}">${label}</span>`;
|
|
}
|
|
|
|
function entryStatusBadge(s) {
|
|
const m = {
|
|
checked_in: ['badge-blue', '체크인'],
|
|
checked_out: ['badge-gray', '체크아웃'],
|
|
approved: ['badge-green', '승인'],
|
|
training_completed: ['badge-blue', '교육완료'],
|
|
absent: ['badge-red', '불참']
|
|
};
|
|
const [cls, label] = m[s] || ['badge-gray', s];
|
|
return `<span class="badge ${cls}">${label}</span>`;
|
|
}
|
|
|
|
/* ===== Load dashboard data ===== */
|
|
async function loadDashboard() {
|
|
const date = document.getElementById('dashboardDate').value;
|
|
try {
|
|
const [dashRes, statsRes] = await Promise.all([
|
|
api('/visit-requests/entry-dashboard?date=' + date),
|
|
api('/visit-requests/entry-dashboard/stats?date=' + date)
|
|
]);
|
|
dashboardData = dashRes.data || [];
|
|
updateStats(statsRes.data || {});
|
|
renderDashboard();
|
|
} catch (e) {
|
|
showToast('대시보드 로드 실패: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
function updateStats(stats) {
|
|
const total = stats.external_visit + stats.internal_visit + stats.partner + stats.tbm;
|
|
document.getElementById('statTotal').textContent = total;
|
|
document.getElementById('statTbm').textContent = stats.tbm;
|
|
document.getElementById('statPartner').textContent = stats.partner;
|
|
document.getElementById('statExternal').textContent = stats.external_visit;
|
|
document.getElementById('statInternal').textContent = stats.internal_visit;
|
|
}
|
|
|
|
/* ===== Render table ===== */
|
|
function renderDashboard() {
|
|
const tbody = document.getElementById('dashboardBody');
|
|
const filtered = currentSourceFilter
|
|
? dashboardData.filter(r => r.source === currentSourceFilter)
|
|
: dashboardData;
|
|
|
|
if (!filtered.length) {
|
|
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-400 py-8">데이터가 없습니다</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = filtered.map(r => {
|
|
const name = escapeHtml(r.visitor_name || '-');
|
|
const org = escapeHtml(r.visitor_company || '-');
|
|
const workplace = escapeHtml(r.workplace_name || '-');
|
|
const inTime = r.check_in_time ? String(r.check_in_time).substring(11, 16) : (r.entry_time ? String(r.entry_time).substring(0, 5) : '-');
|
|
const outTime = r.check_out_time ? String(r.check_out_time).substring(11, 16) : '-';
|
|
const purpose = escapeHtml(r.purpose_name || '-');
|
|
const note = r.source_note ? `<span class="text-xs text-gray-500 italic">${escapeHtml(r.source_note)}</span>` : '';
|
|
const count = r.visitor_count > 1 ? ` <span class="text-xs text-gray-400">(${r.visitor_count}명)</span>` : '';
|
|
|
|
return `<tr>
|
|
<td>${sourceBadge(r.source)}</td>
|
|
<td>${name}${count}</td>
|
|
<td>${org}</td>
|
|
<td>${workplace}</td>
|
|
<td>${inTime}</td>
|
|
<td>${outTime}</td>
|
|
<td>${purpose}</td>
|
|
<td>${entryStatusBadge(r.status)}</td>
|
|
<td class="hide-mobile">${note}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
/* ===== Tab filter ===== */
|
|
function filterSource(source) {
|
|
currentSourceFilter = source;
|
|
document.querySelectorAll('.source-tab').forEach(t => {
|
|
t.classList.toggle('active', t.dataset.source === source);
|
|
});
|
|
renderDashboard();
|
|
}
|
|
|
|
/* ===== Auto refresh ===== */
|
|
function setupAutoRefresh() {
|
|
const cb = document.getElementById('autoRefresh');
|
|
cb.addEventListener('change', () => {
|
|
if (cb.checked) startAutoRefresh();
|
|
else stopAutoRefresh();
|
|
});
|
|
startAutoRefresh();
|
|
}
|
|
|
|
function startAutoRefresh() {
|
|
stopAutoRefresh();
|
|
refreshTimer = setInterval(loadDashboard, 180000); // 3분
|
|
}
|
|
|
|
function stopAutoRefresh() {
|
|
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
}
|
|
|
|
/* ===== Init ===== */
|
|
function initEntryDashboard() {
|
|
if (!initAuth()) return;
|
|
|
|
const today = new Date().toISOString().substring(0, 10);
|
|
document.getElementById('dashboardDate').value = today;
|
|
|
|
document.getElementById('dashboardDate').addEventListener('change', loadDashboard);
|
|
|
|
// Source tab styling
|
|
const style = document.createElement('style');
|
|
style.textContent = `.source-tab { border-bottom-color: transparent; color: #6b7280; cursor: pointer; }
|
|
.source-tab:hover { color: #374151; background: #f9fafb; }
|
|
.source-tab.active { border-bottom-color: #2563eb; color: #2563eb; font-weight: 600; }`;
|
|
document.head.appendChild(style);
|
|
|
|
loadDashboard();
|
|
setupAutoRefresh();
|
|
}
|