ERP/frontend/js/components/finished-goods-receipt.js

405 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 成品入库管理
(() => {
let receiptList = [];
let outsourcingOrders = [];
let currentPage = 1;
const pageSize = 20;
Router.register('/outsourcing-mgmt/finished-goods-receipt', async () => {
const html = `
<style>
#receipt-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#receipt-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#receipt-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#receipt-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#receipt-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#receipt-page th {
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: var(--text);
border-bottom: 2px solid var(--border);
font-size: 14px;
white-space: nowrap;
}
#receipt-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#receipt-page tbody tr:hover {
background: var(--hover);
}
</style>
<div id="receipt-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">成品入库管理</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="add-receipt-btn" class="btn btn-primary">新增入库单</button>
</div>
</div>
<div class="content-area">
<div class="filter-section" style="padding: 20px; display: flex; gap: 15px; align-items: center;">
<input type="text" id="search-keyword" class="input" placeholder="搜索入库单号、委外工单号或产品" style="flex: 1; max-width: 400px;" />
<button class="btn btn-secondary" onclick="FinishedGoodsReceipt.search()">搜索</button>
<button class="btn btn-secondary" onclick="FinishedGoodsReceipt.resetSearch()">重置</button>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>入库单号</th>
<th>委外工单号</th>
<th>产品编码</th>
<th>产品名称</th>
<th>入库数量</th>
<th>入库日期</th>
<th>创建人</th>
<th>创建时间</th>
</tr>
</thead>
<tbody id="receipt-list">
<tr><td colspan="8" class="text-center">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="padding: 20px; display: flex; justify-content: center; gap: 8px;"></div>
</div>
</div>
<!-- 新增入库单弹窗 -->
<div id="receipt-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 700px;">
<div class="modal-header">
<h2>新增成品入库单</h2>
<button class="modal-close" onclick="FinishedGoodsReceipt.closeModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>关联委外工单号 <span style="color: var(--danger);">*</span></label>
<select id="outsourcing-order-no" class="input" onchange="FinishedGoodsReceipt.loadOrderInfo()">
<option value="">请选择委外工单</option>
</select>
</div>
<div id="order-info" style="display: none; padding: 15px; background: var(--surface); border-radius: 8px; margin-bottom: 15px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
<div><strong>产品编码:</strong><span id="info-product-code"></span></div>
<div><strong>产品名称:</strong><span id="info-product-name"></span></div>
<div><strong>生产数量:</strong><span id="info-production-qty"></span></div>
<div><strong>外协厂:</strong><span id="info-factory"></span></div>
</div>
</div>
<div class="field">
<label>入库数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="receipt-qty" class="input" min="1" value="1" placeholder="输入实际收到的整机数量" />
</div>
<div class="field">
<label>入库日期 <span style="color: var(--danger);">*</span></label>
<input type="date" id="receipt-date" class="input" />
</div>
<div id="consumption-preview" style="display: none; margin-top: 20px;">
<h3 style="margin-bottom: 10px; color: var(--warning);">预计物料消耗(将从委外在制库存扣减)</h3>
<div class="table-wrapper" style="max-height: 300px; overflow: auto;">
<table>
<thead>
<tr>
<th>物料编码</th>
<th>物料名称</th>
<th>单机用量</th>
<th>预计消耗数量</th>
<th>单位</th>
</tr>
</thead>
<tbody id="consumption-list">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="FinishedGoodsReceipt.closeModal()">取消</button>
<button class="btn btn-primary" onclick="FinishedGoodsReceipt.save()">确认入库</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('add-receipt-btn')?.addEventListener('click', () => openModal());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') search();
});
document.getElementById('receipt-qty')?.addEventListener('input', () => updateConsumptionPreview());
const today = new Date().toISOString().split('T')[0];
const dateInput = document.getElementById('receipt-date');
if (dateInput) dateInput.value = today;
loadList();
loadOutsourcingOrders();
}, 0);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/finished-goods-receipt');
receiptList = res.list || [];
renderList();
} catch (e) {
console.error('加载入库列表失败:', e);
document.getElementById('receipt-list').innerHTML = '<tr><td colspan="8" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
async function loadOutsourcingOrders() {
try {
const res = await API.get('/api/outsourcing-orders');
outsourcingOrders = res.list || [];
const select = document.getElementById('outsourcing-order-no');
if (select) {
select.innerHTML = '<option value="">请选择委外工单</option>' +
outsourcingOrders.map(o => `<option value="${escapeHtml(o.order_no)}">${escapeHtml(o.order_no)} - ${escapeHtml(o.product_name)} (${o.production_qty}台)</option>`).join('');
}
} catch (e) {
console.error('加载委外工单失败:', e);
}
}
let currentOrderMaterials = [];
async function loadOrderInfo() {
const orderNo = document.getElementById('outsourcing-order-no').value;
if (!orderNo) {
document.getElementById('order-info').style.display = 'none';
document.getElementById('consumption-preview').style.display = 'none';
currentOrderMaterials = [];
return;
}
try {
const order = outsourcingOrders.find(o => o.order_no === orderNo);
if (!order) return;
document.getElementById('info-product-code').textContent = order.product_code || '';
document.getElementById('info-product-name').textContent = order.product_name || '';
document.getElementById('info-production-qty').textContent = order.production_qty || 0;
document.getElementById('info-factory').textContent = order.outsourcing_factory || '';
document.getElementById('order-info').style.display = 'block';
const res = await API.get(`/api/outsourcing-orders/${order.id}`);
currentOrderMaterials = res.materials || [];
updateConsumptionPreview();
} catch (e) {
console.error('加载工单信息失败:', e);
alert('加载工单信息失败');
}
}
function updateConsumptionPreview() {
const receiptQty = parseInt(document.getElementById('receipt-qty')?.value) || 0;
if (receiptQty <= 0 || currentOrderMaterials.length === 0) {
document.getElementById('consumption-preview').style.display = 'none';
return;
}
document.getElementById('consumption-preview').style.display = 'block';
document.getElementById('consumption-list').innerHTML = currentOrderMaterials.map(m => {
const consumedQty = Math.ceil(m.bom_unit_qty * receiptQty);
return `
<tr>
<td>${escapeHtml(m.material_code)}</td>
<td>${escapeHtml(m.material_name)}</td>
<td>${m.bom_unit_qty}</td>
<td style="font-weight: 600; color: var(--warning);">${consumedQty}</td>
<td>${escapeHtml(m.unit)}</td>
</tr>
`;
}).join('');
}
function renderList() {
const tbody = document.getElementById('receipt-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = receiptList;
if (keyword) {
filtered = receiptList.filter(item =>
(item.receipt_no || '').toLowerCase().includes(keyword) ||
(item.outsourcing_order_no || '').toLowerCase().includes(keyword) ||
(item.product_code || '').toLowerCase().includes(keyword) ||
(item.product_name || '').toLowerCase().includes(keyword)
);
}
const totalPages = Math.ceil(filtered.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
if (pageData.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.receipt_no || '')}</span></td>
<td>${escapeHtml(item.outsourcing_order_no || '')}</td>
<td>${escapeHtml(item.product_code || '')}</td>
<td>${escapeHtml(item.product_name || '')}</td>
<td style="font-weight: 600; color: var(--success);">${item.receipt_qty || 0}</td>
<td>${escapeHtml(item.receipt_date || '')}</td>
<td>${escapeHtml(item.created_by || '')}</td>
<td>${formatTime(item.created_at)}</td>
</tr>
`).join('');
}
renderPagination(totalPages);
}
function renderPagination(totalPages) {
const container = document.getElementById('pagination');
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="FinishedGoodsReceipt.goPage(${currentPage - 1})">上一页</button>`;
html += `<span style="padding: 0 12px; line-height: 32px;">第 ${currentPage} / ${totalPages} 页</span>`;
html += `<button class="btn btn-sm btn-secondary" ${currentPage >= totalPages ? 'disabled' : ''} onclick="FinishedGoodsReceipt.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openModal() {
document.getElementById('outsourcing-order-no').value = '';
document.getElementById('receipt-qty').value = 1;
const today = new Date().toISOString().split('T')[0];
document.getElementById('receipt-date').value = today;
document.getElementById('order-info').style.display = 'none';
document.getElementById('consumption-preview').style.display = 'none';
currentOrderMaterials = [];
document.getElementById('receipt-modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('receipt-modal').style.display = 'none';
}
async function save() {
const outsourcing_order_no = document.getElementById('outsourcing-order-no').value.trim();
const receipt_qty = parseInt(document.getElementById('receipt-qty').value) || 0;
const receipt_date = document.getElementById('receipt-date').value.trim();
if (!outsourcing_order_no || !receipt_date) {
alert('请填写所有必填字段');
return;
}
if (receipt_qty <= 0) {
alert('入库数量必须大于0');
return;
}
try {
const res = await API.post('/api/finished-goods-receipt', {
outsourcing_order_no,
receipt_qty,
receipt_date
});
let message = `入库成功,入库单号:${res.receipt_no}`;
if (res.material_consumption && res.material_consumption.length > 0) {
message += '\n\n物料消耗明细';
res.material_consumption.forEach(m => {
message += `\n${m.material_name}: ${m.consumed_qty}`;
});
}
alert(message);
closeModal();
loadList();
} catch (e) {
alert(e.message || '入库失败');
}
}
function search() {
currentPage = 1;
renderList();
}
function resetSearch() {
document.getElementById('search-keyword').value = '';
currentPage = 1;
renderList();
}
function goPage(page) {
currentPage = page;
renderList();
}
function formatTime(ts) {
if (!ts) return '-';
try {
const d = new Date(ts);
return d.toLocaleString('zh-CN', { hour12: false });
} catch {
return ts;
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.FinishedGoodsReceipt = {
search,
resetSearch,
closeModal,
save,
goPage,
loadOrderInfo
};
})();