1632 lines
60 KiB
JavaScript
1632 lines
60 KiB
JavaScript
// 采购需求清单管理
|
||
(() => {
|
||
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(46, 142, 255, 0.7);
|
||
box-shadow: 0 0 10px rgba(46, 142, 255, 0.5);
|
||
outline: none;
|
||
}
|
||
|
||
#purchase-demand-page .product-tab.active {
|
||
background-color: rgba(46, 142, 255, 0.9);
|
||
box-shadow: 0 0 15px rgba(46, 142, 255, 0.7);
|
||
}
|
||
|
||
#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 {
|
||
color: #003d7a;
|
||
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="batch-delete-btn" class="btn btn-danger">批量删除</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-area">
|
||
|
||
<!-- 产品选择标签页 -->
|
||
<div style="padding: 0 20px;">
|
||
<div class="product-tabs-wrapper" id="product-tabs-container">
|
||
<button class="product-tab active" data-product="" onclick="PurchaseDemand.switchProduct('')">
|
||
<div class="product-tab-inner">全部</div>
|
||
</button>
|
||
</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>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()">×</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 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()">×</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()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<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>
|
||
<button type="button" class="btn btn-secondary" style="margin-top: 8px;" onclick="testXLSX()">测试XLSX库</button>
|
||
</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;">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>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()">×</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()">×</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-product-select-container" style="margin-top: 12px; display: none;">
|
||
<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>
|
||
</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();
|
||
}, 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) => {
|
||
// 优先使用 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-tab').forEach(tab => {
|
||
if (tab.dataset.product === productCode) {
|
||
tab.classList.add('active');
|
||
} else {
|
||
tab.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
renderList();
|
||
}
|
||
|
||
async function loadProducts() {
|
||
try {
|
||
// 从采购需求表中获取产品列表
|
||
const res = await API.get('/api/purchase-demand');
|
||
const demandData = res.list || [];
|
||
|
||
// 提取唯一的产品编码
|
||
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('从采购需求表加载到的产品列表:', productList); // 调试日志
|
||
|
||
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)}</option>`).join('');
|
||
}
|
||
} catch (e) {
|
||
console.error('加载产品列表失败:', e);
|
||
}
|
||
}
|
||
|
||
let currentProduct = ''; // 当前选中的产品
|
||
|
||
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 grouped = {};
|
||
demandList.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.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;
|
||
}
|
||
|
||
function openCalcModal() {
|
||
document.getElementById('product-select').value = '';
|
||
document.getElementById('order-qty').value = 1;
|
||
document.getElementById('calc-modal').style.display = 'flex';
|
||
// 重新加载产品列表
|
||
loadProducts();
|
||
}
|
||
|
||
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() {
|
||
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
// 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 missingHeaders = requiredHeaders.filter(h => !headers.includes(h));
|
||
|
||
if (missingHeaders.length > 0) {
|
||
API.toast(`缺少必要的列:${missingHeaders.join(', ')}。当前列:${headers.join(', ')}`, '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 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 = {
|
||
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.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('保存失败');
|
||
}
|
||
}
|
||
|
||
function openBatchEditStatusModal() {
|
||
// 获取选中的复选框
|
||
const checkboxes = document.querySelectorAll('.row-checkbox:checked');
|
||
const selectedCount = checkboxes.length;
|
||
|
||
// 更新选中数量显示
|
||
document.getElementById('selected-count').textContent = selectedCount;
|
||
|
||
// 加载产品列表
|
||
loadProductOptions();
|
||
|
||
// 重置表单
|
||
document.getElementById('batch-status').value = '';
|
||
document.getElementById('batch-remark').value = '';
|
||
document.querySelector('input[name="batch-scope"][value="selected"]').checked = true;
|
||
document.getElementById('batch-product-select-container').style.display = 'none';
|
||
|
||
// 添加事件监听
|
||
document.getElementById('batch-scope-all').onchange = function() {
|
||
document.getElementById('batch-product-select-container').style.display =
|
||
this.checked ? 'block' : 'none';
|
||
};
|
||
document.getElementById('batch-scope-selected').onchange = function() {
|
||
document.getElementById('batch-product-select-container').style.display =
|
||
this.checked ? 'none' : 'block';
|
||
};
|
||
|
||
// 显示弹窗
|
||
document.getElementById('batch-edit-status-modal').style.display = 'flex';
|
||
}
|
||
|
||
function loadProductOptions() {
|
||
// 获取所有产品
|
||
const products = new Set();
|
||
demandList.forEach(item => {
|
||
if (item.product_code) {
|
||
products.add(item.product_code);
|
||
}
|
||
});
|
||
|
||
const productSelect = document.getElementById('batch-product-select');
|
||
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);
|
||
});
|
||
|
||
// 如果当前有选中的产品,设为默认值
|
||
if (currentProduct) {
|
||
productSelect.value = currentProduct;
|
||
}
|
||
}
|
||
|
||
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 selectedProduct = document.getElementById('batch-product-select').value;
|
||
if (!selectedProduct) {
|
||
alert('请选择要更新的产品');
|
||
return;
|
||
}
|
||
requestData.product_code = selectedProduct;
|
||
confirmMessage = `确定要将"${selectedProduct}"的所有记录状态更新为"${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 importExcel() {
|
||
if (excelData.length === 0) {
|
||
API.toast('没有可导入的数据', 'error');
|
||
return;
|
||
}
|
||
|
||
// 获取产品编码
|
||
const productCode = document.getElementById('import-product-code').value.trim();
|
||
if (!productCode) {
|
||
API.toast('请输入产品编码', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 确保XLSX库已加载(虽然这里不需要,但为了保持一致性)
|
||
await ensureXLSXLoaded();
|
||
|
||
console.log('准备导入的数据:', excelData); // 调试日志
|
||
console.log('产品编码:', productCode); // 调试日志
|
||
|
||
const res = await API.post('/api/purchase-demand/import', {
|
||
data: excelData,
|
||
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(); // 重新加载列表
|
||
} 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,
|
||
exportExcel,
|
||
deleteProductData,
|
||
edit,
|
||
closeEditModal,
|
||
saveEdit,
|
||
openBatchEditStatusModal,
|
||
closeBatchEditStatusModal,
|
||
saveBatchEditStatus,
|
||
syncToInitialStock,
|
||
toggleSelectAll,
|
||
switchProduct,
|
||
goPage
|
||
};
|
||
})();
|