aps-agent/aps-frontend/pages/orders.html

348 lines
24 KiB
HTML
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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>销售订单 - 智能排产系统</title>
<link rel="stylesheet" href="../css/common.css">
</head>
<body>
<div id="app">
<header class="header" id="header"></header>
<div class="body">
<aside class="sidebar" id="sidebar"></aside>
<main class="main">
<h1 class="page-title">销售订单</h1>
<p class="page-desc">支持订单录入、提交审核、优先级评分、订单变更与变更历史追溯。</p>
<div class="toolbar">
<button class="btn btn-primary" onclick="openCreateModal()">+ 新增订单</button>
<button class="btn" onclick="importModal()">导入 Excel</button>
<button class="btn" onclick="priorityConfigModal()">优先级权重</button>
<div class="search-box"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><input id="searchInput" placeholder="搜索订单号/客户/产品" oninput="render()"></div>
<select id="statusFilter" class="btn" style="padding:7px 10px" onchange="render()">
<option value="">全部状态</option>
<option value="DRAFT">草稿</option>
<option value="SUBMITTED">待审核</option>
<option value="CONFIRMED">已确认</option>
<option value="REJECTED">已退回</option>
<option value="IN_PRODUCTION">生产中</option>
<option value="COMPLETED">已完成</option>
<option value="CANCELLED">已取消</option>
</select>
</div>
<div class="card">
<div class="card-body table-wrap">
<table id="orderTable">
<thead><tr><th>订单号</th><th>客户</th><th>产品/数量</th><th>交期</th><th>优先级</th><th>来源</th><th>状态</th><th>操作</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div class="card-footer flex justify-between">
<span id="totalInfo" class="text-muted"></span>
<div id="pagination" class="pagination"></div>
</div>
</div>
</main>
</div>
</div>
<script src="../js/data.js"></script>
<script src="../js/common.js"></script>
<script>
initPage('orders');
let page = 1, pageSize = 10;
function levelScore(level) { return { VIP: 10, A: 8, B: 5, C: 3 }[level] || 3; }
function urgencyScore(deliveryDate) {
const days = Math.ceil((apsData.$utils.parseDate(deliveryDate + ' 00:00') - apsData.$utils.today()) / (24 * 60 * 60 * 1000));
if (days <= 3) return 10; if (days <= 7) return 7; if (days <= 14) return 5; return 3;
}
function amountScore(amount) { return Math.min(10, Math.round(amount / 50000)); }
function computePriorityScore(o, manual) {
const w = apsData.scheduleParams.priorityWeights || {
customer_level_weight: 0.30, urgency_weight: 0.35, amount_weight: 0.15, manual_weight: 0.20
};
const s1 = levelScore(o.customerLevel);
const s2 = urgencyScore(o.deliveryDate);
const s3 = amountScore(o.totalAmount);
const s4 = manual || o.manualPriority || 5;
return (w.customer_level_weight * s1 + w.urgency_weight * s2 + w.amount_weight * s3 + w.manual_weight * s4).toFixed(2);
}
function render(pg) {
if (pg) page = pg;
const kw = document.getElementById('searchInput').value.toLowerCase();
const st = document.getElementById('statusFilter').value;
let rows = apsData.salesOrders.filter(o => {
if (st && o.status !== st) return false;
const text = (o.orderNo + ' ' + o.customerName + ' ' + o.items.map(i => i.productName).join(' ')).toLowerCase();
return !kw || text.includes(kw);
});
rows.sort((a, b) => b.id - a.id);
const total = rows.length;
const start = (page - 1) * pageSize;
rows = rows.slice(start, start + pageSize);
const tbody = document.querySelector('#orderTable tbody');
tbody.innerHTML = rows.map(o => {
const item = o.items[0];
return `<tr>
<td><a href="javascript:viewOrder(${o.id})">${o.orderNo}</a></td>
<td>${o.customerName} <span class="badge badge-${o.customerLevel === 'VIP' ? 'danger' : (o.customerLevel === 'A' ? 'primary' : 'default')}">${o.customerLevel}</span></td>
<td>${item ? item.productName + ' × ' + item.quantity : '-'}</td>
<td>${o.deliveryDate}</td>
<td>${o.priority}</td>
<td>${o.source}</td>
<td>${statusBadge(o.status, { DRAFT:'default', SUBMITTED:'warning', CONFIRMED:'primary', REJECTED:'danger', IN_PRODUCTION:'warning', COMPLETED:'success', CANCELLED:'danger' })}</td>
<td class="actions">
<button class="btn btn-sm" onclick="viewOrder(${o.id})">详情</button>
${o.status === 'DRAFT' ? `<button class="btn btn-sm btn-primary" onclick="submitAudit(${o.id})">提交审核</button>` : ''}
${o.status === 'SUBMITTED' ? `<button class="btn btn-sm btn-success" onclick="auditOrder(${o.id})">审核</button>` : ''}
${o.status === 'DRAFT' || o.status === 'REJECTED' ? `<button class="btn btn-sm" onclick="editOrder(${o.id})">编辑</button>` : ''}
${o.status === 'CONFIRMED' || o.status === 'IN_PRODUCTION' ? `<button class="btn btn-sm btn-warning" onclick="changeOrder(${o.id})">变更</button>` : ''}
${o.status !== 'CANCELLED' && o.status !== 'COMPLETED' ? `<button class="btn btn-sm btn-danger" onclick="cancelOrder(${o.id})">取消</button>` : ''}
<button class="btn btn-sm" onclick="historyModal(${o.id})">历史</button>
</td>
</tr>`;
}).join('') || '<tr><td colspan="8" class="text-center text-muted">无数据</td></tr>';
document.getElementById('totalInfo').textContent = `共 ${total} 条`;
renderPagination('pagination', page, total, pageSize, 'render');
}
function openCreateModal() {
const products = apsData.materials.filter(m => m.type === 'FINISHED_PRODUCT');
const opts = products.map(p => `<option value="${p.id}">${p.name} (${p.code})</option>`).join('');
openModal('新增销售订单', `
<div class="form-row"><div class="form-group"><label>客户名称 *</label><input id="c_customer" value="青岛科技"></div><div class="form-group"><label>客户等级</label><select id="c_level"><option>VIP</option><option>A</option><option>B</option><option>C</option></select></div></div>
<div class="form-row"><div class="form-group"><label>产品 *</label><select id="c_product">${opts}</select></div><div class="form-group"><label>数量 *</label><input id="c_qty" type="number" value="500"></div></div>
<div class="form-row"><div class="form-group"><label>交期 *</label><input id="c_delivery" type="date"></div><div class="form-group"><label>优先级 (1-10)</label><input id="c_priority" type="number" min="1" max="10" value="5"></div></div>
<div class="form-row"><div class="form-group"><label>特殊要求</label><textarea id="c_note" rows="2"></textarea></div></div>
`, `
<button class="btn" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="saveOrder()">保存</button>
`);
document.getElementById('c_delivery').value = apsData.$utils.fmtDate(apsData.$utils.addMinutes(apsData.$utils.today(), 7 * 24 * 60));
}
function saveOrder() {
const customer = document.getElementById('c_customer').value.trim();
const qty = Number(document.getElementById('c_qty').value);
const delivery = document.getElementById('c_delivery').value;
const priority = Number(document.getElementById('c_priority').value);
const productId = Number(document.getElementById('c_product').value);
if (!customer || !qty || !delivery || priority < 1 || priority > 10) return showToast('校验失败', '请填写完整且正确的信息', 'error');
const product = apsData.materials.find(m => m.id == productId);
const id = apsData.$utils.nextId('salesOrder');
const orderNo = 'SO' + delivery.replace(/-/g, '') + String(id).padStart(3, '0');
apsData.salesOrders.push({
id, orderNo, customerId: 'CUST' + String(id).padStart(3, '0'), customerName: customer,
customerLevel: document.getElementById('c_level').value, orderDate: apsData.$utils.fmtDate(apsData.$utils.today()),
deliveryDate: delivery, priority, manualPriority: priority, status: 'DRAFT', source: 'MANUAL',
specialRequirements: document.getElementById('c_note').value, totalAmount: qty * 150, isRush: false, rushStrategy: null,
changes: [], createdBy: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()), updatedAt: apsData.$utils.fmtDateTime(new Date()),
items: [{ id: apsData.$utils.nextId('salesOrderItem'), orderId: id, lineNo: 1, productId: product.id, productName: product.name, productCode: product.code, quantity: qty, unit: '件', bomVersion: 'V2.1', routingVersion: 'V1.0', status: 'PENDING', note: '' }]
});
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'ORDER', operationType: 'CREATE', targetType: 'SALES_ORDER', targetId: id, action: '创建销售订单', description: `订单 ${orderNo} 已创建`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData);
closeModal(); showToast('保存成功', `订单 ${orderNo} 已创建`); render();
}
function submitAudit(id) {
const o = findById(apsData.salesOrders, id);
o.status = 'SUBMITTED'; o.updatedAt = apsData.$utils.fmtDateTime(new Date());
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'ORDER', operationType: 'SUBMIT', targetType: 'SALES_ORDER', targetId: id, action: '提交审核', description: `订单 ${o.orderNo} 已提交审核`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData); showToast('提交成功', `订单 ${o.orderNo} 已进入审核流程`); render();
}
function auditOrder(id) {
const o = findById(apsData.salesOrders, id);
const score = computePriorityScore(o, o.manualPriority || o.priority);
openModal('订单审核', `
<div class="form-row"><div class="form-group"><label>订单号</label><input value="${o.orderNo}" readonly></div><div class="form-group"><label>客户等级</label><input value="${o.customerLevel}" readonly></div></div>
<div class="form-row"><div class="form-group"><label>产品/数量</label><input value="${o.items[0].productName} × ${o.items[0].quantity}" readonly></div><div class="form-group"><label>订单金额</label><input value="${o.totalAmount.toLocaleString()}" readonly></div></div>
<div class="form-row"><div class="form-group"><label>交期</label><input value="${o.deliveryDate}" readonly></div><div class="form-group"><label>当前优先级</label><input id="a_priority" type="number" min="1" max="10" value="${o.priority}" oninput="updateAuditScore(${id})"></div></div>
<div class="card mt-2" style="padding:12px">
<div class="flex justify-between"><span>客户等级得分</span><strong>${levelScore(o.customerLevel)}</strong></div>
<div class="flex justify-between"><span>交期紧迫度得分</span><strong id="a_urgency">${urgencyScore(o.deliveryDate)}</strong></div>
<div class="flex justify-between"><span>订单金额得分</span><strong id="a_amount">${amountScore(o.totalAmount)}</strong></div>
<div class="flex justify-between"><span>手工优先级得分</span><strong id="a_manual">${o.manualPriority || o.priority}</strong></div>
<div class="flex justify-between mt-2 text-primary"><span>综合优先级评分</span><strong id="a_score">${score}</strong></div>
</div>
<div class="form-row mt-2"><div class="form-group"><label>审核意见 / 退回原因</label><textarea id="a_reason" rows="2"></textarea></div></div>
`, `
<button class="btn" onclick="closeModal()">取消</button>
<button class="btn btn-danger" onclick="rejectOrder(${id})">退回</button>
<button class="btn btn-success" onclick="approveOrder(${id})">通过</button>
`);
}
function updateAuditScore(id) {
const o = findById(apsData.salesOrders, id);
const manual = Number(document.getElementById('a_priority').value);
document.getElementById('a_manual').textContent = manual;
document.getElementById('a_score').textContent = computePriorityScore(o, manual);
}
function approveOrder(id) {
const o = findById(apsData.salesOrders, id);
o.priority = Number(document.getElementById('a_priority').value);
o.manualPriority = o.priority;
o.status = 'CONFIRMED';
o.updatedAt = apsData.$utils.fmtDateTime(new Date());
addChange(id, 'PRIORITY', o.priority, o.priority, '审核通过并确认优先级');
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'ORDER', operationType: 'AUDIT', targetType: 'SALES_ORDER', targetId: id, action: '审核通过', description: `订单 ${o.orderNo} 审核通过,优先级 ${o.priority}`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData); closeModal(); showToast('审核通过', o.orderNo); render();
}
function rejectOrder(id) {
const o = findById(apsData.salesOrders, id);
const reason = document.getElementById('a_reason').value || '审核不通过';
o.status = 'REJECTED';
o.updatedAt = apsData.$utils.fmtDateTime(new Date());
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'WARNING', category: 'ORDER', operationType: 'AUDIT', targetType: 'SALES_ORDER', targetId: id, action: '审核退回', description: `订单 ${o.orderNo} 被退回:${reason}`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData); closeModal(); showToast('已退回', reason); render();
}
function changeOrder(id) {
const o = findById(apsData.salesOrders, id);
const scheduled = apsData.productionOrders.some(po => po.salesOrderId == id);
openModal('订单变更', `
<div class="form-row"><div class="form-group"><label>订单号</label><input value="${o.orderNo}" readonly></div><div class="form-group"><label>产品</label><input value="${o.items[0].productName}" readonly></div></div>
<div class="form-row"><div class="form-group"><label>数量 (原 ${o.items[0].quantity})</label><input id="ch_qty" type="number" value="${o.items[0].quantity}"></div><div class="form-group"><label>交期 (原 ${o.deliveryDate})</label><input id="ch_delivery" type="date" value="${o.deliveryDate}"></div></div>
<div class="form-row"><div class="form-group"><label>优先级</label><input id="ch_priority" type="number" min="1" max="10" value="${o.priority}"></div></div>
<div class="form-row"><div class="form-group"><label>变更原因</label><textarea id="ch_reason" rows="2"></textarea></div></div>
${scheduled ? '<div class="badge badge-warning mt-2">该订单已排产,变更后请重新执行排产。</div>' : ''}
`, `
<button class="btn" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="doChangeOrder(${id})">保存变更</button>
`);
}
function doChangeOrder(id) {
const o = findById(apsData.salesOrders, id);
const newQty = Number(document.getElementById('ch_qty').value);
const newDelivery = document.getElementById('ch_delivery').value;
const newPriority = Number(document.getElementById('ch_priority').value);
const reason = document.getElementById('ch_reason').value || '订单变更';
if (!newQty || !newDelivery || newPriority < 1 || newPriority > 10) return showToast('校验失败', '请填写有效信息', 'error');
const oldQty = o.items[0].quantity;
const oldDelivery = o.deliveryDate;
const oldPriority = o.priority;
if (newQty !== oldQty) { o.items[0].quantity = newQty; addChange(id, 'QUANTITY', oldQty, newQty, reason); }
if (newDelivery !== oldDelivery) { o.deliveryDate = newDelivery; addChange(id, 'DELIVERY', oldDelivery, newDelivery, reason); }
if (newPriority !== oldPriority) { o.priority = newPriority; o.manualPriority = newPriority; addChange(id, 'PRIORITY', oldPriority, newPriority, reason); }
o.totalAmount = newQty * 150;
o.updatedAt = apsData.$utils.fmtDateTime(new Date());
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'ORDER', operationType: 'CHANGE', targetType: 'SALES_ORDER', targetId: id, action: '订单变更', description: `订单 ${o.orderNo} 发生变更`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData); closeModal(); showToast('变更已保存', o.orderNo); render();
}
function addChange(orderId, type, oldVal, newVal, reason) {
const o = findById(apsData.salesOrders, orderId);
o.changes.push({
id: apsData.$utils.nextId('orderChange'),
changeType: type,
oldValue: String(oldVal),
newValue: String(newVal),
reason,
operator: 'admin',
createdAt: apsData.$utils.fmtDateTime(new Date())
});
}
function viewOrder(id) {
const o = findById(apsData.salesOrders, id);
const item = o.items[0];
const pos = apsData.productionOrders.filter(po => po.salesOrderId == id).map(po => `<li>${po.orderNo} (${po.lineName}) ${po.plannedStartDate ? po.plannedStartDate.slice(0,10) : ''} ~ ${po.plannedEndDate ? po.plannedEndDate.slice(0,10) : ''}</li>`).join('') || '<li class="text-muted">尚未排产</li>';
openModal('订单详情', `
<div class="form-row"><div class="form-group"><label>订单号</label><input value="${o.orderNo}" readonly></div><div class="form-group"><label>客户</label><input value="${o.customerName} (${o.customerLevel})" readonly></div></div>
<div class="form-row"><div class="form-group"><label>产品</label><input value="${item.productName} × ${item.quantity}" readonly></div><div class="form-group"><label>交期</label><input value="${o.deliveryDate}" readonly></div></div>
<div class="form-row"><div class="form-group"><label>优先级</label><input value="${o.priority}" readonly></div><div class="form-group"><label>来源</label><input value="${o.source}" readonly></div></div>
<div class="form-row"><div class="form-group"><label>特殊要求</label><textarea readonly>${o.specialRequirements}</textarea></div></div>
<p class="mt-2"><strong>已排产生产订单:</strong></p><ul>${pos}</ul>
`);
}
function editOrder(id) {
const o = findById(apsData.salesOrders, id);
openModal('编辑销售订单', `
<div class="form-row"><div class="form-group"><label>优先级</label><input id="e_priority" type="number" min="1" max="10" value="${o.priority}"></div><div class="form-group"><label>交期</label><input id="e_delivery" type="date" value="${o.deliveryDate}"></div></div>
<div class="form-row"><div class="form-group"><label>特殊要求</label><textarea id="e_note" rows="2">${o.specialRequirements}</textarea></div></div>
`, `
<button class="btn" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="doEditOrder(${id})">保存</button>
`);
}
function doEditOrder(id) {
const o = findById(apsData.salesOrders, id);
o.priority = Number(document.getElementById('e_priority').value);
o.deliveryDate = document.getElementById('e_delivery').value;
o.specialRequirements = document.getElementById('e_note').value;
o.updatedAt = apsData.$utils.fmtDateTime(new Date());
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'ORDER', operationType: 'UPDATE', targetType: 'SALES_ORDER', targetId: id, action: '更新销售订单', description: `订单 ${o.orderNo} 已更新`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData);
closeModal(); showToast('更新成功', '订单已更新'); render();
}
function confirmOrder(id) {
const o = findById(apsData.salesOrders, id);
o.status = 'CONFIRMED'; o.updatedAt = apsData.$utils.fmtDateTime(new Date());
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'INFO', category: 'ORDER', operationType: 'CONFIRM', targetType: 'SALES_ORDER', targetId: id, action: '确认订单', description: `订单 ${o.orderNo} 已确认`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData);
showToast('确认成功', `订单 ${o.orderNo} 已确认`); render();
}
function cancelOrder(id) {
confirmAction('确定取消该订单吗?已排产需先撤销。', `cancelOrderExec(${id})`);
}
function cancelOrderExec(id) {
const o = findById(apsData.salesOrders, id);
const old = o.status;
o.status = 'CANCELLED'; o.updatedAt = apsData.$utils.fmtDateTime(new Date());
addChange(id, 'CANCEL', old, 'CANCELLED', '订单取消');
apsData.logs.unshift({ id: apsData.$utils.nextId('log'), type: 'WARNING', category: 'ORDER', operationType: 'CANCEL', targetType: 'SALES_ORDER', targetId: id, action: '取消订单', description: `订单 ${o.orderNo} 已取消`, operator: 'admin', createdAt: apsData.$utils.fmtDateTime(new Date()) });
apsData.$utils.save(apsData);
showToast('取消成功', `订单 ${o.orderNo} 已取消`); render();
}
function historyModal(id) {
const o = findById(apsData.salesOrders, id);
const rows = (o.changes || []).slice().reverse().map(c => `
<tr><td>${c.createdAt}</td><td>${c.changeType}</td><td>${c.oldValue}</td><td>${c.newValue}</td><td>${c.reason}</td><td>${c.operator}</td></tr>
`).join('') || '<tr><td colspan="6" class="text-center text-muted">无变更记录</td></tr>';
openModal('订单变更历史', `<table><thead><tr><th>时间</th><th>变更类型</th><th>原值</th><th>新值</th><th>原因</th><th>操作人</th></tr></thead><tbody>${rows}</tbody></table>`, '', true);
}
function priorityConfigModal() {
const w = apsData.scheduleParams.priorityWeights || {
customer_level_weight: 0.30, urgency_weight: 0.35, amount_weight: 0.15, manual_weight: 0.20
};
openModal('优先级权重配置', `
<div class="form-row"><div class="form-group"><label>客户等级权重</label><input id="w_cust" type="number" step="0.05" value="${w.customer_level_weight}"></div><div class="form-group"><label>交期紧迫度权重</label><input id="w_urg" type="number" step="0.05" value="${w.urgency_weight}"></div></div>
<div class="form-row"><div class="form-group"><label>订单金额权重</label><input id="w_amt" type="number" step="0.05" value="${w.amount_weight}"></div><div class="form-group"><label>手工优先级权重</label><input id="w_man" type="number" step="0.05" value="${w.manual_weight}"></div></div>
<p class="hint">权重之和建议为 1。评分越高表示优先级越高。</p>
`, `
<button class="btn" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="savePriorityWeights()">保存</button>
`);
}
function savePriorityWeights() {
apsData.scheduleParams.priorityWeights = {
customer_level_weight: Number(document.getElementById('w_cust').value),
urgency_weight: Number(document.getElementById('w_urg').value),
amount_weight: Number(document.getElementById('w_amt').value),
manual_weight: Number(document.getElementById('w_man').value)
};
apsData.$utils.save(apsData); closeModal(); showToast('权重已更新'); render();
}
function importModal() {
openModal('批量导入销售订单', `<p>请上传 Excel 或 CSV 文件,系统将进行数据校验并异步导入。</p><input type="file" class="w-full" style="padding:8px;border:1px solid #d1d5db;border-radius:4px">`, `
<button class="btn" onclick="closeModal()">取消</button>
<button class="btn btn-primary" onclick="simulateImport()">开始导入</button>
`);
}
function simulateImport() {
closeModal();
showToast('导入任务已提交', '后台正在处理,请稍后刷新查看', 'info');
}
render();
</script>
</body>
</html>