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

348 lines
24 KiB
HTML
Raw Permalink Normal View History

2026-07-21 11:05:57 +08:00
<!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>