tkuser: 업체(공급업체) CRUD + 소모품 마스터 CRUD (사진 업로드 포함) tkfb: 구매신청 → 구매 처리 → 월간 분석/정산 전체 워크플로 설비(equipment) 분류 구매 시 자동 등록 + 실패 시 admin 알림 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
11 KiB
JavaScript
206 lines
11 KiB
JavaScript
/* ===== tkuser 소모품 마스터 CRUD ===== */
|
|
let consumablesLoaded = false;
|
|
let consumablesList = [];
|
|
|
|
const CONSUMABLE_CATEGORIES = {
|
|
consumable: '소모품',
|
|
safety: '안전용품',
|
|
repair: '수선비',
|
|
equipment: '설비'
|
|
};
|
|
const CONSUMABLE_CAT_COLORS = {
|
|
consumable: 'bg-blue-50 text-blue-600',
|
|
safety: 'bg-green-50 text-green-600',
|
|
repair: 'bg-amber-50 text-amber-600',
|
|
equipment: 'bg-purple-50 text-purple-600'
|
|
};
|
|
|
|
async function loadConsumablesTab() {
|
|
if (consumablesLoaded) return;
|
|
consumablesLoaded = true;
|
|
if (currentUser && ['admin', 'system'].includes(currentUser.role)) {
|
|
document.getElementById('btnAddConsumableTkuser')?.classList.remove('hidden');
|
|
}
|
|
await loadConsumablesList();
|
|
}
|
|
|
|
async function loadConsumablesList() {
|
|
try {
|
|
const category = document.getElementById('consumableFilterCategoryTkuser')?.value || '';
|
|
const isActive = document.getElementById('consumableFilterActiveTkuser')?.value;
|
|
const search = document.getElementById('consumableSearchTkuser')?.value?.trim() || '';
|
|
const params = new URLSearchParams();
|
|
if (category) params.set('category', category);
|
|
if (isActive !== '' && isActive !== undefined) params.set('is_active', isActive);
|
|
if (search) params.set('search', search);
|
|
const r = await api('/consumable-items?' + params.toString());
|
|
consumablesList = r.data || [];
|
|
renderConsumablesListTkuser();
|
|
} catch (e) {
|
|
document.getElementById('consumablesListTkuser').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 renderConsumablesListTkuser() {
|
|
const c = document.getElementById('consumablesListTkuser');
|
|
if (!consumablesList.length) {
|
|
c.innerHTML = '<p class="text-gray-400 text-center py-4 text-sm">등록된 소모품이 없습니다.</p>';
|
|
return;
|
|
}
|
|
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role);
|
|
c.innerHTML = `<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">` +
|
|
consumablesList.map(item => {
|
|
const catLabel = CONSUMABLE_CATEGORIES[item.category] || item.category;
|
|
const catColor = CONSUMABLE_CAT_COLORS[item.category] || 'bg-gray-50 text-gray-600';
|
|
const price = item.base_price ? Number(item.base_price).toLocaleString() + '원' : '-';
|
|
return `<div class="bg-white border rounded-lg p-3 hover:shadow-md transition-shadow">
|
|
<div class="flex gap-3">
|
|
${item.photo_path
|
|
? `<img src="${item.photo_path}" class="w-16 h-16 rounded object-cover flex-shrink-0 cursor-pointer" onclick="document.getElementById('photoViewImage').src=this.src; document.getElementById('photoViewModal').classList.remove('hidden');" onerror="this.style.display='none'">`
|
|
: `<div class="w-16 h-16 rounded bg-gray-100 flex items-center justify-center flex-shrink-0"><i class="fas fa-box text-gray-300 text-xl"></i></div>`}
|
|
<div class="flex-1 min-w-0">
|
|
<div class="text-sm font-medium text-gray-800 truncate">${escHtml(item.item_name)}</div>
|
|
<div class="text-xs text-gray-500 mt-0.5">${escHtml(item.maker) || '-'}</div>
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<span class="px-1.5 py-0.5 rounded text-xs ${catColor}">${catLabel}</span>
|
|
<span class="text-xs text-gray-600 font-medium">${price}</span>
|
|
<span class="text-xs text-gray-400">${escHtml(item.unit) || 'EA'}</span>
|
|
</div>
|
|
${!item.is_active ? '<span class="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-400 mt-1 inline-block">비활성</span>' : ''}
|
|
</div>
|
|
</div>
|
|
${isAdmin ? `<div class="flex justify-end gap-1 mt-2 pt-2 border-t">
|
|
<button onclick="openEditConsumableTkuser(${item.item_id})" class="px-2 py-1 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded text-xs"><i class="fas fa-pen mr-1"></i>수정</button>
|
|
${item.is_active ? `<button onclick="deactivateConsumableTkuser(${item.item_id}, '${escHtml(item.item_name).replace(/'/g, "\\'")}')" class="px-2 py-1 text-red-400 hover:text-red-600 hover:bg-red-50 rounded text-xs"><i class="fas fa-ban mr-1"></i>비활성화</button>` : ''}
|
|
</div>` : ''}
|
|
</div>`;
|
|
}).join('') + `</div>`;
|
|
}
|
|
|
|
/* ===== 소모품 등록 ===== */
|
|
function openAddConsumableTkuser() {
|
|
document.getElementById('addConsumablePhotoPreviewTkuser').innerHTML = '';
|
|
document.getElementById('addConsumableModalTkuser').classList.remove('hidden');
|
|
}
|
|
function closeAddConsumableTkuser() { document.getElementById('addConsumableModalTkuser').classList.add('hidden'); document.getElementById('addConsumableFormTkuser').reset(); document.getElementById('addConsumablePhotoPreviewTkuser').innerHTML = ''; }
|
|
|
|
function previewAddConsumablePhoto() {
|
|
const file = document.getElementById('newConsumablePhotoTkuser').files[0];
|
|
const preview = document.getElementById('addConsumablePhotoPreviewTkuser');
|
|
if (!file) { preview.innerHTML = ''; return; }
|
|
const reader = new FileReader();
|
|
reader.onload = e => { preview.innerHTML = `<img src="${e.target.result}" class="w-20 h-20 rounded object-cover">`; };
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
async function submitAddConsumableTkuser(e) {
|
|
e.preventDefault();
|
|
const itemName = document.getElementById('newConsumableNameTkuser').value.trim();
|
|
const category = document.getElementById('newConsumableCategoryTkuser').value;
|
|
if (!itemName) { showToast('품명은 필수입니다', 'error'); return; }
|
|
if (!category) { showToast('분류는 필수입니다', 'error'); return; }
|
|
|
|
const fd = new FormData();
|
|
fd.append('item_name', itemName);
|
|
fd.append('maker', document.getElementById('newConsumableMakerTkuser').value.trim());
|
|
fd.append('category', category);
|
|
fd.append('base_price', document.getElementById('newConsumablePriceTkuser').value || '0');
|
|
fd.append('unit', document.getElementById('newConsumableUnitTkuser').value.trim() || 'EA');
|
|
const photoFile = document.getElementById('newConsumablePhotoTkuser').files[0];
|
|
if (photoFile) fd.append('photo', photoFile);
|
|
|
|
try {
|
|
const token = getToken();
|
|
const res = await fetch('/api/consumable-items', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
body: fd
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || '등록 실패');
|
|
showToast('소모품이 등록되었습니다');
|
|
closeAddConsumableTkuser();
|
|
await loadConsumablesList();
|
|
} catch (e) { showToast(e.message, 'error'); }
|
|
}
|
|
|
|
/* ===== 소모품 수정 ===== */
|
|
function openEditConsumableTkuser(id) {
|
|
const item = consumablesList.find(x => x.item_id === id);
|
|
if (!item) return;
|
|
document.getElementById('editConsumableIdTkuser').value = item.item_id;
|
|
document.getElementById('editConsumableNameTkuser').value = item.item_name;
|
|
document.getElementById('editConsumableMakerTkuser').value = item.maker || '';
|
|
document.getElementById('editConsumableCategoryTkuser').value = item.category;
|
|
document.getElementById('editConsumablePriceTkuser').value = item.base_price || '';
|
|
document.getElementById('editConsumableUnitTkuser').value = item.unit || 'EA';
|
|
const preview = document.getElementById('editConsumablePhotoPreviewTkuser');
|
|
preview.innerHTML = item.photo_path ? `<img src="${item.photo_path}" class="w-20 h-20 rounded object-cover">` : '';
|
|
document.getElementById('editConsumablePhotoTkuser').value = '';
|
|
document.getElementById('editConsumableModalTkuser').classList.remove('hidden');
|
|
}
|
|
function closeEditConsumableTkuser() { document.getElementById('editConsumableModalTkuser').classList.add('hidden'); }
|
|
|
|
function previewEditConsumablePhoto() {
|
|
const file = document.getElementById('editConsumablePhotoTkuser').files[0];
|
|
const preview = document.getElementById('editConsumablePhotoPreviewTkuser');
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = e => { preview.innerHTML = `<img src="${e.target.result}" class="w-20 h-20 rounded object-cover">`; };
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
async function submitEditConsumableTkuser(e) {
|
|
e.preventDefault();
|
|
const id = document.getElementById('editConsumableIdTkuser').value;
|
|
const fd = new FormData();
|
|
fd.append('item_name', document.getElementById('editConsumableNameTkuser').value.trim());
|
|
fd.append('maker', document.getElementById('editConsumableMakerTkuser').value.trim());
|
|
fd.append('category', document.getElementById('editConsumableCategoryTkuser').value);
|
|
fd.append('base_price', document.getElementById('editConsumablePriceTkuser').value || '0');
|
|
fd.append('unit', document.getElementById('editConsumableUnitTkuser').value.trim() || 'EA');
|
|
const photoFile = document.getElementById('editConsumablePhotoTkuser').files[0];
|
|
if (photoFile) fd.append('photo', photoFile);
|
|
|
|
try {
|
|
const token = getToken();
|
|
const res = await fetch(`/api/consumable-items/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
body: fd
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || '수정 실패');
|
|
showToast('수정되었습니다');
|
|
closeEditConsumableTkuser();
|
|
await loadConsumablesList();
|
|
} catch (e) { showToast(e.message, 'error'); }
|
|
}
|
|
|
|
/* ===== 소모품 비활성화 ===== */
|
|
async function deactivateConsumableTkuser(id, name) {
|
|
if (!confirm(`"${name}" 소모품을 비활성화하시겠습니까?`)) return;
|
|
try {
|
|
await api(`/consumable-items/${id}`, { method: 'DELETE' });
|
|
showToast('비활성화 완료');
|
|
await loadConsumablesList();
|
|
} catch (e) { showToast(e.message, 'error'); }
|
|
}
|
|
|
|
// 검색/필터 이벤트 + 모달 폼 이벤트
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
let searchTimeout;
|
|
const searchEl = document.getElementById('consumableSearchTkuser');
|
|
if (searchEl) searchEl.addEventListener('input', () => {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(loadConsumablesList, 300);
|
|
});
|
|
const filterCatEl = document.getElementById('consumableFilterCategoryTkuser');
|
|
if (filterCatEl) filterCatEl.addEventListener('change', loadConsumablesList);
|
|
const filterActiveEl = document.getElementById('consumableFilterActiveTkuser');
|
|
if (filterActiveEl) filterActiveEl.addEventListener('change', loadConsumablesList);
|
|
|
|
document.getElementById('addConsumableFormTkuser')?.addEventListener('submit', submitAddConsumableTkuser);
|
|
document.getElementById('editConsumableFormTkuser')?.addEventListener('submit', submitEditConsumableTkuser);
|
|
});
|