430 lines
16 KiB
JavaScript
430 lines
16 KiB
JavaScript
|
|
// 委外工单管理
|
|||
|
|
(() => {
|
|||
|
|
let orderList = [];
|
|||
|
|
let customerOrders = [];
|
|||
|
|
let currentPage = 1;
|
|||
|
|
const pageSize = 20;
|
|||
|
|
|
|||
|
|
const statusMap = {
|
|||
|
|
'pending': { text: '待发料', color: 'var(--warning)' },
|
|||
|
|
'issued': { text: '已发料', color: 'var(--info)' },
|
|||
|
|
'completed': { text: '已完成', color: 'var(--success)' },
|
|||
|
|
'cancelled': { text: '已取消', color: 'var(--text-2)' }
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
Router.register('/outsourcing-mgmt/orders', async () => {
|
|||
|
|
const html = `
|
|||
|
|
<style>
|
|||
|
|
#outsourcing-orders-page .page-header {
|
|||
|
|
padding: 20px;
|
|||
|
|
background: var(--surface);
|
|||
|
|
border-bottom: 1px solid var(--border);
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page .content-area {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 20px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page .table-wrapper {
|
|||
|
|
flex: 1;
|
|||
|
|
overflow: auto;
|
|||
|
|
background: var(--surface);
|
|||
|
|
border: 1px solid var(--border);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page table {
|
|||
|
|
width: 100%;
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page thead {
|
|||
|
|
background: var(--border);
|
|||
|
|
position: sticky;
|
|||
|
|
top: 0;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page td {
|
|||
|
|
padding: 8px 12px;
|
|||
|
|
border-bottom: 1px solid var(--border);
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page tbody tr:hover {
|
|||
|
|
background: var(--hover);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page .btn-text {
|
|||
|
|
background: none;
|
|||
|
|
border: none;
|
|||
|
|
color: var(--primary);
|
|||
|
|
cursor: pointer;
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
margin-right: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#outsourcing-orders-page .btn-text:hover {
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|
|||
|
|
<div id="outsourcing-orders-page">
|
|||
|
|
<div class="page-header">
|
|||
|
|
<h1 style="margin: 0; font-size: 24px;">委外工单管理</h1>
|
|||
|
|
<div class="page-actions" style="margin-top: 16px;">
|
|||
|
|
<button id="add-order-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="OutsourcingOrders.search()">搜索</button>
|
|||
|
|
<button class="btn btn-secondary" onclick="OutsourcingOrders.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>
|
|||
|
|
<th>创建时间</th>
|
|||
|
|
<th>操作</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="order-list">
|
|||
|
|
<tr><td colspan="11" 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="order-modal" class="modal" style="display:none;">
|
|||
|
|
<div class="modal-content" style="max-width: 700px;">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h2 id="modal-title">新增委外工单</h2>
|
|||
|
|
<button class="modal-close" onclick="OutsourcingOrders.closeModal()">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<div class="field">
|
|||
|
|
<label>客户订单号 <span style="color: var(--danger);">*</span></label>
|
|||
|
|
<select id="customer-order-no" class="input">
|
|||
|
|
<option value="">请选择客户订单</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>成品编码 <span style="color: var(--danger);">*</span></label>
|
|||
|
|
<input type="text" id="product-code" class="input" placeholder="输入成品编码" />
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>成品名称 <span style="color: var(--danger);">*</span></label>
|
|||
|
|
<input type="text" id="product-name" class="input" placeholder="输入成品名称" />
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>生产数量 <span style="color: var(--danger);">*</span></label>
|
|||
|
|
<input type="number" id="production-qty" class="input" min="1" value="1" placeholder="输入生产数量" />
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>外协厂 <span style="color: var(--danger);">*</span></label>
|
|||
|
|
<input type="text" id="outsourcing-factory" class="input" placeholder="输入外协厂名称" />
|
|||
|
|
</div>
|
|||
|
|
<div class="field">
|
|||
|
|
<label>交期 <span style="color: var(--danger);">*</span></label>
|
|||
|
|
<input type="date" id="delivery-date" class="input" />
|
|||
|
|
</div>
|
|||
|
|
<div id="material-list-section" style="display: none; margin-top: 20px;">
|
|||
|
|
<h3 style="margin-bottom: 10px;">物料明细(根据BOM自动计算)</h3>
|
|||
|
|
<div class="table-wrapper" style="max-height: 300px; overflow: auto;">
|
|||
|
|
<table>
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>物料编码</th>
|
|||
|
|
<th>物料名称</th>
|
|||
|
|
<th>BOM用量</th>
|
|||
|
|
<th>需发数量</th>
|
|||
|
|
<th>单位</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody id="material-list">
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer">
|
|||
|
|
<button class="btn btn-secondary" onclick="OutsourcingOrders.closeModal()">取消</button>
|
|||
|
|
<button class="btn btn-primary" onclick="OutsourcingOrders.save()">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
document.getElementById('add-order-btn')?.addEventListener('click', () => openModal());
|
|||
|
|
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
|
|||
|
|
if (e.key === 'Enter') search();
|
|||
|
|
});
|
|||
|
|
loadList();
|
|||
|
|
loadCustomerOrders();
|
|||
|
|
}, 0);
|
|||
|
|
|
|||
|
|
return html;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
async function loadList() {
|
|||
|
|
try {
|
|||
|
|
const res = await API.get('/api/outsourcing-orders');
|
|||
|
|
orderList = res.list || [];
|
|||
|
|
renderList();
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('加载委外工单失败:', e);
|
|||
|
|
document.getElementById('order-list').innerHTML = '<tr><td colspan="11" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadCustomerOrders() {
|
|||
|
|
try {
|
|||
|
|
const res = await API.get('/api/customer-orders-for-outsourcing');
|
|||
|
|
customerOrders = res.list || [];
|
|||
|
|
const select = document.getElementById('customer-order-no');
|
|||
|
|
if (select) {
|
|||
|
|
select.innerHTML = '<option value="">请选择客户订单</option>' +
|
|||
|
|
customerOrders.map(o => `<option value="${escapeHtml(o.order_no)}">${escapeHtml(o.order_no)} - ${escapeHtml(o.customer_name)} - ${escapeHtml(o.material)}</option>`).join('');
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('加载客户订单失败:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderList() {
|
|||
|
|
const tbody = document.getElementById('order-list');
|
|||
|
|
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
|
|||
|
|
|
|||
|
|
let filtered = orderList;
|
|||
|
|
if (keyword) {
|
|||
|
|
filtered = orderList.filter(item =>
|
|||
|
|
(item.order_no || '').toLowerCase().includes(keyword) ||
|
|||
|
|
(item.customer_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="11" class="text-center">暂无数据</td></tr>';
|
|||
|
|
} else {
|
|||
|
|
tbody.innerHTML = pageData.map(item => {
|
|||
|
|
const status = statusMap[item.status] || { text: item.status, color: 'var(--text-2)' };
|
|||
|
|
return `
|
|||
|
|
<tr>
|
|||
|
|
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.order_no || '')}</span></td>
|
|||
|
|
<td>${escapeHtml(item.customer_order_no || '')}</td>
|
|||
|
|
<td>${escapeHtml(item.product_code || '')}</td>
|
|||
|
|
<td>${escapeHtml(item.product_name || '')}</td>
|
|||
|
|
<td style="font-weight: 600; color: var(--primary);">${item.production_qty || 0}</td>
|
|||
|
|
<td>${escapeHtml(item.outsourcing_factory || '')}</td>
|
|||
|
|
<td>${escapeHtml(item.delivery_date || '')}</td>
|
|||
|
|
<td><span style="padding: 2px 8px; border-radius: 4px; background: ${status.color}20; color: ${status.color}; font-size: 12px;">${status.text}</span></td>
|
|||
|
|
<td>${escapeHtml(item.created_by || '')}</td>
|
|||
|
|
<td>${formatTime(item.created_at)}</td>
|
|||
|
|
<td>
|
|||
|
|
<button class="btn btn-sm btn-secondary" onclick="OutsourcingOrders.viewDetail(${item.id})">查看</button>
|
|||
|
|
<button class="btn btn-sm btn-danger" onclick="OutsourcingOrders.deleteOrder(${item.id})">删除</button>
|
|||
|
|
</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="OutsourcingOrders.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="OutsourcingOrders.goPage(${currentPage + 1})">下一页</button>`;
|
|||
|
|
container.innerHTML = html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let editingId = null;
|
|||
|
|
|
|||
|
|
function openModal(item = null) {
|
|||
|
|
editingId = item?.id || null;
|
|||
|
|
document.getElementById('modal-title').textContent = item ? '编辑委外工单' : '新增委外工单';
|
|||
|
|
document.getElementById('customer-order-no').value = item?.customer_order_no || '';
|
|||
|
|
document.getElementById('product-code').value = item?.product_code || '';
|
|||
|
|
document.getElementById('product-name').value = item?.product_name || '';
|
|||
|
|
document.getElementById('production-qty').value = item?.production_qty || 1;
|
|||
|
|
document.getElementById('outsourcing-factory').value = item?.outsourcing_factory || '';
|
|||
|
|
document.getElementById('delivery-date').value = item?.delivery_date || '';
|
|||
|
|
document.getElementById('material-list-section').style.display = 'none';
|
|||
|
|
document.getElementById('order-modal').style.display = 'flex';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function closeModal() {
|
|||
|
|
document.getElementById('order-modal').style.display = 'none';
|
|||
|
|
editingId = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function save() {
|
|||
|
|
const data = {
|
|||
|
|
customer_order_no: document.getElementById('customer-order-no').value.trim(),
|
|||
|
|
product_code: document.getElementById('product-code').value.trim(),
|
|||
|
|
product_name: document.getElementById('product-name').value.trim(),
|
|||
|
|
production_qty: parseInt(document.getElementById('production-qty').value) || 0,
|
|||
|
|
outsourcing_factory: document.getElementById('outsourcing-factory').value.trim(),
|
|||
|
|
delivery_date: document.getElementById('delivery-date').value.trim()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (!data.customer_order_no || !data.product_code || !data.product_name || !data.outsourcing_factory || !data.delivery_date) {
|
|||
|
|
alert('请填写所有必填字段');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (data.production_qty <= 0) {
|
|||
|
|
alert('生产数量必须大于0');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (editingId) {
|
|||
|
|
await API.put(`/api/outsourcing-orders/${editingId}`, data);
|
|||
|
|
alert('更新成功');
|
|||
|
|
} else {
|
|||
|
|
const res = await API.post('/api/outsourcing-orders', data);
|
|||
|
|
alert(`委外工单创建成功,工单号:${res.order_no}`);
|
|||
|
|
}
|
|||
|
|
closeModal();
|
|||
|
|
loadList();
|
|||
|
|
} catch (e) {
|
|||
|
|
alert(e.message || '操作失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function viewDetail(id) {
|
|||
|
|
try {
|
|||
|
|
const res = await API.get(`/api/outsourcing-orders/${id}`);
|
|||
|
|
const order = res.order;
|
|||
|
|
const materials = res.materials || [];
|
|||
|
|
|
|||
|
|
document.getElementById('modal-title').textContent = '查看委外工单';
|
|||
|
|
document.getElementById('customer-order-no').value = order.customer_order_no || '';
|
|||
|
|
document.getElementById('product-code').value = order.product_code || '';
|
|||
|
|
document.getElementById('product-name').value = order.product_name || '';
|
|||
|
|
document.getElementById('production-qty').value = order.production_qty || 0;
|
|||
|
|
document.getElementById('outsourcing-factory').value = order.outsourcing_factory || '';
|
|||
|
|
document.getElementById('delivery-date').value = order.delivery_date || '';
|
|||
|
|
|
|||
|
|
if (materials.length > 0) {
|
|||
|
|
document.getElementById('material-list-section').style.display = 'block';
|
|||
|
|
document.getElementById('material-list').innerHTML = materials.map(m => `
|
|||
|
|
<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(--primary);">${m.need_qty}</td>
|
|||
|
|
<td>${escapeHtml(m.unit)}</td>
|
|||
|
|
</tr>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById('order-modal').style.display = 'flex';
|
|||
|
|
} catch (e) {
|
|||
|
|
alert(e.message || '获取详情失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function deleteOrder(id) {
|
|||
|
|
if (!confirm('确定要删除这条委外工单吗?')) return;
|
|||
|
|
try {
|
|||
|
|
await API.delete(`/api/outsourcing-orders/${id}`);
|
|||
|
|
alert('删除成功');
|
|||
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.OutsourcingOrders = {
|
|||
|
|
search,
|
|||
|
|
resetSearch,
|
|||
|
|
viewDetail,
|
|||
|
|
deleteOrder,
|
|||
|
|
closeModal,
|
|||
|
|
save,
|
|||
|
|
goPage
|
|||
|
|
};
|
|||
|
|
})();
|