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

2009 lines
74 KiB
JavaScript
Executable File
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 = `
<style>
#purchase-demand-page .page-header {
padding: 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#purchase-demand-page .content-area {
flex: 1;
padding: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
#purchase-demand-page .table-wrapper {
flex: 1;
overflow: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
#purchase-demand-page table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#purchase-demand-page thead {
background: var(--border);
position: sticky;
top: 0;
z-index: 10;
}
#purchase-demand-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;
}
#purchase-demand-page td {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 14px;
}
#purchase-demand-page tbody tr:last-child td {
border-bottom: none;
}
#purchase-demand-page tbody tr:hover {
background: var(--hover);
}
#purchase-demand-page .text-center {
text-align: center;
color: var(--text-2);
}
#purchase-demand-page .btn-text {
background: none;
border: none;
color: var(--primary);
cursor: pointer;
padding: 4px 8px;
margin-right: 8px;
}
#purchase-demand-page .btn-text:hover {
text-decoration: underline;
}
#purchase-demand-page .btn-danger {
color: var(--danger);
}
#purchase-demand-page .btn {
color: var(--text);
}
#purchase-demand-page .btn.btn-danger {
color: white;
}
#purchase-demand-page .btn.btn-primary {
color: white;
}
#purchase-demand-page .btn.btn-secondary {
color: var(--text);
}
#purchase-demand-page .btn-sm {
color: var(--text);
}
#purchase-demand-page .btn-sm.btn-danger {
color: white;
}
#purchase-demand-page .btn-sm.btn-secondary {
color: var(--text);
}
#purchase-demand-page .product-tabs-wrapper {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 20px;
}
#purchase-demand-page .product-tab {
min-width: 100px;
height: 51px;
border-radius: 15px;
cursor: pointer;
transition: 0.3s ease;
background: linear-gradient(
to bottom right,
#2e8eff 0%,
rgba(46, 142, 255, 0) 30%
);
background-color: rgba(46, 142, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: 2px;
}
#purchase-demand-page .product-tab:hover,
#purchase-demand-page .product-tab:focus {
background-color: rgba(255, 152, 0, 0.7);
box-shadow: 0 0 10px rgba(255, 152, 0, 0.5);
outline: none;
}
#purchase-demand-page .product-tab.active {
background-color: rgba(255, 152, 0, 0.95);
box-shadow: 0 0 15px rgba(255, 152, 0, 0.8);
}
#purchase-demand-page .product-tab-inner {
width: 100%;
height: 47px;
border-radius: 13px;
background-color: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
color: #66b3ff;
font-weight: 600;
font-size: 14px;
text-shadow: 0 0 2px rgba(102, 179, 255, 0.5);
}
[data-theme="light"] #purchase-demand-page .product-tab-inner {
color: #0066cc;
text-shadow: none;
}
#purchase-demand-page .product-tab.active .product-tab-inner {
font-weight: 700;
}
</style>
<div id="purchase-demand-page">
<div class="page-header">
<h1 style="margin: 0; font-size: 24px;">采购需求清单</h1>
<div class="page-actions" style="margin-top: 16px;">
<button id="calc-demand-btn" class="btn btn-primary" style="margin-right: 10px;">计算采购需求</button>
<button id="import-excel-btn" class="btn btn-secondary" style="margin-right: 10px;">导入 Excel</button>
<button id="export-excel-btn" class="btn btn-secondary" style="margin-right: 10px;" onclick="PurchaseDemand.exportExcel()">导出 Excel</button>
<button id="sync-stock-btn" class="btn btn-secondary" style="margin-right: 10px;" onclick="PurchaseDemand.syncToInitialStock()">同步到期初库存</button>
<button id="delete-product-btn" class="btn btn-danger" style="margin-right: 10px;" onclick="PurchaseDemand.deleteProductData()">删除产品数据</button>
<button id="delete-factory-btn" class="btn btn-danger" style="margin-right: 10px; display: none;" onclick="PurchaseDemand.showDeleteFactoryModal()">删除工厂</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
</div>
<div class="content-area">
<!-- 工厂筛选按钮 -->
<div style="padding: 20px 20px 10px 20px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<span style="font-weight: 600; color: var(--text);">工厂筛选:</span>
<div id="factory-tabs" style="display: inline-flex; gap: 10px;">
<button class="product-tab active" data-factory="" onclick="PurchaseDemand.switchFactory('')">
<div class="product-tab-inner">全部工厂</div>
</button>
</div>
</div>
</div>
<!-- 产品选择标签页 -->
<div style="padding: 0 20px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<span style="font-weight: 600; color: var(--text);">产品筛选:</span>
<div class="product-tabs-wrapper" id="product-tabs-container" style="display: inline-flex; gap: 10px;">
<button class="product-tab active" data-product="" onclick="PurchaseDemand.switchProduct('')">
<div class="product-tab-inner">全部产品</div>
</button>
</div>
</div>
</div>
<div class="filter-section" style="padding: 20px; display: flex; gap: 15px; align-items: center; justify-content: space-between;">
<div style="display: flex; gap: 15px; align-items: center; flex: 1;">
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码、名称或供应商" style="flex: 1; max-width: 400px;" />
<select id="filter-status" class="input" style="width: 150px;">
<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-secondary" onclick="PurchaseDemand.search()">搜索</button>
<button class="btn btn-secondary" onclick="PurchaseDemand.resetSearch()">重置</button>
</div>
<button id="batch-edit-status-btn" class="btn btn-secondary" onclick="PurchaseDemand.openBatchEditStatusModal()">批量编辑状态</button>
</div>
<div class="table-wrapper">
<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>订单数量</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="padding: 20px; 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="factory-select" class="input">
<option value="">请选择工厂</option>
</select>
</div>
<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 class="field">
<label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: var(--surface); border-radius: 8px; border: 1px solid var(--border);">
<input type="checkbox" id="use-initial-stock" checked style="width: 18px; height: 18px; cursor: pointer; margin-right: 10px;" />
<div>
<div style="font-weight: 500; color: var(--text);">使用期初库存菜单中的库存数据</div>
<div style="margin-top: 4px; font-size: 12px; color: var(--text-3);">
勾选后将使用期初库存管理中的最新库存数据进行计算,否则使用导入时的库存数据
</div>
</div>
</label>
</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>
<!-- Excel导入弹窗 -->
<div id="import-excel-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>导入采购需求清单</h2>
<button class="modal-close" onclick="PurchaseDemand.closeImportModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>默认工厂(可选)</label>
<div style="display: flex; gap: 5px;">
<input type="text" id="import-factory" class="input" placeholder="不填写则使用Excel中的工厂值" style="flex: 1;" />
<select id="import-factory-select" onchange="document.getElementById('import-factory').value = this.value;" style="width: 120px;">
<option value="">快速选择</option>
<option value="友辉">友辉</option>
<option value="铨宝">铨宝</option>
</select>
</div>
<div style="margin-top: 4px; font-size: 12px; color: var(--text-3);">
如果Excel中工厂列为空将使用此默认值如果都未填写则标记为"未指定"
</div>
</div>
<div class="field">
<label>产品编码 <span style="color: var(--danger);">*</span></label>
<input type="text" id="import-product-code" class="input" placeholder="请输入产品编码AP05" />
</div>
<div class="field">
<label>选择Excel文件 <span style="color: var(--danger);">*</span></label>
<input type="file" id="excel-file" class="input" accept=".xlsx,.xls" />
<div style="margin-top: 8px; font-size: 12px; color: var(--text-3);">
支持 .xlsx 和 .xls 格式,文件大小不超过 10MB
</div>
<div style="margin-top: 8px; display: flex; gap: 8px;">
<button type="button" class="btn btn-secondary" onclick="downloadImportTemplate()">下载模板</button>
<button type="button" class="btn btn-secondary" onclick="testXLSX()">测试XLSX库</button>
</div>
</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); margin-bottom: 12px;">
<strong>Excel文件格式要求</strong>
</div>
<table style="width: 100%; font-size: 13px; border-collapse: collapse;">
<thead>
<tr style="background: var(--surface-2);">
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">列名</th>
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">说明</th>
<th style="padding: 8px; text-align: left; border: 1px solid var(--border);">必填</th>
</tr>
</thead>
<tbody>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">工厂</td>
<td style="padding: 6px; border: 1px solid var(--border;">工厂名称(可输入自定义名称,如:友辉、铨宝)</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✗</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">型号</td>
<td style="padding: 6px; border: 1px solid var(--border;">物料编码/型号</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">供应商</td>
<td style="padding: 6px; border: 1px solid var(--border;">供应商名称</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">期初库存</td>
<td style="padding: 6px; border: 1px solid var(--border;">当前库存数量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">MOQ</td>
<td style="padding: 6px; border: 1px solid var(--border;">最小订购量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
<tr>
<td style="padding: 6px; border: 1px solid var(--border); font-family: monospace;">单机数量</td>
<td style="padding: 6px; border: 1px solid var(--border;">BOM单机用量</td>
<td style="padding: 6px; border: 1px solid var(--border); text-align: center;">✓</td>
</tr>
</tbody>
</table>
</div>
<div id="import-preview" style="margin-top: 16px; display: none;">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 8px;">预览数据前5条</div>
<div class="table-container" style="max-height: 200px;">
<table class="data-table" id="preview-table">
<thead>
<tr>
<th>工厂</th>
<th>型号</th>
<th>供应商</th>
<th>期初库存</th>
<th>MOQ</th>
<th>单机数量</th>
</tr>
</thead>
<tbody id="preview-tbody">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeImportModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.importExcel()">导入</button>
</div>
</div>
</div>
<!-- 编辑采购需求弹窗 -->
<div id="edit-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h2>编辑采购需求</h2>
<button class="modal-close" onclick="PurchaseDemand.closeEditModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>物料编码</label>
<input type="text" id="edit-material-code" class="input" readonly />
</div>
<div class="field">
<label>物料名称</label>
<input type="text" id="edit-material-name" class="input" readonly />
</div>
<div class="field">
<label>订单数量</label>
<input type="number" id="edit-order-qty" class="input" min="0" />
</div>
<div class="field">
<label>单机用量</label>
<input type="number" id="edit-bom-unit-qty" class="input" min="0" step="0.01" />
</div>
<div class="field">
<label>期初库存</label>
<input type="number" id="edit-initial-stock" class="input" min="0" />
</div>
<div class="field">
<label>最小包装</label>
<input type="number" id="edit-min-package" class="input" min="1" />
</div>
<div class="field">
<label>实际采购数量</label>
<input type="number" id="edit-actual-purchase-qty" class="input" min="0" />
</div>
<div class="field">
<label>单位</label>
<input type="text" id="edit-unit" class="input" />
</div>
<div class="field">
<label>供应商</label>
<input type="text" id="edit-supplier" class="input" />
</div>
<div class="field">
<label>状态</label>
<select id="edit-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="edit-remark" class="input" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeEditModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.saveEdit()">保存</button>
</div>
</div>
</div>
<!-- 批量编辑状态弹窗 -->
<div id="batch-edit-status-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.closeBatchEditStatusModal()">&times;</button>
</div>
<div class="modal-body">
<div class="field">
<label>选择状态</label>
<select id="batch-status" class="input">
<option value="">请选择状态</option>
<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>
<div style="display: flex; gap: 20px; margin-top: 8px;">
<label for="batch-scope-selected" style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" id="batch-scope-selected" name="batch-scope" value="selected" checked style="margin-right: 8px;">
仅当前选中的记录
</label>
<label for="batch-scope-all" style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" id="batch-scope-all" name="batch-scope" value="all" style="margin-right: 8px;">
指定工厂和产品的所有记录
</label>
</div>
<div id="batch-factory-product-container" style="margin-top: 12px; display: none;">
<label style="font-size: 14px; margin-bottom: 4px; display: block;">选择工厂</label>
<select id="batch-factory-select" class="input" style="width: 100%; margin-bottom: 12px;">
<option value="">请选择工厂</option>
</select>
<label style="font-size: 14px; margin-bottom: 4px; display: block;">选择产品</label>
<select id="batch-product-select" class="input" style="width: 100%;">
<option value="">请先选择工厂</option>
</select>
</div>
</div>
<div class="field">
<label>备注(可选)</label>
<textarea id="batch-remark" class="input" rows="3" placeholder="可以为所有选中的项目添加统一的备注"></textarea>
</div>
<div style="margin-top: 16px; padding: 12px; background: var(--surface); border-radius: 8px;">
<div style="font-size: 14px; color: var(--text-2);">
<p style="margin: 0;">已选择 <span id="selected-count" style="font-weight: bold; color: var(--primary);">0</span> 条记录</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="PurchaseDemand.closeBatchEditStatusModal()">取消</button>
<button class="btn btn-primary" onclick="PurchaseDemand.saveBatchEditStatus()">保存</button>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('calc-demand-btn')?.addEventListener('click', () => openCalcModal());
document.getElementById('import-excel-btn')?.addEventListener('click', () => openImportModal());
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') PurchaseDemand.search();
});
document.getElementById('excel-file')?.addEventListener('change', handleFileSelect);
loadList();
loadProducts();
loadFactories();
}, 0);
return html;
});
async function loadList() {
try {
// 始终加载全部数据,在前端进行筛选
const res = await API.get('/api/purchase-demand');
demandList = res.list || [];
console.log('加载采购需求列表:', demandList.length, '条'); // 调试日志
console.log('前3条数据:', demandList.slice(0, 3)); // 调试日志
console.log('完整数据结构:', demandList[0]); // 查看第一条数据的完整结构
console.log('数据中的所有字段:', Object.keys(demandList[0] || {})); // 查看所有字段名
console.log('第一条数据的product_code:', demandList[0]?.product_code); // 直接查看第一条的product_code
console.log('第二条数据的product_code:', demandList[1]?.product_code); // 直接查看第二条的product_code
// 更新产品标签
updateProductTabs();
renderList();
} catch (e) {
console.error('加载采购需求失败:', e);
document.getElementById('demand-list').innerHTML = '<tr><td colspan="15" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
function updateProductTabs() {
// 提取所有产品编码,根据当前工厂筛选
const products = new Set();
demandList.forEach((item, index) => {
// 如果选择了工厂,只显示该工厂的产品
if (currentFactory && item.factory !== currentFactory) {
return; // 跳过不属于当前工厂的数据
}
// 优先使用 product_code 字段,如果没有则从 demand_no 中提取
let productCode = item.product_code;
console.log(`${index + 1}条数据的product_code:`, productCode); // 调试每条数据
if (productCode) {
products.add(productCode);
}
});
console.log('提取到的产品编码:', Array.from(products)); // 调试日志
console.log('检查数据是否有product_code字段:', demandList.some(item => 'product_code' in item)); // 检查字段是否存在
// 更新标签页
const tabsContainer = document.getElementById('product-tabs-container');
if (!tabsContainer) return;
const productArray = Array.from(products).sort();
let html = `<button class="product-tab ${!currentProduct ? 'active' : ''}" data-product="" onclick="PurchaseDemand.switchProduct('')">
<div class="product-tab-inner">全部</div>
</button>`;
productArray.forEach(product => {
const isActive = product === currentProduct ? 'active' : '';
html += `<button class="product-tab ${isActive}" data-product="${product}" onclick="PurchaseDemand.switchProduct('${product}')">
<div class="product-tab-inner">${product}</div>
</button>`;
});
tabsContainer.innerHTML = html;
}
function switchProduct(productCode) {
currentProduct = productCode; // 保存当前选中的产品
window.PurchaseDemand.currentProduct = productCode; // 也在全局对象中保存
// 只更新产品标签的样式(不影响工厂按钮)
document.querySelectorAll('#product-tabs-container .product-tab').forEach(tab => {
if (tab.dataset.product === productCode) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
});
// 如果当前在"全部工厂"视图,且选择了具体产品,自动切换到该产品所属的工厂
if (!currentFactory && productCode) {
// 找到该产品所属的工厂
const productData = demandList.find(item => item.product_code === productCode);
if (productData && productData.factory) {
// 保存当前选中的产品
const selectedProduct = productCode;
// 自动切换到该工厂这会重置currentProduct
switchFactory(productData.factory);
// 恢复产品选中状态
currentProduct = selectedProduct;
window.PurchaseDemand.currentProduct = selectedProduct;
// 更新产品标签的选中状态
updateProductTabs();
// 重新渲染列表
renderList();
return;
}
}
renderList();
}
function loadProducts(factory = '') {
// 直接使用已有的demandList数据避免重新请求API导致闪烁
let demandData = demandList;
// 如果指定了工厂,只显示该工厂的产品
if (factory) {
demandData = demandList.filter(item => item.factory === factory);
}
// 提取唯一的产品编码
const products = {};
demandData.forEach(item => {
if (item.product_code) {
products[item.product_code] = item.product_code;
}
});
productList = Object.keys(products).map(code => ({
product_code: code,
product_name: code // 使用产品编码作为名称
}));
console.log(`加载产品列表 (工厂: ${factory || '全部'}):`, productList); // 调试日志
const select = document.getElementById('product-select');
if (select) {
if (productList.length === 0) {
select.innerHTML = '<option value="">该工厂暂无产品</option>';
} else {
select.innerHTML = '<option value="">请选择产品</option>' +
productList.map(p => `<option value="${escapeHtml(p.product_code)}">${escapeHtml(p.product_code)}</option>`).join('');
}
}
}
async function loadFactories() {
try {
const res = await API.get('/api/factories');
const factories = res.factories || [];
renderFactoryTabs(factories);
} catch (e) {
console.error('加载工厂列表失败:', e);
}
}
async function loadFactoriesForSelect(selectId) {
try {
const res = await API.get('/api/factories');
const factories = res.factories || [];
const select = document.getElementById(selectId);
if (select) {
const currentValue = select.value;
const firstOption = select.options[0] ? select.options[0].outerHTML : '<option value="">请选择工厂</option>';
select.innerHTML = firstOption;
factories.forEach(factory => {
const option = document.createElement('option');
option.value = factory;
option.textContent = factory;
select.appendChild(option);
});
if (currentValue) {
select.value = currentValue;
}
}
} catch (e) {
console.error('加载工厂列表失败:', e);
}
}
function renderFactoryTabs(factories) {
const container = document.getElementById('factory-tabs');
if (!container) return;
// 始终保留"全部工厂"按钮
let html = `<button class="product-tab ${!currentFactory ? 'active' : ''}" data-factory="" onclick="PurchaseDemand.switchFactory('')">
<div class="product-tab-inner">全部工厂</div>
</button>`;
// 添加工厂按钮
factories.forEach(factory => {
const isActive = factory === currentFactory ? 'active' : '';
html += `
<button class="product-tab ${isActive}" data-factory="${factory}" onclick="PurchaseDemand.switchFactory('${factory}')">
<div class="product-tab-inner">${factory}</div>
</button>
`;
});
container.innerHTML = html;
}
async function showDeleteFactoryModal() {
if (!currentFactory) {
API.toast('请先选择要删除的工厂', 'error');
return;
}
if (!confirm(`确定要删除工厂"${currentFactory}"吗?\n\n这将删除该工厂的所有采购需求和期初库存数据,且不可恢复!`)) {
return;
}
try {
// 删除采购需求
await API.delete(`/api/purchase-demand/by-factory?factory=${encodeURIComponent(currentFactory)}`);
// 删除期初库存
await API.delete(`/api/initial-stock/by-factory?factory=${encodeURIComponent(currentFactory)}`);
API.toast(`成功删除工厂"${currentFactory}"及其所有数据`, 'success');
// 重新加载数据
loadList();
loadFactories();
loadProducts();
// 切换到全部工厂
switchFactory('');
} catch (e) {
console.error('删除工厂失败:', e);
API.toast('删除工厂失败:' + (e.message || '未知错误'), 'error');
}
}
let currentProduct = ''; // 当前选中的产品
let currentFactory = ''; // 当前选中的工厂
function switchFactory(factory) {
currentFactory = factory;
currentProduct = ''; // 切换工厂时重置产品筛选
// 只更新工厂按钮状态(不影响产品标签)
document.querySelectorAll('#factory-tabs .product-tab').forEach(btn => {
if (btn.getAttribute('data-factory') === factory) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
// 显示或隐藏删除工厂按钮(只在选中具体工厂时显示)
const deleteBtn = document.getElementById('delete-factory-btn');
if (deleteBtn) {
deleteBtn.style.display = factory ? 'inline-block' : 'none';
}
// 重新加载产品列表(根据工厂筛选)
loadProducts(factory);
// 更新产品标签
updateProductTabs();
renderList();
}
function toggleSelectAll(source) {
const checkboxes = document.querySelectorAll('.row-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = source.checked;
});
}
function renderList() {
const tbody = document.getElementById('demand-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
const filterStatus = document.getElementById('filter-status')?.value || '';
// 先根据工厂筛选数据
let filteredList = demandList;
if (currentFactory) {
filteredList = demandList.filter(item => item.factory === currentFactory);
}
// 按产品分组
let grouped = {};
filteredList.forEach(item => {
const productCode = item.product_code || '';
if (!grouped[productCode]) {
grouped[productCode] = [];
}
grouped[productCode].push(item);
});
// 如果选择了特定产品,只显示该产品的数据
if (currentProduct) {
grouped = currentProduct in grouped ? { [currentProduct]: grouped[currentProduct] } : {};
}
// 合并所有数据用于搜索和过滤
let allData = [];
Object.values(grouped).forEach(items => {
allData = allData.concat(items);
});
// 应用搜索过滤
if (keyword) {
allData = allData.filter(item =>
(item.demand_no || '').toLowerCase().includes(keyword) ||
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_name || '').toLowerCase().includes(keyword)
);
}
if (filterStatus) {
allData = allData.filter(item => item.status === filterStatus);
}
// 分页
const totalPages = Math.ceil(allData.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = allData.slice(start, start + pageSize);
if (pageData.length === 0) {
tbody.innerHTML = '<tr><td colspan="15" class="text-center">暂无数据</td></tr>';
} else {
// 按产品分组渲染
let html = '';
let currentGroup = null;
// 计算每个产品的总采购数量(跨所有页面)
const productTotals = {};
allData.forEach(item => {
if (item.product_code) {
productTotals[item.product_code] = (productTotals[item.product_code] || 0) + (item.actual_purchase_qty || 0);
}
});
pageData.forEach(item => {
if (item.product_code !== currentGroup) {
currentGroup = item.product_code;
// 只在有产品编码时才添加分组标题
if (currentGroup) {
const productTotal = productTotals[currentGroup] || 0;
html += `<tr style="background: var(--surface-2);">
<td colspan="10" style="font-weight: bold; padding: 12px 16px; color: var(--text);">
产品: ${currentGroup}
</td>
<td colspan="5" style="font-weight: bold; padding: 12px 16px; color: var(--primary); text-align: right;">
总采购数量: ${productTotal}
</td>
</tr>`;
}
}
const status = statusMap[item.status] || { text: item.status, color: 'var(--text-2)' };
html += `
<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.factory || '-')}</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.edit(${item.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="PurchaseDemand.delete(${item.id})">删除</button>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
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;
}
// 保存工厂选择的事件处理函数
let calcFactoryChangeHandler = null;
async function openCalcModal() {
document.getElementById('factory-select').value = '';
document.getElementById('product-select').value = '';
document.getElementById('order-qty').value = 1;
document.getElementById('calc-modal').style.display = 'flex';
// 动态加载工厂列表
await loadFactoriesForSelect('factory-select');
// 添加工厂选择监听
const factorySelect = document.getElementById('factory-select');
if (factorySelect) {
// 移除旧的监听器(如果存在)
if (calcFactoryChangeHandler) {
factorySelect.removeEventListener('change', calcFactoryChangeHandler);
}
// 创建新的事件处理函数
calcFactoryChangeHandler = function() {
const selectedFactory = this.value;
// 清空产品选择
document.getElementById('product-select').value = '';
// 根据选择的工厂加载产品
loadProducts(selectedFactory);
};
// 添加新的监听器
factorySelect.addEventListener('change', calcFactoryChangeHandler);
}
// 初始加载时不显示产品,等待选择工厂
const productSelect = document.getElementById('product-select');
if (productSelect) {
productSelect.innerHTML = '<option value="">请先选择工厂</option>';
}
}
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;
const useInitialStock = document.getElementById('use-initial-stock').checked;
if (!productCode) {
alert('请选择产品');
return;
}
if (orderQty <= 0) {
alert('订单数量必须大于0');
return;
}
try {
// 基于现有采购需求重新计算
const res = await API.post('/api/purchase-demand/recalculate', {
product_code: productCode,
order_qty: orderQty,
use_initial_stock: useInitialStock
});
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() {
const factory = document.getElementById('factory-select').value;
if (!factory) {
alert('请选择工厂');
return;
}
if (!confirm('将根据所有客户订单自动计算采购需求,确定继续吗?')) {
return;
}
try {
const res = await API.post('/api/purchase-demand/calculate-from-orders', { factory });
alert(res.message || '计算完成');
closeCalcModal();
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;');
}
// Excel导入相关函数
let excelData = [];
function openImportModal() {
document.getElementById('excel-file').value = '';
document.getElementById('import-preview').style.display = 'none';
document.getElementById('import-excel-modal').style.display = 'flex';
}
function closeImportModal() {
document.getElementById('import-excel-modal').style.display = 'none';
excelData = [];
}
// 测试XLSX库
window.testXLSX = function() {
console.log('测试XLSX库...');
console.log('typeof XLSX:', typeof XLSX);
if (typeof XLSX !== 'undefined') {
console.log('XLSX版本:', XLSX.version);
API.toast('XLSX库已加载版本: ' + XLSX.version);
} else {
console.error('XLSX库未加载');
// 尝试动态加载
loadXLSXLibrary();
}
};
// 动态加载XLSX库
function loadXLSXLibrary() {
if (typeof XLSX !== 'undefined') {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = './assets/xlsx.min.js';
script.onload = () => {
console.log('XLSX库动态加载成功版本:', XLSX.version);
API.toast('XLSX库加载成功');
resolve();
};
script.onerror = () => {
console.error('XLSX库加载失败');
API.toast('XLSX库加载失败请刷新页面重试', 'error');
reject(new Error('Failed to load XLSX library'));
};
document.head.appendChild(script);
});
}
// 确保XLSX库已加载
async function ensureXLSXLoaded() {
if (typeof XLSX === 'undefined') {
try {
await loadXLSXLibrary();
} catch (err) {
throw err;
}
}
}
async function handleFileSelect(e) {
const file = e.target.files[0];
if (!file) return;
try {
// 确保XLSX库已加载
await ensureXLSXLoaded();
// 检查文件大小
if (file.size > 10 * 1024 * 1024) {
API.toast('文件大小不能超过10MB', 'error');
e.target.value = '';
return;
}
// 检查文件类型
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.xlsx') && !fileName.endsWith('.xls')) {
API.toast('请选择Excel文件.xlsx或.xls格式', 'error');
e.target.value = '';
return;
}
// 读取Excel文件
const reader = new FileReader();
reader.onload = async function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
console.log('Excel解析结果:', jsonData); // 调试日志
// 验证数据格式
if (jsonData.length < 2) {
API.toast('Excel文件没有数据', 'error');
return;
}
// 获取表头
const headers = jsonData[0];
console.log('表头:', headers); // 调试日志
const requiredHeaders = ['型号', '供应商', '期初库存', 'MOQ', '单机数量'];
const optionalHeaders = ['工厂'];
const missingHeaders = requiredHeaders.filter(h => !headers.includes(h));
if (missingHeaders.length > 0) {
API.toast(`缺少必要的列:${missingHeaders.join(', ')}。当前列:${headers.join(', ')}`, 'error');
return;
}
// 检查工厂列
const hasFactoryColumn = headers.includes('工厂');
if (!hasFactoryColumn) {
const defaultFactory = document.getElementById('import-factory').value.trim();
if (!defaultFactory) {
API.toast('Excel中没有工厂列且未设置默认工厂请至少提供一种方式', 'error');
return;
}
}
// 解析数据
excelData = [];
let emptyRows = 0;
let validRows = 0;
for (let i = 1; i < jsonData.length; i++) {
const row = jsonData[i];
if (!row) {
emptyRows++;
continue;
}
// 获取各列的值
const factory = hasFactoryColumn ? String(row[headers.indexOf('工厂')] || '').trim() : '';
const model = String(row[headers.indexOf('型号')] || '').trim();
const supplier = String(row[headers.indexOf('供应商')] || '').trim();
const initial_stock = row[headers.indexOf('期初库存')] || 0;
const moq = row[headers.indexOf('MOQ')] || 1;
const per_unit_qty = row[headers.indexOf('单机数量')] || 1;
// 跳过完全空白的行
if (!model && !supplier) {
emptyRows++;
continue;
}
const item = {
factory: factory,
model: model,
supplier: supplier,
initial_stock: parseInt(initial_stock) || 0,
moq: parseInt(moq) || 1,
per_unit_qty: parseInt(per_unit_qty) || 1
};
console.log(`${i}行数据:`, item); // 调试日志
excelData.push(item);
validRows++;
}
console.log(`总共处理 ${jsonData.length - 1} 行,有效数据 ${validRows} 条,空行 ${emptyRows}`);
if (excelData.length === 0) {
API.toast('Excel文件没有有效数据', 'error');
return;
}
// 显示预览
showPreview(excelData.slice(0, 5));
API.toast(`成功读取 ${excelData.length} 条数据`, 'success');
} catch (err) {
console.error('解析Excel失败:', err);
API.toast(`解析Excel文件失败${err.message || '请检查文件格式'}`, 'error');
}
};
reader.readAsArrayBuffer(file);
} catch (err) {
console.error('读取文件失败:', err);
API.toast('读取文件失败', 'error');
}
}
function showPreview(data) {
const tbody = document.getElementById('preview-tbody');
tbody.innerHTML = data.map(item => `
<tr>
<td>${escapeHtml(item.factory)}</td>
<td>${escapeHtml(item.model)}</td>
<td>${escapeHtml(item.supplier)}</td>
<td>${item.initial_stock}</td>
<td>${item.moq}</td>
<td>${item.per_unit_qty}</td>
</tr>
`).join('');
document.getElementById('import-preview').style.display = 'block';
}
async function edit(id) {
try {
const res = await API.get(`/api/purchase-demand/${id}`);
const item = res;
if (!item) {
alert('未找到该采购需求');
return;
}
// 填充表单
document.getElementById('edit-material-code').value = item.material_code || '';
document.getElementById('edit-material-name').value = item.material_name || '';
document.getElementById('edit-order-qty').value = item.order_qty || 0;
document.getElementById('edit-bom-unit-qty').value = item.bom_unit_qty || 0;
document.getElementById('edit-initial-stock').value = item.initial_stock || 0;
document.getElementById('edit-min-package').value = item.min_package || 1;
document.getElementById('edit-actual-purchase-qty').value = item.actual_purchase_qty || 0;
document.getElementById('edit-unit').value = item.unit || 'pcs';
document.getElementById('edit-supplier').value = item.supplier || '';
document.getElementById('edit-status').value = item.status || 'pending';
document.getElementById('edit-remark').value = item.remark || '';
// 保存当前编辑的ID
window.PurchaseDemand.editingId = id;
// 显示弹窗
document.getElementById('edit-modal').style.display = 'flex';
} catch (e) {
console.error('获取数据失败:', e);
alert('获取数据失败');
}
}
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
window.PurchaseDemand.editingId = null;
}
async function saveEdit() {
const id = window.PurchaseDemand.editingId;
if (!id) {
alert('无效的编辑ID');
return;
}
// 获取表单数据
const data = {
order_qty: parseInt(document.getElementById('edit-order-qty').value) || 0,
bom_unit_qty: parseFloat(document.getElementById('edit-bom-unit-qty').value) || 0,
initial_stock: parseInt(document.getElementById('edit-initial-stock').value) || 0,
min_package: parseInt(document.getElementById('edit-min-package').value) || 1,
actual_purchase_qty: parseInt(document.getElementById('edit-actual-purchase-qty').value) || 0,
unit: document.getElementById('edit-unit').value.trim() || 'pcs',
supplier: document.getElementById('edit-supplier').value.trim(),
status: document.getElementById('edit-status').value,
remark: document.getElementById('edit-remark').value.trim()
};
try {
const res = await API.put(`/api/purchase-demand/${id}`, data);
if (res.ok) {
alert('保存成功');
closeEditModal();
loadList();
} else {
alert(res.error || '保存失败');
}
} catch (e) {
console.error('保存失败:', e);
alert('保存失败');
}
}
// 保存批量编辑工厂选择的事件处理函数
let batchFactoryChangeHandler = null;
async function openBatchEditStatusModal() {
// 获取选中的复选框
const checkboxes = document.querySelectorAll('.row-checkbox:checked');
const selectedCount = checkboxes.length;
// 更新选中数量显示
document.getElementById('selected-count').textContent = selectedCount;
// 重置表单
document.getElementById('batch-status').value = '';
document.getElementById('batch-remark').value = '';
document.getElementById('batch-factory-select').value = '';
document.getElementById('batch-product-select').innerHTML = '<option value="">请先选择工厂</option>';
document.querySelector('input[name="batch-scope"][value="selected"]').checked = true;
document.getElementById('batch-factory-product-container').style.display = 'none';
// 动态加载工厂列表
await loadFactoriesForSelect('batch-factory-select');
// 添加范围选择事件监听
document.getElementById('batch-scope-all').onchange = function() {
document.getElementById('batch-factory-product-container').style.display =
this.checked ? 'block' : 'none';
};
document.getElementById('batch-scope-selected').onchange = function() {
document.getElementById('batch-factory-product-container').style.display =
this.checked ? 'none' : 'block';
};
// 添加工厂选择监听
const batchFactorySelect = document.getElementById('batch-factory-select');
if (batchFactorySelect) {
// 移除旧的监听器(如果存在)
if (batchFactoryChangeHandler) {
batchFactorySelect.removeEventListener('change', batchFactoryChangeHandler);
}
// 创建新的事件处理函数
batchFactoryChangeHandler = function() {
const selectedFactory = this.value;
// 清空产品选择
document.getElementById('batch-product-select').value = '';
// 根据选择的工厂加载产品
loadBatchProductOptions(selectedFactory);
};
// 添加新的监听器
batchFactorySelect.addEventListener('change', batchFactoryChangeHandler);
}
// 显示弹窗
document.getElementById('batch-edit-status-modal').style.display = 'flex';
}
function loadBatchProductOptions(factory = '') {
// 获取指定工厂的产品
const products = new Set();
demandList.forEach(item => {
// 如果指定了工厂,只显示该工厂的产品
if (factory && item.factory !== factory) {
return;
}
if (item.product_code) {
products.add(item.product_code);
}
});
const productSelect = document.getElementById('batch-product-select');
if (!factory) {
productSelect.innerHTML = '<option value="">请先选择工厂</option>';
} else if (products.size === 0) {
productSelect.innerHTML = '<option value="">该工厂暂无产品</option>';
} else {
productSelect.innerHTML = '<option value="">请选择产品</option>';
// 添加产品选项
Array.from(products).sort().forEach(product => {
const option = document.createElement('option');
option.value = product;
option.textContent = product;
productSelect.appendChild(option);
});
}
}
function closeBatchEditStatusModal() {
document.getElementById('batch-edit-status-modal').style.display = 'none';
}
async function saveBatchEditStatus() {
const status = document.getElementById('batch-status').value;
const remark = document.getElementById('batch-remark').value.trim();
const scope = document.querySelector('input[name="batch-scope"]:checked').value;
if (!status) {
alert('请选择状态');
return;
}
let requestData = {
status: status,
remark: remark
};
let confirmMessage = '';
if (scope === 'all') {
// 更新指定工厂和产品的所有记录
const selectedFactory = document.getElementById('batch-factory-select').value;
const selectedProduct = document.getElementById('batch-product-select').value;
if (!selectedFactory) {
alert('请选择工厂');
return;
}
if (!selectedProduct) {
alert('请选择要更新的产品');
return;
}
// 获取该工厂和产品的所有记录ID
const ids = demandList
.filter(item => item.factory === selectedFactory && item.product_code === selectedProduct)
.map(item => item.id);
if (ids.length === 0) {
alert(`未找到工厂"${selectedFactory}"下产品"${selectedProduct}"的记录`);
return;
}
requestData.ids = ids;
confirmMessage = `确定要将工厂"${selectedFactory}"下产品"${selectedProduct}"的所有 ${ids.length} 条记录状态更新为"${getStatusText(status)}"吗?`;
} else {
// 仅更新选中的记录
const checkboxes = document.querySelectorAll('.row-checkbox:checked');
const ids = Array.from(checkboxes).map(cb => parseInt(cb.dataset.id));
if (ids.length === 0) {
alert('请选择要更新的记录');
return;
}
requestData.ids = ids;
confirmMessage = `确定要将选中的 ${ids.length} 条记录状态更新为"${getStatusText(status)}"吗?`;
}
if (!confirm(confirmMessage)) {
return;
}
try {
const res = await API.post('/api/purchase-demand/batch-update-status', requestData);
if (res.ok) {
alert(`成功更新 ${res.count} 条记录`);
closeBatchEditStatusModal();
loadList();
// 清除选择
document.getElementById('select-all').checked = false;
} else {
alert(res.error || '更新失败');
}
} catch (e) {
console.error('批量更新失败:', e);
alert('更新失败');
}
}
function getStatusText(status) {
const statusMap = {
'pending': '待处理',
'ordered': '已下单',
'received': '已收货',
'completed': '已完成',
'cancelled': '已取消'
};
return statusMap[status] || status;
}
async function syncToInitialStock() {
try {
// 获取当前显示的数据
const res = await API.get('/api/purchase-demand');
let dataToSync = res.list || [];
// 应用当前的筛选条件
const keyword = document.getElementById('search-keyword').value.trim();
const statusFilter = document.getElementById('filter-status').value;
const currentProduct = window.PurchaseDemand.currentProduct || '';
if (currentProduct) {
dataToSync = dataToSync.filter(item => item.product_code === currentProduct);
}
if (statusFilter) {
dataToSync = dataToSync.filter(item => item.status === statusFilter);
}
if (keyword) {
const lowerKeyword = keyword.toLowerCase();
dataToSync = dataToSync.filter(item =>
(item.material_code && item.material_code.toLowerCase().includes(lowerKeyword)) ||
(item.material_name && item.material_name.toLowerCase().includes(lowerKeyword)) ||
(item.supplier && item.supplier.toLowerCase().includes(lowerKeyword))
);
}
if (dataToSync.length === 0) {
alert('没有数据可同步');
return;
}
// 提取唯一物料
const uniqueMaterials = {};
dataToSync.forEach(item => {
const key = item.material_code;
if (!uniqueMaterials[key] || item.initial_stock > uniqueMaterials[key].initial_stock) {
uniqueMaterials[key] = {
material_code: item.material_code,
material_name: item.material_name,
stock_qty: item.initial_stock,
unit: item.unit || 'pcs',
min_package: item.min_package || 1,
supplier: item.supplier || ''
};
}
});
const materialsToSync = Object.values(uniqueMaterials);
if (!confirm(`确定要将 ${materialsToSync.length} 个物料的期初库存同步到期初库存管理吗?\n注意:如果物料已存在,将更新其库存数量。`)) {
return;
}
const res2 = await API.post('/api/purchase-demand/sync-to-initial-stock', {
materials: materialsToSync
});
if (res2.ok) {
alert(`成功同步 ${res2.success_count} 个物料的期初库存`);
} else {
alert(res2.error || '同步失败');
}
} catch (e) {
console.error('同步失败:', e);
alert('同步失败');
}
}
async function deleteProductData() {
const currentProduct = window.PurchaseDemand.currentProduct || '';
if (!currentProduct) {
alert('请先选择一个产品');
return;
}
if (!confirm(`确定要删除产品 ${currentProduct} 的所有采购需求数据吗?\n此操作不可恢复!`)) {
return;
}
try {
const res = await API.post('/api/purchase-demand/delete-product', {
product_code: currentProduct
});
if (res.ok) {
alert(`成功删除产品 ${currentProduct}${res.count} 条数据`);
window.PurchaseDemand.currentProduct = '';
loadList();
} else {
alert(res.error || '删除失败');
}
} catch (e) {
alert('删除失败:' + e.message);
}
}
async function exportExcel() {
try {
// 获取当前显示的数据(包括筛选条件)
const res = await API.get('/api/purchase-demand');
let dataToExport = res.list || [];
// 应用当前的筛选条件
const keyword = document.getElementById('search-keyword').value.trim();
const statusFilter = document.getElementById('filter-status').value;
const currentProduct = window.PurchaseDemand.currentProduct || '';
if (currentProduct) {
dataToExport = dataToExport.filter(item => item.product_code === currentProduct);
}
if (statusFilter) {
dataToExport = dataToExport.filter(item => item.status === statusFilter);
}
if (keyword) {
const lowerKeyword = keyword.toLowerCase();
dataToExport = dataToExport.filter(item =>
(item.material_code && item.material_code.toLowerCase().includes(lowerKeyword)) ||
(item.material_name && item.material_name.toLowerCase().includes(lowerKeyword)) ||
(item.supplier && item.supplier.toLowerCase().includes(lowerKeyword))
);
}
if (dataToExport.length === 0) {
alert('没有数据可导出');
return;
}
// 确保XLSX库已加载
await ensureXLSXLoaded();
// 准备导出数据
const exportData = dataToExport.map(item => ({
'需求编号': item.demand_no || '',
'产品编码': item.product_code || '',
'物料编码': item.material_code || '',
'物料名称': item.material_name || '',
'订单数量': item.order_qty || 0,
'单机用量': item.bom_unit_qty || 0,
'总需求': item.total_demand || 0,
'期初库存': item.initial_stock || 0,
'净需求': item.net_demand || 0,
'最小包装': item.min_package || 1,
'实际采购数量': item.actual_purchase_qty || 0,
'单位': item.unit || 'pcs',
'供应商': item.supplier || '',
'状态': getStatusText(item.status),
'备注': item.remark || '',
'创建时间': item.created_at || '',
'创建人': item.created_by || ''
}));
// 创建工作簿
const ws = XLSX.utils.json_to_sheet(exportData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "采购需求清单");
// 设置列宽
const colWidths = [
{ wch: 20 }, // 需求编号
{ wch: 12 }, // 产品编码
{ wch: 20 }, // 物料编码
{ wch: 30 }, // 物料名称
{ wch: 10 }, // 订单数量
{ wch: 10 }, // 单机用量
{ wch: 10 }, // 总需求
{ wch: 10 }, // 期初库存
{ wch: 10 }, // 净需求
{ wch: 10 }, // 最小包装
{ wch: 15 }, // 实际采购数量
{ wch: 8 }, // 单位
{ wch: 20 }, // 供应商
{ wch: 10 }, // 状态
{ wch: 20 }, // 备注
{ wch: 20 }, // 创建时间
{ wch: 10 } // 创建人
];
ws['!cols'] = colWidths;
// 生成文件名
const now = new Date();
const timestamp = now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') + '_' +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0');
const filename = `采购需求清单_${currentProduct || '全部'}_${timestamp}.xlsx`;
// 导出文件
XLSX.writeFile(wb, filename);
console.log(`成功导出 ${dataToExport.length} 条数据到 ${filename}`);
} catch (err) {
console.error('导出失败:', err);
alert('导出失败,请稍后重试');
}
}
function getStatusText(status) {
const statusMap = {
'pending': '待处理',
'ordered': '已下单',
'received': '已收货',
'completed': '已完成',
'cancelled': '已取消'
};
return statusMap[status] || status;
}
async function downloadImportTemplate() {
try {
// 确保XLSX库已加载
await ensureXLSXLoaded();
// 创建模板数据
const templateData = [
{
'工厂': '友辉',
'型号': '示例物料编码',
'供应商': '示例供应商',
'期初库存': 100,
'MOQ': 1000,
'单机数量': 1
},
{
'工厂': '铨宝',
'型号': '示例物料编码2',
'供应商': '示例供应商2',
'期初库存': 200,
'MOQ': 500,
'单机数量': 2
},
{
'工厂': '新工厂',
'型号': '示例物料编码3',
'供应商': '示例供应商3',
'期初库存': 150,
'MOQ': 300,
'单机数量': 1
}
];
// 创建工作簿
const ws = XLSX.utils.json_to_sheet(templateData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "采购需求导入模板");
// 设置列宽
const colWidths = [
{ wch: 10 }, // 工厂
{ wch: 20 }, // 型号
{ wch: 20 }, // 供应商
{ wch: 12 }, // 期初库存
{ wch: 10 }, // MOQ
{ wch: 12 } // 单机数量
];
ws['!cols'] = colWidths;
// 导出文件
XLSX.writeFile(wb, '采购需求导入模板.xlsx');
console.log('成功下载导入模板');
} catch (err) {
console.error('下载模板失败:', err);
alert('下载模板失败,请稍后重试');
}
}
async function importExcel() {
if (excelData.length === 0) {
API.toast('没有可导入的数据', 'error');
return;
}
// 获取工厂和产品编码
const factory = document.getElementById('import-factory').value.trim();
const productCode = document.getElementById('import-product-code').value.trim();
if (!productCode) {
API.toast('请输入产品编码', 'error');
return;
}
try {
// 确保XLSX库已加载虽然这里不需要但为了保持一致性
await ensureXLSXLoaded();
console.log('准备导入的数据:', excelData); // 调试日志
console.log('工厂:', factory); // 调试日志
console.log('产品编码:', productCode); // 调试日志
const res = await API.post('/api/purchase-demand/import', {
data: excelData,
factory: factory,
product_code: productCode
});
console.log('导入响应:', res); // 调试日志
if (res.ok) {
API.toast(`成功导入 ${res.count || excelData.length} 条数据`, 'success');
if (res.errors && res.errors.length > 0) {
console.log('导入错误:', res.errors);
}
closeImportModal();
loadList(); // 重新加载列表
loadFactories(); // 重新加载工厂列表
loadProducts(); // 重新加载产品列表
} else {
API.toast(res.error || '导入失败', 'error');
}
} catch (err) {
console.error('导入失败:', err);
API.toast('导入失败,请稍后重试', 'error');
}
}
window.PurchaseDemand = {
search,
resetSearch,
delete: deleteDemand,
updateStatus,
closeCalcModal,
doCalculate,
openImportModal,
closeImportModal,
handleFileSelect,
importExcel,
downloadImportTemplate,
exportExcel,
deleteProductData,
edit,
closeEditModal,
saveEdit,
openBatchEditStatusModal,
closeBatchEditStatusModal,
saveBatchEditStatus,
syncToInitialStock,
toggleSelectAll,
switchProduct,
switchFactory,
showDeleteFactoryModal,
goPage
};
})();