feat(tkuser): 입사일 자동표시 + 퇴사자 목록 분리 + 퇴사일 관리
- 사용자 추가 시 hire_date 전송 (서울 오늘날짜 기본값) - resigned_date 컬럼 마이그레이션 + CRUD 지원 - 비활성화(삭제) 시 resigned_date 자동 설정 (COALESCE) - 활성/비활성 사용자 목록 분리, 퇴사자 접기/펼치기 - 퇴사자 재활성화 기능 (resigned_date 초기화) - 편집 모달에 퇴사일 필드 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,7 @@ function deptLabel(d, deptId) {
|
||||
return DEPT_FALLBACK[d] || d || '';
|
||||
}
|
||||
function formatDate(d) { if (!d) return ''; return d.substring(0, 10); }
|
||||
function getSeoulToday() { return new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' }); }
|
||||
function escHtml(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
/* ===== Logout ===== */
|
||||
|
||||
@@ -113,6 +113,8 @@ async function loadUsers() {
|
||||
const r = await api('/users'); users = r.data || r;
|
||||
displayUsers(); updatePermissionUserSelect();
|
||||
populateUserDeptSelects();
|
||||
const hireDateInput = document.getElementById('newHireDate');
|
||||
if (hireDateInput && !hireDateInput.value) hireDateInput.value = getSeoulToday();
|
||||
} catch (err) {
|
||||
document.getElementById('userList').innerHTML = `<div class="text-red-500 text-center py-6"><i class="fas fa-exclamation-triangle text-xl"></i><p class="text-sm mt-2">${err.message}</p></div>`;
|
||||
}
|
||||
@@ -127,35 +129,75 @@ function populateUserDeptSelects() {
|
||||
sel.value = val;
|
||||
});
|
||||
}
|
||||
function displayUsers() {
|
||||
const c = document.getElementById('userList');
|
||||
if (!users.length) { c.innerHTML = '<p class="text-gray-400 text-center py-4 text-sm">등록된 사용자가 없습니다.</p>'; return; }
|
||||
c.innerHTML = users.map(u => `
|
||||
<div class="flex items-center justify-between p-2.5 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
function renderUserRow(u, isResigned) {
|
||||
return `<div class="flex items-center justify-between p-2.5 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-800 truncate"><i class="fas fa-user mr-1.5 text-gray-400 text-xs"></i>${u.name||u.username}</div>
|
||||
<div class="text-sm font-medium ${isResigned ? 'text-gray-400' : 'text-gray-800'} truncate"><i class="fas fa-user mr-1.5 text-gray-400 text-xs"></i>${u.name||u.username}</div>
|
||||
<div class="text-xs text-gray-500 flex items-center gap-1.5 mt-0.5 flex-wrap">
|
||||
<span>${u.username}</span>
|
||||
${u.department||u.department_id?`<span class="px-1.5 py-0.5 rounded bg-green-50 text-green-600">${deptLabel(u.department, u.department_id)}</span>`:''}
|
||||
<span class="px-1.5 py-0.5 rounded ${u.role==='admin'?'bg-red-50 text-red-600':'bg-slate-50 text-slate-500'}">${u.role==='admin'?'관리자':'사용자'}</span>
|
||||
${u.hire_date ? `<span class="px-1.5 py-0.5 rounded bg-blue-50 text-blue-600">입사 ${formatDate(u.hire_date)}</span>` : '<span class="px-1.5 py-0.5 rounded bg-orange-50 text-orange-500">입사일 미등록</span>'}
|
||||
${u.is_active===0||u.is_active===false?'<span class="px-1.5 py-0.5 rounded bg-gray-100 text-gray-400">비활성</span>':''}
|
||||
${isResigned && u.resigned_date ? `<span class="px-1.5 py-0.5 rounded bg-gray-200 text-gray-500">퇴사 ${formatDate(u.resigned_date)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 ml-2 flex-shrink-0">
|
||||
<button onclick="editUser(${u.user_id})" class="p-1.5 text-slate-500 hover:text-slate-700 hover:bg-slate-200 rounded" title="편집"><i class="fas fa-pen-to-square text-xs"></i></button>
|
||||
${isResigned ? `<button onclick="reactivateUser(${u.user_id},'${escHtml(u.name||u.username)}')" class="p-1.5 text-emerald-500 hover:text-emerald-700 hover:bg-emerald-100 rounded" title="재활성화"><i class="fas fa-user-check text-xs"></i></button>` : `
|
||||
<button onclick="resetPassword(${u.user_id},'${u.username}')" class="p-1.5 text-amber-500 hover:text-amber-700 hover:bg-amber-100 rounded" title="비밀번호 초기화"><i class="fas fa-key text-xs"></i></button>
|
||||
${u.username!=='hyungi'?`<button onclick="deleteUser(${u.user_id},'${u.username}')" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="삭제"><i class="fas fa-trash text-xs"></i></button>`:''}
|
||||
${u.username!=='hyungi'?`<button onclick="deleteUser(${u.user_id},'${u.username}')" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="삭제"><i class="fas fa-trash text-xs"></i></button>`:''}`}
|
||||
</div>
|
||||
</div>`).join('');
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function displayUsers() {
|
||||
const activeUsers = users.filter(u => u.is_active !== 0 && u.is_active !== false);
|
||||
const resignedUsers = users.filter(u => u.is_active === 0 || u.is_active === false);
|
||||
|
||||
const c = document.getElementById('userList');
|
||||
if (!activeUsers.length) { c.innerHTML = '<p class="text-gray-400 text-center py-4 text-sm">등록된 사용자가 없습니다.</p>'; }
|
||||
else { c.innerHTML = activeUsers.map(u => renderUserRow(u, false)).join(''); }
|
||||
|
||||
const resignedSection = document.getElementById('resignedSection');
|
||||
const resignedList = document.getElementById('resignedUserList');
|
||||
const resignedCount = document.getElementById('resignedCount');
|
||||
if (resignedUsers.length > 0) {
|
||||
resignedSection.classList.remove('hidden');
|
||||
resignedCount.textContent = `(${resignedUsers.length}명)`;
|
||||
resignedList.innerHTML = resignedUsers.map(u => renderUserRow(u, true)).join('');
|
||||
} else {
|
||||
resignedSection.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleResignedList() {
|
||||
const list = document.getElementById('resignedUserList');
|
||||
const btn = document.getElementById('resignedToggleBtn');
|
||||
if (list.classList.contains('hidden')) {
|
||||
list.classList.remove('hidden');
|
||||
btn.innerHTML = '<i class="fas fa-chevron-up mr-1"></i>접기';
|
||||
} else {
|
||||
list.classList.add('hidden');
|
||||
btn.innerHTML = '<i class="fas fa-chevron-down mr-1"></i>펼치기';
|
||||
}
|
||||
}
|
||||
|
||||
async function reactivateUser(id, name) {
|
||||
if (!confirm(`${name}을(를) 재활성화하시겠습니까?`)) return;
|
||||
try {
|
||||
await api(`/users/${id}`, { method: 'PUT', body: JSON.stringify({ is_active: true, resigned_date: null }) });
|
||||
showToast('재활성화 완료');
|
||||
await loadUsers();
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
document.getElementById('addUserForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const deptIdVal = document.getElementById('newDepartmentId').value;
|
||||
const hireDateVal = document.getElementById('newHireDate').value;
|
||||
try {
|
||||
await api('/users', { method:'POST', body: JSON.stringify({ username: document.getElementById('newUsername').value.trim(), name: document.getElementById('newFullName').value.trim(), password: document.getElementById('newPassword').value, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('newRole').value }) });
|
||||
showToast('사용자가 추가되었습니다.'); document.getElementById('addUserForm').reset(); await loadUsers();
|
||||
await api('/users', { method:'POST', body: JSON.stringify({ username: document.getElementById('newUsername').value.trim(), name: document.getElementById('newFullName').value.trim(), password: document.getElementById('newPassword').value, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('newRole').value, hire_date: hireDateVal || null }) });
|
||||
showToast('사용자가 추가되었습니다.'); document.getElementById('addUserForm').reset(); document.getElementById('newHireDate').value = getSeoulToday(); await loadUsers();
|
||||
} catch(e) { showToast(e.message,'error'); }
|
||||
});
|
||||
|
||||
@@ -167,6 +209,7 @@ function editUser(id) {
|
||||
document.getElementById('editDepartmentId').value=u.department_id||'';
|
||||
document.getElementById('editRole').value=u.role;
|
||||
document.getElementById('editHireDate').value = formatDate(u.hire_date);
|
||||
document.getElementById('editResignedDate').value = formatDate(u.resigned_date);
|
||||
document.getElementById('editUserModal').classList.remove('hidden');
|
||||
}
|
||||
function closeEditModal() { document.getElementById('editUserModal').classList.add('hidden'); }
|
||||
@@ -175,7 +218,7 @@ document.getElementById('editUserForm').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const deptIdVal = document.getElementById('editDepartmentId').value;
|
||||
try {
|
||||
await api(`/users/${document.getElementById('editUserId').value}`, { method:'PUT', body: JSON.stringify({ name: document.getElementById('editFullName').value.trim()||null, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('editRole').value, hire_date: document.getElementById('editHireDate').value || null }) });
|
||||
await api(`/users/${document.getElementById('editUserId').value}`, { method:'PUT', body: JSON.stringify({ name: document.getElementById('editFullName').value.trim()||null, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('editRole').value, hire_date: document.getElementById('editHireDate').value || null, resigned_date: document.getElementById('editResignedDate').value || null }) });
|
||||
showToast('수정되었습니다.'); closeEditModal(); await loadUsers();
|
||||
} catch(e) { showToast(e.message,'error'); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user