/* ===== 구매신청 페이지 ===== */ const TKUSER_BASE_URL = location.hostname.includes('technicalkorea.net') ? 'https://tkuser.technicalkorea.net' : location.protocol + '//' + location.hostname + ':30180'; const CAT_LABELS = { consumable: '소모품', safety: '안전용품', repair: '수선비', equipment: '설비' }; const CAT_COLORS = { consumable: 'badge-blue', safety: 'badge-green', repair: 'badge-amber', equipment: 'badge-purple' }; const STATUS_LABELS = { pending: '대기', purchased: '구매완료', hold: '보류' }; const STATUS_COLORS = { pending: 'badge-amber', purchased: 'badge-green', hold: 'badge-gray' }; let consumableItems = []; let vendorsList = []; let requestsList = []; let currentRequestForPurchase = null; let currentRequestForHold = null; let isAdmin = false; async function loadInitialData() { try { const [itemsRes, vendorsRes] = await Promise.all([ api('/purchase-requests/consumable-items'), api('/purchase-requests/vendors') ]); consumableItems = itemsRes.data || []; vendorsList = vendorsRes.data || []; populateItemSelect(); populateVendorSelect(); } catch (e) { console.error('초기 데이터 로드 실패:', e); } } function populateItemSelect() { const sel = document.getElementById('prItemSelect'); const groups = {}; consumableItems.forEach(item => { const cat = CAT_LABELS[item.category] || item.category; if (!groups[cat]) groups[cat] = []; groups[cat].push(item); }); let html = ''; for (const [cat, items] of Object.entries(groups)) { html += ``; items.forEach(item => { const maker = item.maker ? ` (${escapeHtml(item.maker)})` : ''; html += ``; }); html += ''; } sel.innerHTML = html; } function populateVendorSelect() { const sel = document.getElementById('pmVendor'); sel.innerHTML = '' + vendorsList.map(v => ``).join(''); } function onItemSelect() { const itemId = parseInt(document.getElementById('prItemSelect').value); const preview = document.getElementById('prItemPreview'); const item = consumableItems.find(i => i.item_id === itemId); if (!item) { preview.classList.add('hidden'); return; } preview.classList.remove('hidden'); const photoEl = document.getElementById('prItemPhoto'); if (item.photo_path) { photoEl.src = TKUSER_BASE_URL + item.photo_path; photoEl.classList.remove('hidden'); photoEl.onerror = () => photoEl.classList.add('hidden'); } else { photoEl.classList.add('hidden'); } document.getElementById('prItemInfo').textContent = `${item.item_name} ${item.maker ? '(' + item.maker + ')' : ''}`; const price = item.base_price ? Number(item.base_price).toLocaleString() + '원/' + (item.unit || 'EA') : '기준가 미설정'; document.getElementById('prItemPrice').textContent = price; } /* ===== 구매신청 제출 ===== */ async function submitPurchaseRequest() { const item_id = document.getElementById('prItemSelect').value; const quantity = parseInt(document.getElementById('prQuantity').value) || 0; const notes = document.getElementById('prNotes').value.trim(); if (!item_id) { showToast('소모품을 선택해주세요.', 'error'); return; } if (quantity < 1) { showToast('수량은 1 이상이어야 합니다.', 'error'); return; } try { await api('/purchase-requests', { method: 'POST', body: JSON.stringify({ item_id: parseInt(item_id), quantity, notes }) }); showToast('구매신청이 등록되었습니다.'); document.getElementById('prItemSelect').value = ''; document.getElementById('prQuantity').value = '1'; document.getElementById('prNotes').value = ''; document.getElementById('prItemPreview').classList.add('hidden'); await loadRequests(); } catch (e) { showToast(e.message, 'error'); } } /* ===== 신청 목록 ===== */ async function loadRequests() { try { const status = document.getElementById('prFilterStatus').value; const category = document.getElementById('prFilterCategory').value; const params = new URLSearchParams(); if (status) params.set('status', status); if (category) params.set('category', category); const res = await api('/purchase-requests?' + params.toString()); requestsList = res.data || []; renderRequests(); } catch (e) { document.getElementById('prRequestList').innerHTML = `${escapeHtml(e.message)}`; } } function renderRequests() { const tbody = document.getElementById('prRequestList'); if (!requestsList.length) { tbody.innerHTML = '구매신청 내역이 없습니다.'; return; } tbody.innerHTML = requestsList.map(r => { const catLabel = CAT_LABELS[r.category] || r.category; const catColor = CAT_COLORS[r.category] || 'badge-gray'; const statusLabel = STATUS_LABELS[r.status] || r.status; const statusColor = STATUS_COLORS[r.status] || 'badge-gray'; const photoSrc = r.photo_path ? TKUSER_BASE_URL + r.photo_path : ''; let actions = ''; if (isAdmin && r.status === 'pending') { actions = ` `; } else if (isAdmin && r.status === 'hold') { actions = ``; } if (r.status === 'pending' && (isAdmin || r.requester_id === currentUser.id)) { actions += ` `; } return `
${photoSrc ? `` : ''}
${escapeHtml(r.item_name)}
${escapeHtml(r.maker || '')}
${catLabel} ${r.quantity} ${escapeHtml(r.requester_name || '')} ${formatDate(r.request_date)} ${statusLabel} ${r.status === 'hold' && r.hold_reason ? `
${escapeHtml(r.hold_reason)}
` : ''} ${actions} `; }).join(''); } /* ===== 구매 처리 모달 ===== */ function openPurchaseModal(requestId) { const r = requestsList.find(x => x.request_id === requestId); if (!r) return; currentRequestForPurchase = r; const basePrice = r.base_price ? Number(r.base_price).toLocaleString() + '원' : '-'; document.getElementById('purchaseModalInfo').innerHTML = `
${escapeHtml(r.item_name)} ${r.maker ? '(' + escapeHtml(r.maker) + ')' : ''}
분류: ${CAT_LABELS[r.category] || r.category} | 기준가: ${basePrice} | 신청수량: ${r.quantity}
`; document.getElementById('pmUnitPrice').value = r.base_price || ''; document.getElementById('pmQuantity').value = r.quantity; document.getElementById('pmDate').value = new Date().toISOString().substring(0, 10); document.getElementById('pmNotes').value = ''; document.getElementById('pmPriceDiffArea').innerHTML = ''; document.getElementById('purchaseModal').classList.remove('hidden'); showPriceDiff(); } function closePurchaseModal() { document.getElementById('purchaseModal').classList.add('hidden'); currentRequestForPurchase = null; } function showPriceDiff() { if (!currentRequestForPurchase) return; const basePrice = Number(currentRequestForPurchase.base_price) || 0; const unitPrice = Number(document.getElementById('pmUnitPrice').value) || 0; const area = document.getElementById('pmPriceDiffArea'); if (basePrice > 0 && unitPrice > 0 && basePrice !== unitPrice) { const diff = unitPrice - basePrice; const arrow = diff > 0 ? '▲' : '▼'; const color = diff > 0 ? 'text-red-600' : 'text-blue-600'; area.innerHTML = `
기준가 ${basePrice.toLocaleString()}원 → 실구매가 ${unitPrice.toLocaleString()}원 ${arrow}${Math.abs(diff).toLocaleString()}
`; } else { area.innerHTML = ''; } } async function submitPurchase() { if (!currentRequestForPurchase) return; const unit_price = Number(document.getElementById('pmUnitPrice').value); const purchase_date = document.getElementById('pmDate').value; if (!unit_price) { showToast('구매 단가를 입력해주세요.', 'error'); return; } if (!purchase_date) { showToast('구매일을 입력해주세요.', 'error'); return; } const updateCheckbox = document.getElementById('pmUpdateBasePrice'); const body = { request_id: currentRequestForPurchase.request_id, item_id: currentRequestForPurchase.item_id, vendor_id: parseInt(document.getElementById('pmVendor').value) || null, quantity: parseInt(document.getElementById('pmQuantity').value) || currentRequestForPurchase.quantity, unit_price, purchase_date, update_base_price: updateCheckbox ? updateCheckbox.checked : false, notes: document.getElementById('pmNotes').value.trim() }; try { const res = await api('/purchases', { method: 'POST', body: JSON.stringify(body) }); let msg = '구매 처리가 완료되었습니다.'; if (res.data?.equipment?.success) msg += ` 설비 ${res.data.equipment.equipment_code} 자동 등록됨.`; else if (res.data?.equipment && !res.data.equipment.success) msg += ' (설비 자동 등록 실패 - 수동 등록 필요)'; showToast(msg); closePurchaseModal(); await loadRequests(); } catch (e) { showToast(e.message, 'error'); } } /* ===== 보류 모달 ===== */ function openHoldModal(requestId) { currentRequestForHold = requestId; document.getElementById('holdReason').value = ''; document.getElementById('holdModal').classList.remove('hidden'); } function closeHoldModal() { document.getElementById('holdModal').classList.add('hidden'); currentRequestForHold = null; } async function submitHold() { if (!currentRequestForHold) return; const hold_reason = document.getElementById('holdReason').value.trim(); try { await api(`/purchase-requests/${currentRequestForHold}/hold`, { method: 'PUT', body: JSON.stringify({ hold_reason }) }); showToast('보류 처리되었습니다.'); closeHoldModal(); await loadRequests(); } catch (e) { showToast(e.message, 'error'); } } /* ===== 기타 액션 ===== */ async function revertRequest(requestId) { if (!confirm('이 신청을 대기 상태로 되돌리시겠습니까?')) return; try { await api(`/purchase-requests/${requestId}/revert`, { method: 'PUT' }); showToast('대기 상태로 되돌렸습니다.'); await loadRequests(); } catch (e) { showToast(e.message, 'error'); } } async function deleteRequest(requestId) { if (!confirm('이 구매신청을 삭제하시겠습니까?')) return; try { await api(`/purchase-requests/${requestId}`, { method: 'DELETE' }); showToast('삭제되었습니다.'); await loadRequests(); } catch (e) { showToast(e.message, 'error'); } } /* ===== Init ===== */ (async function() { if (!await initAuth()) return; isAdmin = currentUser && ['admin', 'system', 'system admin'].includes(currentUser.role); await loadInitialData(); await loadRequests(); })();