feat: 안전 코드 tksafety 이관 + 사용자 관리 정리 + UI Tailwind 전환
Phase 1: tksafety에 출입신청/체크리스트 API·웹 추가, tkfb 안전 코드 삭제
Phase 2: 사용자 관리 페이지 삭제, API 축소, 알림 수신자 tkuser 이관
Phase 3: tkuser 권한 페이지 정의 업데이트
Phase 4: 전체 34개 페이지 Tailwind CSS + tkfb-core.js 전환,
미사용 CSS 20개·인프라 JS 10개·템플릿·컴포넌트 삭제
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
158
tksafety/web/static/js/tksafety-visit-request.js
Normal file
158
tksafety/web/static/js/tksafety-visit-request.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* ===== Visit Request (출입 신청) ===== */
|
||||
let myRequests = [];
|
||||
let categories = [];
|
||||
let workplaces = [];
|
||||
let purposes = [];
|
||||
|
||||
/* ===== Status badge for visit requests ===== */
|
||||
function vrStatusBadge(s) {
|
||||
const m = {
|
||||
pending: ['badge-amber', '대기중'],
|
||||
approved: ['badge-green', '승인됨'],
|
||||
rejected: ['badge-red', '반려됨'],
|
||||
training_completed: ['badge-blue', '교육완료']
|
||||
};
|
||||
const [cls, label] = m[s] || ['badge-gray', s];
|
||||
return `<span class="badge ${cls}">${label}</span>`;
|
||||
}
|
||||
|
||||
/* ===== Load form data (purposes, categories) ===== */
|
||||
async function loadFormData() {
|
||||
try {
|
||||
const [purposeRes, categoryRes] = await Promise.all([
|
||||
api('/visit-requests/purposes/active'),
|
||||
api('/visit-requests/categories')
|
||||
]);
|
||||
purposes = purposeRes.data || [];
|
||||
categories = categoryRes.data || [];
|
||||
|
||||
const purposeSelect = document.getElementById('purposeId');
|
||||
purposeSelect.innerHTML = '<option value="">선택</option>' +
|
||||
purposes.map(p => `<option value="${p.purpose_id}">${escapeHtml(p.purpose_name)}</option>`).join('');
|
||||
|
||||
const categorySelect = document.getElementById('categoryId');
|
||||
categorySelect.innerHTML = '<option value="">선택</option>' +
|
||||
categories.map(c => `<option value="${c.category_id}">${escapeHtml(c.category_name)}</option>`).join('');
|
||||
} catch (e) {
|
||||
showToast('폼 데이터 로드 실패: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Load workplaces by category ===== */
|
||||
async function loadWorkplaces(categoryId) {
|
||||
const workplaceSelect = document.getElementById('workplaceId');
|
||||
if (!categoryId) {
|
||||
workplaceSelect.innerHTML = '<option value="">분류를 먼저 선택하세요</option>';
|
||||
workplaces = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api('/visit-requests/workplaces?category_id=' + categoryId);
|
||||
workplaces = res.data || [];
|
||||
workplaceSelect.innerHTML = '<option value="">선택</option>' +
|
||||
workplaces.map(w => `<option value="${w.workplace_id}">${escapeHtml(w.workplace_name)}</option>`).join('');
|
||||
} catch (e) {
|
||||
showToast('작업장 로드 실패: ' + e.message, 'error');
|
||||
workplaceSelect.innerHTML = '<option value="">로드 실패</option>';
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Load my requests ===== */
|
||||
async function loadMyRequests() {
|
||||
try {
|
||||
const res = await api('/visit-requests/requests?requester_id=' + currentUser.id);
|
||||
myRequests = res.data || [];
|
||||
renderMyRequests();
|
||||
} catch (e) {
|
||||
showToast('신청 목록 로드 실패: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderMyRequests() {
|
||||
const tbody = document.getElementById('myRequestsBody');
|
||||
if (!myRequests.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-400 py-8">신청 내역이 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = myRequests.map(r => {
|
||||
const canDelete = r.status === 'pending';
|
||||
return `<tr>
|
||||
<td>${formatDate(r.created_at)}</td>
|
||||
<td>${escapeHtml(r.visitor_company)}</td>
|
||||
<td class="text-center">${r.visitor_count}</td>
|
||||
<td>${escapeHtml(r.workplace_name || '-')}</td>
|
||||
<td>${formatDate(r.visit_date)}</td>
|
||||
<td class="hide-mobile">${r.visit_time ? String(r.visit_time).substring(0, 5) : '-'}</td>
|
||||
<td>${escapeHtml(r.purpose_name || '-')}</td>
|
||||
<td>${vrStatusBadge(r.status)}</td>
|
||||
<td class="text-right">
|
||||
${canDelete ? `<button onclick="deleteRequest(${r.request_id})" class="text-gray-400 hover:text-red-500 text-xs" title="삭제"><i class="fas fa-trash"></i></button>` : ''}
|
||||
${r.status === 'rejected' && r.rejection_reason ? `<button onclick="alert('반려 사유: ' + ${JSON.stringify(r.rejection_reason)})" class="text-gray-400 hover:text-gray-600 text-xs ml-1" title="반려사유"><i class="fas fa-info-circle"></i></button>` : ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ===== Submit request ===== */
|
||||
async function submitRequest(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
visitor_company: document.getElementById('visitorCompany').value.trim(),
|
||||
visitor_count: parseInt(document.getElementById('visitorCount').value) || 1,
|
||||
category_id: parseInt(document.getElementById('categoryId').value) || null,
|
||||
workplace_id: parseInt(document.getElementById('workplaceId').value) || null,
|
||||
visit_date: document.getElementById('visitDate').value,
|
||||
visit_time: document.getElementById('visitTime').value,
|
||||
purpose_id: parseInt(document.getElementById('purposeId').value) || null,
|
||||
notes: document.getElementById('notes').value.trim() || null
|
||||
};
|
||||
|
||||
if (!data.visitor_company) { showToast('업체명을 입력해주세요', 'error'); return; }
|
||||
if (!data.category_id) { showToast('작업장 분류를 선택해주세요', 'error'); return; }
|
||||
if (!data.workplace_id) { showToast('작업장을 선택해주세요', 'error'); return; }
|
||||
if (!data.visit_date) { showToast('방문일을 선택해주세요', 'error'); return; }
|
||||
if (!data.visit_time) { showToast('방문시간을 입력해주세요', 'error'); return; }
|
||||
if (!data.purpose_id) { showToast('방문 목적을 선택해주세요', 'error'); return; }
|
||||
|
||||
try {
|
||||
await api('/visit-requests/requests', { method: 'POST', body: JSON.stringify(data) });
|
||||
showToast('출입 신청이 완료되었습니다');
|
||||
document.getElementById('visitRequestForm').reset();
|
||||
document.getElementById('workplaceId').innerHTML = '<option value="">분류를 먼저 선택하세요</option>';
|
||||
document.getElementById('visitorCount').value = '1';
|
||||
await loadMyRequests();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Delete request ===== */
|
||||
async function deleteRequest(id) {
|
||||
if (!confirm('이 신청을 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
await api('/visit-requests/requests/' + id, { method: 'DELETE' });
|
||||
showToast('삭제되었습니다');
|
||||
await loadMyRequests();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Init ===== */
|
||||
function initVisitRequestPage() {
|
||||
if (!initAuth()) return;
|
||||
|
||||
// Set default visit date to today
|
||||
const today = new Date().toISOString().substring(0, 10);
|
||||
document.getElementById('visitDate').value = today;
|
||||
|
||||
// Category change -> load workplaces
|
||||
document.getElementById('categoryId').addEventListener('change', function() {
|
||||
loadWorkplaces(this.value);
|
||||
});
|
||||
|
||||
document.getElementById('visitRequestForm').addEventListener('submit', submitRequest);
|
||||
|
||||
loadFormData();
|
||||
loadMyRequests();
|
||||
}
|
||||
Reference in New Issue
Block a user