feat(training): 안전교육 실시 페이지 수정/삭제 기능 추가
대기 목록·완료 이력 양쪽에 수정/삭제 버튼 추가. 교육 기록 삭제 시 트랜잭션으로 출입 신청 상태를 approved로 복원. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -280,6 +280,19 @@ exports.updateTrainingRecord = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.deleteTrainingRecord = async (req, res) => {
|
||||
try {
|
||||
const result = await visitRequestModel.deleteTrainingRecord(req.params.id);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
|
||||
}
|
||||
res.json({ success: true, message: '안전교육 기록이 삭제되었습니다.' });
|
||||
} catch (err) {
|
||||
console.error('안전교육 기록 삭제 오류:', err);
|
||||
res.status(500).json({ success: false, message: '안전교육 기록 삭제 중 오류가 발생했습니다.' });
|
||||
}
|
||||
};
|
||||
|
||||
exports.completeTraining = async (req, res) => {
|
||||
try {
|
||||
const trainingId = req.params.id;
|
||||
|
||||
@@ -350,6 +350,42 @@ const completeTraining = async (trainingId, signatureData) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const deleteTrainingRecord = async (trainingId) => {
|
||||
const db = getPool();
|
||||
const connection = await db.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
|
||||
// 1. training record 조회 → request_id 획득
|
||||
const [rows] = await connection.query(
|
||||
'SELECT training_id, request_id FROM safety_training_records WHERE training_id = ?',
|
||||
[trainingId]
|
||||
);
|
||||
if (!rows.length) {
|
||||
await connection.rollback();
|
||||
return { affectedRows: 0 };
|
||||
}
|
||||
const requestId = rows[0].request_id;
|
||||
|
||||
// 2. training record 삭제
|
||||
await connection.query('DELETE FROM safety_training_records WHERE training_id = ?', [trainingId]);
|
||||
|
||||
// 3. 출입 신청 상태를 approved로 복원
|
||||
await connection.query(
|
||||
"UPDATE workplace_visit_requests SET status = 'approved', updated_at = NOW() WHERE request_id = ?",
|
||||
[requestId]
|
||||
);
|
||||
|
||||
await connection.commit();
|
||||
return { affectedRows: 1 };
|
||||
} catch (err) {
|
||||
await connection.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
};
|
||||
|
||||
const getTrainingRecords = async (filters = {}) => {
|
||||
const db = getPool();
|
||||
let query = `
|
||||
@@ -581,6 +617,7 @@ module.exports = {
|
||||
createTrainingRecord,
|
||||
getTrainingRecordByRequestId,
|
||||
updateTrainingRecord,
|
||||
deleteTrainingRecord,
|
||||
completeTraining,
|
||||
getTrainingRecords,
|
||||
getAllCategories,
|
||||
|
||||
@@ -41,6 +41,7 @@ router.post('/training', requireAdmin, visitRequestController.createTrainingReco
|
||||
router.get('/training', visitRequestController.getTrainingRecords);
|
||||
router.get('/training/request/:requestId', visitRequestController.getTrainingRecordByRequestId);
|
||||
router.put('/training/:id', requireAdmin, visitRequestController.updateTrainingRecord);
|
||||
router.delete('/training/:id', requireAdmin, visitRequestController.deleteTrainingRecord);
|
||||
router.post('/training/:id/complete', requireAdmin, visitRequestController.completeTraining);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -36,6 +36,12 @@ function renderPendingTraining() {
|
||||
<button onclick="openTrainingModal(${r.request_id})" class="text-blue-600 hover:text-blue-800 text-xs px-2 py-1 border border-blue-200 rounded hover:bg-blue-50">
|
||||
<i class="fas fa-chalkboard-teacher mr-1"></i>교육실시
|
||||
</button>
|
||||
<button onclick="openEditRequest(${r.request_id})" class="text-gray-600 hover:text-gray-800 text-xs px-1.5 py-1 border border-gray-200 rounded hover:bg-gray-50 ml-1" title="수정">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<button onclick="doDeleteRequest(${r.request_id})" class="text-red-500 hover:text-red-700 text-xs px-1.5 py-1 border border-red-200 rounded hover:bg-red-50 ml-1" title="삭제">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
@@ -70,6 +76,12 @@ function renderCompletedTraining() {
|
||||
<td>${escapeHtml(t.trainer_full_name || t.trainer_name || '-')}</td>
|
||||
<td class="text-right">
|
||||
${t.completed_at ? '<span class="badge badge-green">완료</span>' : '<span class="badge badge-amber">진행중</span>'}
|
||||
<button onclick="openEditTraining(${t.training_id})" class="text-gray-600 hover:text-gray-800 text-xs px-1.5 py-1 border border-gray-200 rounded hover:bg-gray-50 ml-1" title="수정">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<button onclick="doDeleteTraining(${t.training_id})" class="text-red-500 hover:text-red-700 text-xs px-1.5 py-1 border border-red-200 rounded hover:bg-red-50 ml-1" title="삭제">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -217,6 +229,112 @@ function clearSignature() {
|
||||
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;
|
||||
@@ -233,6 +351,8 @@ function initTrainingPage() {
|
||||
}
|
||||
|
||||
document.getElementById('trainingForm').addEventListener('submit', submitTraining);
|
||||
document.getElementById('editRequestForm').addEventListener('submit', submitEditRequest);
|
||||
document.getElementById('editTrainingForm').addEventListener('submit', submitEditTraining);
|
||||
initSignaturePad();
|
||||
loadPendingTraining();
|
||||
loadCompletedTraining();
|
||||
|
||||
@@ -133,6 +133,84 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 출입 신청 수정 모달 -->
|
||||
<div id="editRequestModal" class="hidden modal-overlay" onclick="if(event.target===this)closeEditRequestModal()">
|
||||
<div class="modal-content p-6" style="max-width: 32rem;">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">출입 신청 수정</h3>
|
||||
<button onclick="closeEditRequestModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<form id="editRequestForm">
|
||||
<input type="hidden" id="editRequestId">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">업체 <span class="text-red-400">*</span></label>
|
||||
<input type="text" id="editReqCompany" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">인원 <span class="text-red-400">*</span></label>
|
||||
<input type="number" id="editReqCount" class="input-field w-full px-3 py-2 rounded-lg text-sm" min="1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">방문일 <span class="text-red-400">*</span></label>
|
||||
<input type="date" id="editReqDate" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">방문시간 <span class="text-red-400">*</span></label>
|
||||
<input type="time" id="editReqTime" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
|
||||
<textarea id="editReqNotes" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4 gap-2">
|
||||
<button type="button" onclick="closeEditRequestModal()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
|
||||
<i class="fas fa-save mr-1"></i>저장
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 교육 기록 수정 모달 -->
|
||||
<div id="editTrainingModal" class="hidden modal-overlay" onclick="if(event.target===this)closeEditTrainingModal()">
|
||||
<div class="modal-content p-6" style="max-width: 32rem;">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">교육 기록 수정</h3>
|
||||
<button onclick="closeEditTrainingModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<form id="editTrainingForm">
|
||||
<input type="hidden" id="editTrainingId">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">교육일 <span class="text-red-400">*</span></label>
|
||||
<input type="date" id="editTrainDate" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">시작시간 <span class="text-red-400">*</span></label>
|
||||
<input type="time" id="editTrainStartTime" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">종료시간</label>
|
||||
<input type="time" id="editTrainEndTime" class="input-field w-full px-3 py-2 rounded-lg text-sm">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">교육 내용</label>
|
||||
<textarea id="editTrainTopics" class="input-field w-full px-3 py-2 rounded-lg text-sm" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4 gap-2">
|
||||
<button type="button" onclick="closeEditTrainingModal()" class="px-4 py-2 border rounded-lg text-sm hover:bg-gray-50">취소</button>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
|
||||
<i class="fas fa-save mr-1"></i>저장
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tksafety-core.js?v=3"></script>
|
||||
<script src="/static/js/tksafety-training.js"></script>
|
||||
<script>initTrainingPage();</script>
|
||||
|
||||
Reference in New Issue
Block a user