- System2 신고: SSO JWT 인증 전환, API base 정리 - System3 부적합: SSO 인증 매니저 통합, 권한 체계 정비 - User Management: SSO 토큰 기반 사용자 관리 API 연동 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
219 lines
11 KiB
JavaScript
219 lines
11 KiB
JavaScript
/* ===== Tasks CRUD ===== */
|
|
let taskWorkTypes = [], allTasks = [], tasksLoaded = false;
|
|
let selectedTaskWorkTypeFilter = null;
|
|
|
|
async function loadTasksTab() {
|
|
await loadWorkTypes();
|
|
await loadTasks();
|
|
tasksLoaded = true;
|
|
}
|
|
|
|
async function loadWorkTypes() {
|
|
try {
|
|
const r = await api('/tasks/work-types');
|
|
taskWorkTypes = r.data || [];
|
|
renderWorkTypeSidebar();
|
|
populateTaskWorkTypeSelect();
|
|
} catch(e) { console.warn('공정 로드 실패:', e); }
|
|
}
|
|
|
|
function renderWorkTypeSidebar() {
|
|
const c = document.getElementById('workTypeSidebar');
|
|
if (!c) return;
|
|
let html = `<div onclick="filterTasksByWorkType(null)" class="flex items-center justify-between p-2 rounded-lg cursor-pointer transition-colors ${selectedTaskWorkTypeFilter === null ? 'bg-blue-50 ring-1 ring-blue-200' : 'hover:bg-gray-50'}">
|
|
<span class="text-sm font-medium ${selectedTaskWorkTypeFilter === null ? 'text-blue-700' : 'text-gray-700'}">전체</span>
|
|
<span class="text-xs text-gray-400">${allTasks.length}</span>
|
|
</div>`;
|
|
// 카테고리별 그룹핑
|
|
const grouped = {};
|
|
taskWorkTypes.forEach(wt => {
|
|
const cat = wt.category || '미분류';
|
|
if (!grouped[cat]) grouped[cat] = [];
|
|
grouped[cat].push(wt);
|
|
});
|
|
Object.keys(grouped).sort().forEach(cat => {
|
|
html += `<div class="text-[10px] font-semibold text-gray-400 uppercase tracking-wider mt-3 mb-1 px-2">${cat}</div>`;
|
|
grouped[cat].forEach(wt => {
|
|
const count = allTasks.filter(t => t.work_type_id === wt.id).length;
|
|
const isActive = selectedTaskWorkTypeFilter === wt.id;
|
|
html += `<div onclick="filterTasksByWorkType(${wt.id})" class="group flex items-center justify-between p-2 rounded-lg cursor-pointer transition-colors ${isActive ? 'bg-blue-50 ring-1 ring-blue-200' : 'hover:bg-gray-50'}">
|
|
<span class="text-sm ${isActive ? 'font-medium text-blue-700' : 'text-gray-700'} truncate">${wt.name}</span>
|
|
<div class="flex items-center gap-1">
|
|
<span class="text-xs text-gray-400">${count}</span>
|
|
<button onclick="event.stopPropagation(); editWorkType(${wt.id})" class="p-0.5 text-gray-300 hover:text-slate-600 opacity-0 group-hover:opacity-100 transition-opacity" title="수정"><i class="fas fa-pen text-[10px]"></i></button>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
});
|
|
// 미지정 작업 수
|
|
const noType = allTasks.filter(t => !t.work_type_id).length;
|
|
if (noType > 0) {
|
|
html += `<div onclick="filterTasksByWorkType(0)" class="flex items-center justify-between p-2 rounded-lg cursor-pointer transition-colors mt-2 ${selectedTaskWorkTypeFilter === 0 ? 'bg-blue-50 ring-1 ring-blue-200' : 'hover:bg-gray-50'}">
|
|
<span class="text-sm text-gray-400 italic">미지정</span>
|
|
<span class="text-xs text-gray-400">${noType}</span>
|
|
</div>`;
|
|
}
|
|
c.innerHTML = html;
|
|
}
|
|
|
|
function populateTaskWorkTypeSelect() {
|
|
const sel = document.getElementById('taskWorkType');
|
|
if (!sel) return;
|
|
const val = sel.value;
|
|
sel.innerHTML = '<option value="">미지정</option>';
|
|
taskWorkTypes.forEach(wt => {
|
|
sel.innerHTML += `<option value="${wt.id}">${wt.category ? escapeHtml(wt.category) + ' > ' : ''}${escapeHtml(wt.name)}</option>`;
|
|
});
|
|
sel.value = val;
|
|
}
|
|
|
|
function filterTasksByWorkType(wtId) {
|
|
selectedTaskWorkTypeFilter = wtId;
|
|
renderWorkTypeSidebar();
|
|
displayTasks();
|
|
}
|
|
|
|
async function loadTasks() {
|
|
try {
|
|
const r = await api('/tasks');
|
|
allTasks = r.data || [];
|
|
renderWorkTypeSidebar();
|
|
displayTasks();
|
|
} catch(e) {
|
|
document.getElementById('taskList').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">${e.message}</p></div>`;
|
|
}
|
|
}
|
|
|
|
function displayTasks() {
|
|
const c = document.getElementById('taskList');
|
|
let filtered = allTasks;
|
|
let label = '전체';
|
|
if (selectedTaskWorkTypeFilter === 0) {
|
|
filtered = allTasks.filter(t => !t.work_type_id);
|
|
label = '미지정';
|
|
} else if (selectedTaskWorkTypeFilter) {
|
|
filtered = allTasks.filter(t => t.work_type_id === selectedTaskWorkTypeFilter);
|
|
const wt = taskWorkTypes.find(w => w.id === selectedTaskWorkTypeFilter);
|
|
label = wt ? wt.name : '';
|
|
}
|
|
document.getElementById('taskFilterLabel').textContent = `- ${label}`;
|
|
const active = filtered.filter(t => t.is_active).length;
|
|
const inactive = filtered.length - active;
|
|
document.getElementById('taskStats').textContent = `활성 ${active} / 비활성 ${inactive}`;
|
|
|
|
if (!filtered.length) { c.innerHTML = '<p class="text-gray-400 text-center py-8 text-sm">등록된 작업이 없습니다.</p>'; return; }
|
|
c.innerHTML = filtered.map(t => `
|
|
<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">${escHtml(t.task_name)}</div>
|
|
<div class="text-xs text-gray-500 flex items-center gap-1.5 mt-0.5 flex-wrap">
|
|
${t.work_type_name ? `<span class="px-1.5 py-0.5 rounded bg-slate-50 text-slate-600">${escHtml(t.work_type_name)}</span>` : '<span class="text-gray-300 italic">미지정</span>'}
|
|
${t.description ? `<span class="text-gray-400 truncate max-w-[200px]" title="${escHtml(t.description)}">${escHtml(t.description)}</span>` : ''}
|
|
${t.is_active ? '<span class="px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-600">활성</span>' : '<span class="px-1.5 py-0.5 rounded bg-gray-100 text-gray-400">비활성</span>'}
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-1 ml-2 flex-shrink-0">
|
|
<button onclick="editTask(${t.task_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>
|
|
<button onclick="deleteTask(${t.task_id},'${escHtml(t.task_name).replace(/'/g,"\\'")}')" class="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-100 rounded" title="삭제"><i class="fas fa-trash-alt text-xs"></i></button>
|
|
</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
// 공정 모달
|
|
function openWorkTypeModal(editId) {
|
|
document.getElementById('wtEditId').value = '';
|
|
document.getElementById('workTypeForm').reset();
|
|
document.getElementById('workTypeModalTitle').textContent = '공정 추가';
|
|
if (editId) {
|
|
const wt = taskWorkTypes.find(w => w.id === editId);
|
|
if (!wt) return;
|
|
document.getElementById('workTypeModalTitle').textContent = '공정 수정';
|
|
document.getElementById('wtEditId').value = wt.id;
|
|
document.getElementById('wtName').value = wt.name || '';
|
|
document.getElementById('wtCategory').value = wt.category || '';
|
|
document.getElementById('wtDesc').value = wt.description || '';
|
|
}
|
|
document.getElementById('workTypeModal').classList.remove('hidden');
|
|
}
|
|
function closeWorkTypeModal() { document.getElementById('workTypeModal').classList.add('hidden'); }
|
|
function editWorkType(id) { openWorkTypeModal(id); }
|
|
|
|
document.getElementById('workTypeForm').addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
const editId = document.getElementById('wtEditId').value;
|
|
const body = {
|
|
name: document.getElementById('wtName').value.trim(),
|
|
category: document.getElementById('wtCategory').value.trim() || null,
|
|
description: document.getElementById('wtDesc').value.trim() || null
|
|
};
|
|
try {
|
|
if (editId) {
|
|
await api(`/tasks/work-types/${editId}`, { method: 'PUT', body: JSON.stringify(body) });
|
|
showToast('공정이 수정되었습니다.');
|
|
} else {
|
|
await api('/tasks/work-types', { method: 'POST', body: JSON.stringify(body) });
|
|
showToast('공정이 추가되었습니다.');
|
|
}
|
|
closeWorkTypeModal();
|
|
await loadWorkTypes();
|
|
await loadTasks();
|
|
} catch(e) { showToast(e.message, 'error'); }
|
|
});
|
|
|
|
// 작업 모달
|
|
function openTaskModal(editId) {
|
|
document.getElementById('taskEditId').value = '';
|
|
document.getElementById('taskForm').reset();
|
|
document.getElementById('taskActive').checked = true;
|
|
document.getElementById('taskModalTitle').textContent = '작업 추가';
|
|
populateTaskWorkTypeSelect();
|
|
// 사이드바 필터 선택된 공정 자동 선택
|
|
if (!editId && selectedTaskWorkTypeFilter && selectedTaskWorkTypeFilter !== 0) {
|
|
document.getElementById('taskWorkType').value = selectedTaskWorkTypeFilter;
|
|
}
|
|
if (editId) {
|
|
const t = allTasks.find(x => x.task_id === editId);
|
|
if (!t) return;
|
|
document.getElementById('taskModalTitle').textContent = '작업 수정';
|
|
document.getElementById('taskEditId').value = t.task_id;
|
|
document.getElementById('taskName').value = t.task_name || '';
|
|
document.getElementById('taskWorkType').value = t.work_type_id || '';
|
|
document.getElementById('taskDesc').value = t.description || '';
|
|
document.getElementById('taskActive').checked = !!t.is_active;
|
|
}
|
|
document.getElementById('taskModal').classList.remove('hidden');
|
|
}
|
|
function closeTaskModal() { document.getElementById('taskModal').classList.add('hidden'); }
|
|
function editTask(id) { openTaskModal(id); }
|
|
|
|
document.getElementById('taskForm').addEventListener('submit', async e => {
|
|
e.preventDefault();
|
|
const editId = document.getElementById('taskEditId').value;
|
|
const body = {
|
|
task_name: document.getElementById('taskName').value.trim(),
|
|
work_type_id: document.getElementById('taskWorkType').value ? parseInt(document.getElementById('taskWorkType').value) : null,
|
|
description: document.getElementById('taskDesc').value.trim() || null,
|
|
is_active: document.getElementById('taskActive').checked ? 1 : 0
|
|
};
|
|
try {
|
|
if (editId) {
|
|
await api(`/tasks/${editId}`, { method: 'PUT', body: JSON.stringify(body) });
|
|
showToast('작업이 수정되었습니다.');
|
|
} else {
|
|
await api('/tasks', { method: 'POST', body: JSON.stringify(body) });
|
|
showToast('작업이 추가되었습니다.');
|
|
}
|
|
closeTaskModal();
|
|
await loadTasks();
|
|
} catch(e) { showToast(e.message, 'error'); }
|
|
});
|
|
|
|
async function deleteTask(id, name) {
|
|
if (!confirm(`"${name}" 작업을 삭제하시겠습니까?`)) return;
|
|
try {
|
|
await api(`/tasks/${id}`, { method: 'DELETE' });
|
|
showToast('작업이 삭제되었습니다.');
|
|
await loadTasks();
|
|
} catch(e) { showToast(e.message, 'error'); }
|
|
}
|