/* ===== Training Management (안전교육 실시 - 관리자) ===== */
let pendingRequests = [];
let completedTrainings = [];
let trainingRequestId = null;
/* Signature canvas state */
let sigCanvas, sigCtx;
let isDrawing = false;
let hasSignature = false;
/* ===== Load approved requests needing training ===== */
async function loadPendingTraining() {
try {
const res = await api('/visit-requests/requests?status=approved');
pendingRequests = res.data || [];
renderPendingTraining();
} catch (e) {
showToast('대기 목록 로드 실패: ' + e.message, 'error');
}
}
function renderPendingTraining() {
const tbody = document.getElementById('pendingTrainingBody');
if (!pendingRequests.length) {
tbody.innerHTML = '
| 교육 대기 중인 신청이 없습니다 |
';
return;
}
tbody.innerHTML = pendingRequests.map(r => `
| ${escapeHtml(r.visitor_company)} |
${r.visitor_count} |
${escapeHtml(r.workplace_name || '-')} |
${formatDate(r.visit_date)} |
${r.visit_time ? String(r.visit_time).substring(0, 5) : '-'} |
${escapeHtml(r.purpose_name || '-')} |
|
`).join('');
}
/* ===== Load completed training records ===== */
async function loadCompletedTraining() {
try {
const res = await api('/visit-requests/training');
completedTrainings = res.data || [];
renderCompletedTraining();
} catch (e) {
showToast('이력 로드 실패: ' + e.message, 'error');
}
}
function renderCompletedTraining() {
const tbody = document.getElementById('completedTrainingBody');
if (!completedTrainings.length) {
tbody.innerHTML = '| 교육 완료 이력이 없습니다 |
';
return;
}
tbody.innerHTML = completedTrainings.map(t => {
const timeRange = t.training_start_time
? String(t.training_start_time).substring(0, 5) + (t.training_end_time ? ' ~ ' + String(t.training_end_time).substring(0, 5) : '')
: '-';
return `
| ${formatDate(t.training_date)} |
${escapeHtml(t.visitor_company || '-')} |
${t.visitor_count || '-'} |
${timeRange} |
${escapeHtml(t.training_topics || '-')} |
${escapeHtml(t.trainer_full_name || t.trainer_name || '-')} |
${t.completed_at ? '완료' : '진행중'}
|
`;
}).join('');
}
/* ===== Training Modal ===== */
function openTrainingModal(requestId) {
const r = pendingRequests.find(x => x.request_id === requestId);
if (!r) return;
trainingRequestId = requestId;
document.getElementById('trainingRequestInfo').innerHTML = `
업체: ${escapeHtml(r.visitor_company)}
인원: ${r.visitor_count}명
작업장: ${escapeHtml(r.workplace_name || '-')}
방문일: ${formatDate(r.visit_date)}
`;
// Set defaults
const today = new Date().toISOString().substring(0, 10);
const now = new Date().toTimeString().substring(0, 5);
document.getElementById('trainingDate').value = today;
document.getElementById('trainingStartTime').value = now;
document.getElementById('trainingEndTime').value = '';
document.getElementById('trainingTopics').value = '';
// Reset signature
clearSignature();
document.getElementById('trainingModal').classList.remove('hidden');
}
function closeTrainingModal() {
document.getElementById('trainingModal').classList.add('hidden');
trainingRequestId = null;
}
/* ===== Submit Training ===== */
async function submitTraining(e) {
e.preventDefault();
if (!trainingRequestId) return;
const data = {
request_id: trainingRequestId,
training_date: document.getElementById('trainingDate').value,
training_start_time: document.getElementById('trainingStartTime').value,
training_end_time: document.getElementById('trainingEndTime').value || null,
training_topics: document.getElementById('trainingTopics').value.trim() || null
};
if (!data.training_date) { showToast('교육일을 선택해주세요', 'error'); return; }
if (!data.training_start_time) { showToast('시작시간을 입력해주세요', 'error'); return; }
try {
// 1. Create training record
const createRes = await api('/visit-requests/training', {
method: 'POST', body: JSON.stringify(data)
});
const trainingId = createRes.data?.training_id || createRes.data?.insertId;
// 2. Complete with signature if exists
if (trainingId && hasSignature) {
const signatureData = sigCanvas.toDataURL('image/png');
await api('/visit-requests/training/' + trainingId + '/complete', {
method: 'PUT', body: JSON.stringify({ signature_data: signatureData })
});
}
showToast('안전교육이 완료되었습니다');
closeTrainingModal();
await Promise.all([loadPendingTraining(), loadCompletedTraining()]);
} catch (e) {
showToast(e.message, 'error');
}
}
/* ===== Signature Pad ===== */
function initSignaturePad() {
sigCanvas = document.getElementById('signatureCanvas');
if (!sigCanvas) return;
sigCtx = sigCanvas.getContext('2d');
// Adjust canvas resolution for retina displays
const rect = sigCanvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
sigCanvas.width = rect.width * dpr;
sigCanvas.height = rect.height * dpr;
sigCtx.scale(dpr, dpr);
sigCtx.lineCap = 'round';
sigCtx.lineJoin = 'round';
sigCtx.lineWidth = 2;
sigCtx.strokeStyle = '#1f2937';
// Mouse events
sigCanvas.addEventListener('mousedown', startDraw);
sigCanvas.addEventListener('mousemove', draw);
sigCanvas.addEventListener('mouseup', stopDraw);
sigCanvas.addEventListener('mouseleave', stopDraw);
// Touch events
sigCanvas.addEventListener('touchstart', function(e) {
e.preventDefault();
const touch = e.touches[0];
startDraw(touchToMouse(touch));
});
sigCanvas.addEventListener('touchmove', function(e) {
e.preventDefault();
const touch = e.touches[0];
draw(touchToMouse(touch));
});
sigCanvas.addEventListener('touchend', function(e) {
e.preventDefault();
stopDraw();
});
}
function touchToMouse(touch) {
const rect = sigCanvas.getBoundingClientRect();
return { offsetX: touch.clientX - rect.left, offsetY: touch.clientY - rect.top };
}
function startDraw(e) {
isDrawing = true;
sigCtx.beginPath();
sigCtx.moveTo(e.offsetX, e.offsetY);
}
function draw(e) {
if (!isDrawing) return;
hasSignature = true;
sigCtx.lineTo(e.offsetX, e.offsetY);
sigCtx.stroke();
}
function stopDraw() {
isDrawing = false;
}
function clearSignature() {
if (!sigCanvas || !sigCtx) return;
const rect = sigCanvas.getBoundingClientRect();
sigCtx.clearRect(0, 0, rect.width, rect.height);
hasSignature = false;
}
/* ===== 대기 목록: 출입 신청 수정/삭제 ===== */
function openEditRequest(requestId) {
const r = pendingRequests.find(x => x.request_id === requestId);
if (!r) return;
document.getElementById('editRequestId').value = requestId;
document.getElementById('editReqCompany').value = r.visitor_company || '';
document.getElementById('editReqCount').value = r.visitor_count || 1;
document.getElementById('editReqDate').value = r.visit_date ? r.visit_date.substring(0, 10) : '';
document.getElementById('editReqTime').value = r.visit_time ? String(r.visit_time).substring(0, 5) : '';
document.getElementById('editReqNotes').value = r.notes || '';
document.getElementById('editRequestModal').classList.remove('hidden');
}
function closeEditRequestModal() {
document.getElementById('editRequestModal').classList.add('hidden');
}
async function submitEditRequest(e) {
e.preventDefault();
const id = document.getElementById('editRequestId').value;
const r = pendingRequests.find(x => x.request_id === parseInt(id));
if (!r) return;
const data = {
visitor_company: document.getElementById('editReqCompany').value,
visitor_count: parseInt(document.getElementById('editReqCount').value),
category_id: r.category_id,
workplace_id: r.workplace_id,
visit_date: document.getElementById('editReqDate').value,
visit_time: document.getElementById('editReqTime').value,
purpose_id: r.purpose_id,
notes: document.getElementById('editReqNotes').value || null
};
try {
await api('/visit-requests/requests/' + id, {
method: 'PUT', body: JSON.stringify(data)
});
showToast('출입 신청이 수정되었습니다');
closeEditRequestModal();
await loadPendingTraining();
} catch (e) {
showToast(e.message, 'error');
}
}
async function doDeleteRequest(requestId) {
if (!confirm('이 출입 신청을 삭제하시겠습니까?')) return;
try {
await api('/visit-requests/requests/' + requestId, { method: 'DELETE' });
showToast('출입 신청이 삭제되었습니다');
await loadPendingTraining();
} catch (e) {
showToast(e.message, 'error');
}
}
/* ===== 완료 이력: 교육 기록 수정/삭제 ===== */
function openEditTraining(trainingId) {
const t = completedTrainings.find(x => x.training_id === trainingId);
if (!t) return;
document.getElementById('editTrainingId').value = trainingId;
document.getElementById('editTrainDate').value = t.training_date ? t.training_date.substring(0, 10) : '';
document.getElementById('editTrainStartTime').value = t.training_start_time ? String(t.training_start_time).substring(0, 5) : '';
document.getElementById('editTrainEndTime').value = t.training_end_time ? String(t.training_end_time).substring(0, 5) : '';
document.getElementById('editTrainTopics').value = t.training_topics || '';
document.getElementById('editTrainingModal').classList.remove('hidden');
}
function closeEditTrainingModal() {
document.getElementById('editTrainingModal').classList.add('hidden');
}
async function submitEditTraining(e) {
e.preventDefault();
const id = document.getElementById('editTrainingId').value;
const data = {
training_date: document.getElementById('editTrainDate').value,
training_start_time: document.getElementById('editTrainStartTime').value,
training_end_time: document.getElementById('editTrainEndTime').value || null,
training_topics: document.getElementById('editTrainTopics').value || null
};
try {
await api('/visit-requests/training/' + id, {
method: 'PUT', body: JSON.stringify(data)
});
showToast('교육 기록이 수정되었습니다');
closeEditTrainingModal();
await loadCompletedTraining();
} catch (e) {
showToast(e.message, 'error');
}
}
async function doDeleteTraining(trainingId) {
if (!confirm('이 교육 기록을 삭제하시겠습니까?\n삭제 시 해당 출입 신청이 대기 목록으로 복원됩니다.')) return;
try {
await api('/visit-requests/training/' + trainingId, { method: 'DELETE' });
showToast('교육 기록이 삭제되었습니다');
await Promise.all([loadPendingTraining(), loadCompletedTraining()]);
} catch (e) {
showToast(e.message, 'error');
}
}
/* ===== Init ===== */
function initTrainingPage() {
if (!initAuth()) return;
// Check admin
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role);
if (!isAdmin) {
document.querySelector('.flex-1.min-w-0').innerHTML = `
`;
return;
}
document.getElementById('trainingForm').addEventListener('submit', submitTraining);
document.getElementById('editRequestForm').addEventListener('submit', submitEditRequest);
document.getElementById('editTrainingForm').addEventListener('submit', submitEditTraining);
initSignaturePad();
loadPendingTraining();
loadCompletedTraining();
}