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

366 lines
13 KiB
JavaScript

// 期初库存管理
(() => {
let stockList = [];
let currentPage = 1;
const pageSize = 20;
let editingId = null;
Router.register('/plan-mgmt/initial-stock', async () => {
const html = `
<div class="page-header">
<h1>期初库存管理</h1>
<div class="page-actions">
<button id="add-stock-btn" class="btn btn-primary">新增库存</button>
<button id="import-stock-btn" class="btn btn-secondary">📥 导入Excel</button>
<button id="download-template-btn" class="btn btn-secondary">📄 下载模板</button>
<button id="batch-delete-btn" class="btn btn-danger">批量删除</button>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="filter-section" style="margin-bottom: 16px;">
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<input type="text" id="search-keyword" class="input" placeholder="搜索物料编码/名称..." style="width: 300px;" />
<button class="btn btn-primary" onclick="InitialStock.search()">搜索</button>
<button class="btn btn-secondary" onclick="InitialStock.resetSearch()">重置</button>
</div>
</div>
<div class="table-container" style="overflow-x: auto;">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"><label class="custom-checkbox"><input type="checkbox" id="select-all" onchange="InitialStock.toggleSelectAll(this)" /><span class="checkmark"></span></label></th>
<th>物料编码</th>
<th>物料名称</th>
<th>库存数量</th>
<th>单位</th>
<th>最小包装</th>
<th>供应商</th>
<th>备注</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="stock-list">
<tr><td colspan="10" class="text-center">加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="margin-top: 16px; display: flex; justify-content: center; gap: 8px;"></div>
</div>
</div>
<!-- 新增/编辑库存弹窗 -->
<div id="stock-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h2 id="modal-title">新增期初库存</h2>
<button class="modal-close" onclick="InitialStock.closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="stock-form">
<div class="field">
<label>物料编码 <span style="color: var(--danger);">*</span></label>
<input type="text" id="material-code" class="input" required />
</div>
<div class="field">
<label>物料名称 <span style="color: var(--danger);">*</span></label>
<input type="text" id="material-name" class="input" required />
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div class="field">
<label>库存数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="stock-qty" class="input" min="0" value="0" required />
</div>
<div class="field">
<label>单位</label>
<input type="text" id="unit" class="input" value="pcs" />
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div class="field">
<label>最小包装</label>
<input type="number" id="min-package" class="input" min="1" value="1" />
</div>
<div class="field">
<label>供应商</label>
<input type="text" id="supplier" class="input" />
</div>
</div>
<div class="field">
<label>备注</label>
<textarea id="remark" class="input" rows="2"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="InitialStock.closeModal()">取消</button>
<button class="btn btn-primary" onclick="InitialStock.save()">保存</button>
</div>
</div>
</div>
`;
setTimeout(() => {
document.getElementById('add-stock-btn')?.addEventListener('click', () => openModal());
document.getElementById('import-stock-btn')?.addEventListener('click', () => showImportDialog());
document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') InitialStock.search();
});
loadList();
}, 100);
return html;
});
function showImportDialog() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.xlsx,.xls,.csv';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const overlay = document.getElementById('overlay');
overlay.classList.remove('hidden');
const res = await fetch('/api/initial-stock/import', {
method: 'POST',
body: formData,
credentials: 'include'
});
overlay.classList.add('hidden');
const data = await res.json();
if (data.ok) {
alert(data.message || '导入成功');
loadList();
} else {
alert(data.error || '导入失败');
}
} catch (err) {
document.getElementById('overlay').classList.add('hidden');
alert('导入失败: ' + err.message);
}
};
input.click();
}
function downloadTemplate() {
const headers = ['物料编码', '物料名称', '库存数量', '单位', '最小包装', '供应商', '备注'];
const example = ['PCB-001', '主控PCB板', '50', 'pcs', '10', '深圳电子', ''];
const csvContent = [headers.join(','), example.join(',')].join('\n');
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = '期初库存导入模板.csv';
link.click();
}
async function loadList() {
try {
const res = await API.get('/api/initial-stock');
stockList = res.list || [];
renderList();
} catch (e) {
console.error('加载库存列表失败:', e);
document.getElementById('stock-list').innerHTML = '<tr><td colspan="10" class="text-center" style="color: var(--danger);">加载失败</td></tr>';
}
}
function renderList() {
const tbody = document.getElementById('stock-list');
const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
let filtered = stockList;
if (keyword) {
filtered = stockList.filter(item =>
(item.material_code || '').toLowerCase().includes(keyword) ||
(item.material_name || '').toLowerCase().includes(keyword)
);
}
const totalPages = Math.ceil(filtered.length / pageSize);
const start = (currentPage - 1) * pageSize;
const pageData = filtered.slice(start, start + pageSize);
if (pageData.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" class="text-center">暂无数据</td></tr>';
} else {
tbody.innerHTML = pageData.map(item => `
<tr>
<td><label class="custom-checkbox"><input type="checkbox" class="row-checkbox" data-id="${item.id}" /><span class="checkmark"></span></label></td>
<td>${escapeHtml(item.material_code || '')}</td>
<td>${escapeHtml(item.material_name || '')}</td>
<td style="font-weight: 600; color: var(--primary);">${item.stock_qty || 0}</td>
<td>${escapeHtml(item.unit || 'pcs')}</td>
<td>${item.min_package || 1}</td>
<td>${escapeHtml(item.supplier || '-')}</td>
<td>${escapeHtml(item.remark || '-')}</td>
<td>${formatTime(item.updated_at)}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="InitialStock.edit(${item.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="InitialStock.delete(${item.id})">删除</button>
</td>
</tr>
`).join('');
}
renderPagination(totalPages);
}
function renderPagination(totalPages) {
const container = document.getElementById('pagination');
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
let html = '';
html += `<button class="btn btn-sm btn-secondary" ${currentPage <= 1 ? 'disabled' : ''} onclick="InitialStock.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="InitialStock.goPage(${currentPage + 1})">下一页</button>`;
container.innerHTML = html;
}
function openModal(item = null) {
editingId = item?.id || null;
document.getElementById('modal-title').textContent = item ? '编辑期初库存' : '新增期初库存';
document.getElementById('material-code').value = item?.material_code || '';
document.getElementById('material-name').value = item?.material_name || '';
document.getElementById('stock-qty').value = item?.stock_qty || 0;
document.getElementById('unit').value = item?.unit || 'pcs';
document.getElementById('min-package').value = item?.min_package || 1;
document.getElementById('supplier').value = item?.supplier || '';
document.getElementById('remark').value = item?.remark || '';
document.getElementById('stock-modal').style.display = 'flex';
}
function closeModal() {
document.getElementById('stock-modal').style.display = 'none';
editingId = null;
}
async function save() {
const data = {
material_code: document.getElementById('material-code').value.trim(),
material_name: document.getElementById('material-name').value.trim(),
stock_qty: parseInt(document.getElementById('stock-qty').value) || 0,
unit: document.getElementById('unit').value.trim() || 'pcs',
min_package: parseInt(document.getElementById('min-package').value) || 1,
supplier: document.getElementById('supplier').value.trim(),
remark: document.getElementById('remark').value.trim()
};
if (!data.material_code || !data.material_name) {
alert('请填写物料编码和物料名称');
return;
}
try {
if (editingId) {
await API.put(`/api/initial-stock/${editingId}`, data);
alert('更新成功');
} else {
await API.post('/api/initial-stock', data);
alert('创建成功');
}
closeModal();
loadList();
} catch (e) {
alert(e.message || '操作失败');
}
}
async function edit(id) {
const item = stockList.find(x => x.id === id);
if (item) openModal(item);
}
async function deleteStock(id) {
if (!confirm('确定要删除这条库存记录吗?')) return;
try {
await API.delete(`/api/initial-stock/${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/initial-stock/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 = '';
currentPage = 1;
renderList();
}
function goPage(page) {
currentPage = page;
renderList();
}
function formatTime(ts) {
if (!ts) return '-';
try {
const d = new Date(ts);
return d.toLocaleString('zh-CN', { hour12: false });
} catch {
return ts;
}
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
window.InitialStock = {
search,
resetSearch,
edit,
delete: deleteStock,
closeModal,
save,
toggleSelectAll,
goPage
};
})();