ERP/frontend/js/components/purchase-demand.js

420 lines
16 KiB
JavaScript
Raw Normal View History

2025-12-08 03:20:47 +00:00
// 采购需求清单管理
(() => {
let demandList = [];
let productList = [];
let currentPage = 1;
const pageSize = 20;
const statusMap = {
'pending': { text: '待处理', color: 'var(--warning)' },
'ordered': { text: '已下单', color: 'var(--info)' },
'received': { text: '已收货', color: 'var(--primary)' },
'completed': { text: '已完成', color: 'var(--success)' },
'cancelled': { text: '已取消', color: 'var(--text-2)' }
};
Router.register('/plan-mgmt/purchase-demand', async () => {
const html = `
<div class="page-header">
<h1>采购需求清单</h1>
<div class="page-actions">
<button id="calc-demand-btn" class="btn btn-primary">计算采购需求</button>
<button id="calc-from-orders-btn" class="btn btn-secondary">从订单生成</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
</div>
<!-- 计算说明卡片 -->
<div class="card" style="margin-bottom: 16px; background: var(--info-bg); border: 1px solid var(--info);">
<div class="card-body" style="padding: 12px 16px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 20px;">💡</span>
<div>
<strong style="color: var(--text);">采购需求计算公式</strong>
<span style="color: var(--text);">客户订单数量 × BOM单机用量 - 期初库存 = 净需求 按最小包装向上取整 = 实际采购数量</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="filter-section" style="margin-bottom: 16px;">
<div style="display: flex; gap: 12px; flex-wrap: wrap; align-items: center;">
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码/名称/需求编号..." style="width: 280px;" />
<select id="filter-status" class="input" style="width: 120px;">
<option value="">全部状态</option>
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
<button class="btn btn-primary" onclick="PurchaseDemand.search()">搜索</button>
<button class="btn btn-secondary" onclick="PurchaseDemand.resetSearch()">重置</button>
</div>
</div>
<div class="table-container" style="overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="PurchaseDemand.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
<th>需求编号</th>
<th>物料编码</th>
<th>物料名称</th>
<th>订单数量</th>
<th>BOM用量</th>
<th>总需求</th>
<th>期初库存</th>
<th>净需求</th>
<th>最小包装</th>
<th style="background: var(--success-bg); color: var(--text);">实际采购</th>
<th>单位</th>
<th>供应商</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="demand-list">
<tr><td colspan="15" class="text-center">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="margin-top: 16px; display: flex; justify-content: center; gap: 8px;"></div>
</div>
</div>
<!-- 计算采购需求弹窗 -->
<div id="calc-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2>计算采购需求</h2>
<button class="modal-close" onclick="PurchaseDemand.closeCalcModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>选择产品 <span style="color: var(--danger);">*</span></label>
<select id="product-select" class="input">
<option value="">请选择产品</option>
</select>
</div>
<div class="field">
<label>订单数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="order-qty" class="input" min="1" value="1" placeholder="输入客户订单数量" />
</div>
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
<div style="font-size: 14px; color: var(--text-2);">
<p style="margin: 0 0 8px 0;">📋 计算将</p>
<ul style="margin: 0; padding-left: 20px;">
<li>根据产品的BOM展开所有物料</li>
<li>计算每种物料的总需求量</li>
<li>扣减期初库存得出净需求</li>
<li>按最小包装取整得出实际采购量</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeCalcModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.doCalculate()">开始计算</button>
</div>
</div>
</div>
<!-- 更新状态弹窗 -->
<div id="status-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h2>更新状态</h2>
<button class="modal-close" onclick="PurchaseDemand.closeStatusModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>状态</label>
<select id="update-status" class="input">
<option value="pending">待处理</option>
<option value="ordered">已下单</option>
<option value="received">已收货</option>
<option value="completed">已完成</option>
<option value="cancelled">已取消</option>
</select>
</div>
<div class="field">
<label>备注</label>
<textarea id="update-remark" class="input" rows="3" placeholder="可选填写备注信息"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeStatusModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.doUpdateStatus()">确定</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('calc-demand-btn')?.addEventListener('click', () => openCalcModal());
document.getElementById('calc-from-orders-btn')?.addEventListener('click', () => calcFromOrders());
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') PurchaseDemand.search();
});
loadList();
loadProducts();
}, 100);
return html;
});
async function loadList() {
try {
const res = await API.get('/api/purchase-demand');
demandList = res.list || [];
renderList();
} catch (e) {
console.error('加载采购需求失败:', e);
document.getElementById('demand-list').innerHTML = '<tr><td colspan="15" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
async function loadProducts() {
try {
const res = await API.get('/api/bom/products');
productList = res.list || [];
const select = document.getElementById('product-select');
if (select) {
select.innerHTML = '<option value="">请选择产品</option>' +
productList.map(p => `<option value="${escapeHtml(p.product_code)}">${escapeHtml(p.product_code)} - ${escapeHtml(p.product_name)}</option>`).join('');
}
} catch (e) {
console.error('加载产品列表失败:', e);
}
}
function renderList() {
const tbody = document.getElementById('demand-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
const filterStatus = document.getElementById('filter-status')?.value || '';
let filtered = demandList;
if (keyword) {
filtered = filtered.filter(item =>
(item.demand_no || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_name || '').toLowerCase().includes(keyword)
);
}
if (filterStatus) {
filtered = filtered.filter(item => item.status === filterStatus);
}
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="15" 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><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
<td><span style="font-family: monospace; font-size: 12px;">${escapeHtml(item.demand_no || '')}</span></td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td>${item.order_qty || 0}</td>
<td>${item.bom_unit_qty || 1}</td>
<td>${item.total_demand || 0}</td>
<td style="color: var(--info);">${item.initial_stock || 0}</td>
<td style="color: ${item.net_demand > 0 ? 'var(--warning)' : 'var(--success)'};">${item.net_demand || 0}</td>
<td>${item.min_package || 1}</td>
<td style="font-weight: 700; color: var(--text); background: var(--success-bg);">${item.actual_purchase_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${escapeHtml(item.supplier || '-')}</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>
<button class="btn btn-sm btn-secondary" onclick="PurchaseDemand.updateStatus(${item.id})">状态</button>
<button class="btn btn-sm btn-danger" onclick="PurchaseDemand.delete(${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="PurchaseDemand.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="PurchaseDemand.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openCalcModal() {
document.getElementById('product-select').value = '';
document.getElementById('order-qty').value = 1;
document.getElementById('calc-modal').style.display = 'flex';
}
function closeCalcModal() {
document.getElementById('calc-modal').style.display = 'none';
}
async function doCalculate() {
const productCode = document.getElementById('product-select').value;
const orderQty = parseInt(document.getElementById('order-qty').value) || 0;
if (!productCode) {
alert('请选择产品');
return;
}
if (orderQty <= 0) {
alert('订单数量必须大于0');
return;
}
try {
const res = await API.post('/api/purchase-demand/calculate', {
product_code: productCode,
order_qty: orderQty
});
closeCalcModal();
alert(res.message || '计算完成');
loadList();
// 显示计算结果摘要
if (res.list && res.list.length > 0) {
const totalPurchase = res.list.reduce((sum, item) => sum + (item.actual_purchase_qty || 0), 0);
console.log(`采购需求计算完成,需求编号: ${res.demand_no},共 ${res.list.length} 种物料,总采购数量: ${totalPurchase}`);
}
} catch (e) {
alert(e.message || '计算失败');
}
}
async function calcFromOrders() {
if (!confirm('将根据所有客户订单自动计算采购需求,确定继续吗?')) {
return;
}
try {
const res = await API.post('/api/purchase-demand/calculate-from-orders', {});
alert(res.message || '计算完成');
loadList();
} catch (e) {
alert(e.message || '计算失败');
}
}
let updatingId = null;
function updateStatus(id) {
updatingId = id;
const item = demandList.find(x => x.id === id);
document.getElementById('update-status').value = item?.status || 'pending';
document.getElementById('update-remark').value = item?.remark || '';
document.getElementById('status-modal').style.display = 'flex';
}
function closeStatusModal() {
document.getElementById('status-modal').style.display = 'none';
updatingId = null;
}
async function doUpdateStatus() {
if (!updatingId) return;
const status = document.getElementById('update-status').value;
const remark = document.getElementById('update-remark').value.trim();
try {
await API.put(`/api/purchase-demand/${updatingId}`, { status, remark });
closeStatusModal();
alert('更新成功');
loadList();
} catch (e) {
alert(e.message || '更新失败');
}
}
async function deleteDemand(id) {
if (!confirm('确定要删除这条采购需求吗?')) return;
try {
await API.delete(`/api/purchase-demand/${id}`);
alert('删除成功');
loadList();
} catch (e) {
alert(e.message || '删除失败');
}
}
async function batchDelete() {
const checked = document.querySelectorAll('.row-checkbox:checked');
if (checked.length === 0) {
alert('请先选择要删除的项');
return;
}
if (!confirm(`确定要删除选中的 ${checked.length} 条采购需求吗?`)) return;
const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
try {
await API.post('/api/purchase-demand/batch-delete', { ids });
alert('批量删除成功');
loadList();
} catch (e) {
alert(e.message || '批量删除失败');
}
}
function toggleSelectAll(checkbox) {
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
}
function search() {
currentPage = 1;
renderList();
}
function resetSearch() {
document.getElementById('search-keyword').value = '';
document.getElementById('filter-status').value = '';
currentPage = 1;
renderList();
}
function goPage(page) {
currentPage = page;
renderList();
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.PurchaseDemand = {
search,
resetSearch,
delete: deleteDemand,
updateStatus,
closeCalcModal,
closeStatusModal,
doCalculate,
doUpdateStatus,
toggleSelectAll,
goPage
};
})();