ERP/frontend/js/components/outsourcing-material-issue.js

385 lines
13 KiB
JavaScript

// 委外发料管理
(() => {
let issueList = [];
let outsourcingOrders = [];
let currentPage = 1;
const pageSize = 20;
Router.register('/outsourcing-mgmt/material-issue', async () => {
const html = `
<style>
#material-issue-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#material-issue-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#material-issue-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#material-issue-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#material-issue-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#material-issue-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;
}
#material-issue-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#material-issue-page tbody tr:hover {
background: var(--hover);
}
</style>
<div id="material-issue-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">委外发料管理</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="add-issue-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="MaterialIssue.search()">搜索</button>
<button class="btn btn-secondary" onclick="MaterialIssue.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>
<th>创建时间</th>
</tr>
</thead>
<tbody id="issue-list">
<tr><td colspan="9" 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="issue-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2>新增委外发料单</h2>
<button class="modal-close" onclick="MaterialIssue.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="MaterialIssue.loadOrderMaterials()">
<option value="">请选择委外工单</option>
</select>
</div>
<div class="field">
<label>发料日期 <span style="color: var(--danger);">*</span></label>
<input type="date" id="issue-date" class="input" />
</div>
<div id="material-section" style="display: none; margin-top: 20px;">
<h3 style="margin-bottom: 10px;">发料明细</h3>
<div class="table-wrapper" style="max-height: 400px; overflow: auto;">
<table>
<thead>
<tr>
<th>物料编码</th>
<th>物料名称</th>
<th>BOM用量</th>
<th>需发数量</th>
<th>实际发料数量 <span style="color: var(--danger);">*</span></th>
<th>单位</th>
</tr>
</thead>
<tbody id="material-list">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="MaterialIssue.closeModal()">取消</button>
<button class="btn btn-primary" onclick="MaterialIssue.save()">确认发料</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('add-issue-btn')?.addEventListener('click', () => openModal());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') search();
});
const today = new Date().toISOString().split('T')[0];
const dateInput = document.getElementById('issue-date');
if (dateInput) dateInput.value = today;
loadList();
loadOutsourcingOrders();
}, 0);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/outsourcing-material-issue');
issueList = res.list || [];
renderList();
} catch (e) {
console.error('加载发料列表失败:', e);
document.getElementById('issue-list').innerHTML = '<tr><td colspan="9" 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);
}
}
async function loadOrderMaterials() {
const orderNo = document.getElementById('outsourcing-order-no').value;
if (!orderNo) {
document.getElementById('material-section').style.display = 'none';
return;
}
try {
const order = outsourcingOrders.find(o => o.order_no === orderNo);
if (!order) return;
const res = await API.get(`/api/outsourcing-orders/${order.id}`);
const materials = res.materials || [];
if (materials.length > 0) {
document.getElementById('material-section').style.display = 'block';
document.getElementById('material-list').innerHTML = materials.map((m, idx) => `
<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(--info);">${m.need_qty}</td>
<td>
<input type="number"
id="issue-qty-${idx}"
class="input"
min="0"
value="${m.need_qty}"
style="width: 100px;"
data-material-code="${escapeHtml(m.material_code)}"
data-material-name="${escapeHtml(m.material_name)}"
data-unit="${escapeHtml(m.unit)}" />
</td>
<td>${escapeHtml(m.unit)}</td>
</tr>
`).join('');
}
} catch (e) {
console.error('加载物料明细失败:', e);
alert('加载物料明细失败');
}
}
function renderList() {
const tbody = document.getElementById('issue-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = issueList;
if (keyword) {
filtered = issueList.filter(item =>
(item.issue_no || '').toLowerCase().includes(keyword) ||
(item.outsourcing_order_no || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_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="9" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.issue_no || '')}</span></td>
<td>${escapeHtml(item.outsourcing_order_no || '')}</td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td style="font-weight: 600; color: var(--primary);">${item.issue_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${escapeHtml(item.issue_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="MaterialIssue.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="MaterialIssue.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openModal() {
document.getElementById('outsourcing-order-no').value = '';
const today = new Date().toISOString().split('T')[0];
document.getElementById('issue-date').value = today;
document.getElementById('material-section').style.display = 'none';
document.getElementById('issue-modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('issue-modal').style.display = 'none';
}
async function save() {
const outsourcing_order_no = document.getElementById('outsourcing-order-no').value.trim();
const issue_date = document.getElementById('issue-date').value.trim();
if (!outsourcing_order_no || !issue_date) {
alert('请填写所有必填字段');
return;
}
const materials = [];
const inputs = document.querySelectorAll('[id^="issue-qty-"]');
inputs.forEach(input => {
const qty = parseInt(input.value) || 0;
if (qty > 0) {
materials.push({
material_code: input.dataset.materialCode,
material_name: input.dataset.materialName,
issue_qty: qty,
unit: input.dataset.unit
});
}
});
if (materials.length === 0) {
alert('请至少填写一条发料数量');
return;
}
try {
const res = await API.post('/api/outsourcing-material-issue', {
outsourcing_order_no,
materials,
issue_date
});
alert(`发料成功,发料单号:${res.issue_no}`);
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.MaterialIssue = {
search,
resetSearch,
closeModal,
save,
goPage,
loadOrderMaterials
};
})();