ERP/frontend/js/components/purchase-demand.js
2025-12-08 11:20:47 +08:00

420 lines
16 KiB
JavaScript
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.

// 采购需求清单管理
(() => {
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
};
})();