ERP/frontend/js/components/reconciliation.js

560 lines
21 KiB
JavaScript
Raw Normal View History

2025-11-25 02:35:02 +00:00
// 对账单管理
(() => {
Router.register('/plan-mgmt/reconciliation', async () => {
const html = `
<div class="page-header">
<h1>对账单管理</h1>
<div class="page-actions">
<button id="batch-delete-btn" class="btn btn-danger" style="margin-right: 10px; display: none;">批量删除</button>
<button id="export-reconciliation-btn" class="btn btn-secondary" style="margin-right: 10px;">导出对账单</button>
<button id="upload-shipment-btn" class="btn btn-secondary" style="margin-right: 10px;">上传发货单</button>
<button id="add-reconciliation-btn" class="btn btn-primary">新增对账单</button>
<input type="file" id="shipment-file-input" accept=".xls,.xlsx,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" style="display: none;" />
</div>
</div>
<div class="card">
<div class="card-body">
<div class="table-container" style="max-height: calc(100vh - 200px); overflow-y: auto; padding-bottom: 5px;">
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" id="select-all-reconciliation" style="cursor: pointer;" />
</th>
<th>序号</th>
<th>下单时间</th>
<th>合同编号</th>
<th>物料名称</th>
<th>规格型号</th>
<th>运输单号</th>
<th>数量</th>
<th>单位</th>
<th>含税单价</th>
<th>含税金额</th>
<th>交货日期</th>
<th>出货日期</th>
<th>操作</th>
</tr>
</thead>
<tbody id="reconciliation-list">
<tr>
<td colspan="14" class="text-center">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="reconciliation-modal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2 id="modal-title">新增对账单</h2>
<button class="modal-close" onclick="window.Reconciliation.closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="reconciliation-form">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div class="field">
<label>下单时间 <span style="color: var(--danger);">*</span></label>
<input type="date" id="order-date" class="input" required />
</div>
<div class="field">
<label>合同编号 <span style="color: var(--danger);">*</span></label>
<input type="text" id="contract-no" class="input" placeholder="如CGDD002876" required />
</div>
<div class="field">
<label>物料名称 <span style="color: var(--danger);">*</span></label>
<input type="text" id="material-name" class="input" placeholder="如:扩产-9988 红黑线" required />
</div>
<div class="field">
<label>规格型号 <span style="color: var(--danger);">*</span></label>
<input type="text" id="spec-model" class="input" placeholder="如PCXK0P0NSNT_1_2.54*1C14TE" required />
</div>
<div class="field">
<label>运输单号</label>
<input type="text" id="transport-no" class="input" placeholder="如:快递上门" />
</div>
<div class="field">
<label>数量 <span style="color: var(--danger);">*</span></label>
<input type="number" id="quantity" class="input" placeholder="数量" min="1" required />
</div>
<div class="field">
<label>单位 <span style="color: var(--danger);">*</span></label>
<input type="text" id="unit" class="input" placeholder="如pcs" value="pcs" required />
</div>
<div class="field">
<label>含税单价 <span style="color: var(--danger);">*</span></label>
<input type="number" id="unit-price" class="input" placeholder="单价" step="0.1" min="0" required />
</div>
<div class="field">
<label>含税金额</label>
<input type="number" id="total-amount" class="input" placeholder="自动计算" step="0.1" readonly />
</div>
<div class="field">
<label>交货日期</label>
<input type="date" id="delivery-date" class="input" />
</div>
<div class="field">
<label>出货日期</label>
<input type="date" id="shipment-date" class="input" />
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="window.Reconciliation.closeModal()">取消</button>
<button class="btn btn-primary" onclick="window.Reconciliation.saveReconciliation()">保存</button>
</div>
</div>
</div>
`;
setTimeout(() => {
const addBtn = document.getElementById('add-reconciliation-btn');
if (addBtn) {
addBtn.addEventListener('click', () => {
openModal();
});
}
// 全选/取消全选
const selectAllCheckbox = document.getElementById('select-all-reconciliation');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.reconciliation-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked);
updateBatchDeleteButton();
});
}
// 批量删除按钮
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', async () => {
const selectedIds = getSelectedReconciliationIds();
if (selectedIds.length === 0) {
API.toast('请选择要删除的对账单', 'warning');
return;
}
if (!confirm(`确定要删除选中的 ${selectedIds.length} 条对账单吗?`)) {
return;
}
await batchDeleteReconciliations(selectedIds);
});
}
const exportBtn = document.getElementById('export-reconciliation-btn');
if (exportBtn) {
exportBtn.addEventListener('click', async () => {
try {
API.toast('正在导出对账单...', 'info');
const res = await fetch('/api/reconciliations/export');
if (!res.ok) {
const data = await res.json();
API.toast(data.error || '导出失败', 'error');
return;
}
// 下载文件
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 从响应头获取文件名,如果没有则使用默认名称
const contentDisposition = res.headers.get('Content-Disposition');
let filename = '对账单.xlsx';
if (contentDisposition) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
// 解码 URL 编码的文件名
filename = decodeURIComponent(filename);
}
}
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
API.toast('导出成功', 'success');
} catch (err) {
console.error('导出对账单失败:', err);
API.toast('导出失败', 'error');
}
});
}
const uploadBtn = document.getElementById('upload-shipment-btn');
const fileInput = document.getElementById('shipment-file-input');
if (uploadBtn && fileInput) {
uploadBtn.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
// 验证文件类型
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
API.toast('请上传 XLS 或 XLSX 格式的发货单', 'error');
fileInput.value = '';
return;
}
// 显示上传中提示
API.toast('正在解析发货单...', 'info');
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/reconciliations/upload-shipment', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok && data.ok) {
let message = data.message;
if (data.errors && data.errors.length > 0) {
message += '\n\n错误详情\n' + data.errors.join('\n');
API.toast(message, 'warning');
} else {
API.toast(message, 'success');
}
await loadReconciliations();
} else {
API.toast(data.error || '上传失败', 'error');
}
} catch (err) {
console.error('上传发货单失败:', err);
API.toast('上传失败', 'error');
} finally {
fileInput.value = '';
}
});
}
const quantityInput = document.getElementById('quantity');
const unitPriceInput = document.getElementById('unit-price');
const totalAmountInput = document.getElementById('total-amount');
const calculateTotal = () => {
const qty = parseFloat(quantityInput.value) || 0;
const price = parseFloat(unitPriceInput.value) || 0;
const total = qty * price;
totalAmountInput.value = total.toFixed(1);
};
if (quantityInput) quantityInput.addEventListener('input', calculateTotal);
if (unitPriceInput) unitPriceInput.addEventListener('input', calculateTotal);
loadReconciliations();
}, 100);
return html;
});
async function loadReconciliations() {
try {
const res = await fetch('/api/reconciliations');
const data = await res.json();
const tbody = document.getElementById('reconciliation-list');
if (!tbody) return;
if (!data.list || data.list.length === 0) {
tbody.innerHTML = '<tr><td colspan="14" class="text-center">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.list.map((item, index) => {
// 格式化数字:去掉不必要的小数点和尾随零
const formatNumber = (num) => {
if (!num && num !== 0) return '—';
const n = parseFloat(num);
// 先四舍五入到2位小数避免浮点数精度问题
const rounded = Math.round(n * 100) / 100;
// 如果是整数,直接返回整数
if (Number.isInteger(rounded)) return rounded.toString();
// 否则返回字符串自动去掉尾随的0
return rounded.toString();
};
return `
<tr>
<td>
<input type="checkbox" class="reconciliation-checkbox" data-id="${item.id}" style="cursor: pointer;" onchange="window.Reconciliation.updateBatchDeleteButton()" />
</td>
<td>${index + 1}</td>
<td>${item.order_date || '—'}</td>
<td>${item.contract_no || '—'}</td>
<td>${item.material_name || '—'}</td>
<td>${item.spec_model || '—'}</td>
<td>${item.transport_no || '—'}</td>
<td>${item.quantity || 0}</td>
<td>${item.unit || '—'}</td>
<td>${formatNumber(item.unit_price)}</td>
<td>${formatNumber(item.total_amount)}</td>
<td>${item.delivery_date || '—'}</td>
<td>${item.shipment_date || '—'}</td>
<td>
<button class="btn-text" onclick="window.Reconciliation.editReconciliation(${item.id})">编辑</button>
<button class="btn-text btn-danger" onclick="window.Reconciliation.deleteReconciliation(${item.id})">删除</button>
</td>
</tr>
`;
}).join('');
// 重置全选状态
const selectAllCheckbox = document.getElementById('select-all-reconciliation');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
updateBatchDeleteButton();
} catch (err) {
console.error('加载对账单失败:', err);
const tbody = document.getElementById('reconciliation-list');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="14" class="text-center" style="color: red;">加载失败,请刷新重试</td></tr>';
}
API.toast('加载对账单失败', 'error');
}
}
let currentEditId = null;
// 日期格式转换YYYY/MM/DD -> YYYY-MM-DD (用于input[type=date])
function formatDateForInput(dateStr) {
if (!dateStr) return '';
return dateStr.replace(/\//g, '-');
}
function openModal(item = null) {
const modal = document.getElementById('reconciliation-modal');
const title = document.getElementById('modal-title');
if (item) {
// 编辑模式
title.textContent = '编辑对账单';
currentEditId = item.id;
document.getElementById('order-date').value = formatDateForInput(item.order_date) || '';
document.getElementById('contract-no').value = item.contract_no || '';
document.getElementById('material-name').value = item.material_name || '';
document.getElementById('spec-model').value = item.spec_model || '';
document.getElementById('transport-no').value = item.transport_no || '';
document.getElementById('quantity').value = item.quantity || '';
document.getElementById('unit').value = item.unit || 'pcs';
document.getElementById('unit-price').value = item.unit_price || '';
document.getElementById('total-amount').value = item.total_amount || '';
document.getElementById('delivery-date').value = formatDateForInput(item.delivery_date) || '';
document.getElementById('shipment-date').value = formatDateForInput(item.shipment_date) || '';
} else {
// 新增模式
title.textContent = '新增对账单';
currentEditId = null;
document.getElementById('reconciliation-form').reset();
const today = new Date().toISOString().split('T')[0];
document.getElementById('order-date').value = today;
document.getElementById('unit').value = 'pcs';
}
modal.style.display = 'flex';
}
function closeModal() {
const modal = document.getElementById('reconciliation-modal');
modal.style.display = 'none';
document.getElementById('reconciliation-form').reset();
currentEditId = null;
}
async function saveReconciliation() {
const orderDate = document.getElementById('order-date').value.trim();
const contractNo = document.getElementById('contract-no').value.trim();
const materialName = document.getElementById('material-name').value.trim();
const specModel = document.getElementById('spec-model').value.trim();
const transportNo = document.getElementById('transport-no').value.trim();
const quantity = parseInt(document.getElementById('quantity').value);
const unit = document.getElementById('unit').value.trim();
const unitPrice = parseFloat(document.getElementById('unit-price').value);
const totalAmount = parseFloat(document.getElementById('total-amount').value);
const deliveryDate = document.getElementById('delivery-date').value.trim();
const shipmentDate = document.getElementById('shipment-date').value.trim();
if (!orderDate || !contractNo || !materialName || !specModel || !quantity || !unit || isNaN(unitPrice)) {
API.toast('请填写必填项', 'error');
return;
}
if (quantity <= 0) {
API.toast('数量必须大于0', 'error');
return;
}
if (unitPrice < 0) {
API.toast('单价不能为负数', 'error');
return;
}
const payload = {
order_date: orderDate,
contract_no: contractNo,
material_name: materialName,
spec_model: specModel,
transport_no: transportNo,
quantity: quantity,
unit: unit,
unit_price: unitPrice,
total_amount: totalAmount,
delivery_date: deliveryDate || null,
shipment_date: shipmentDate || null
};
try {
const url = currentEditId ? `/api/reconciliations/${currentEditId}` : '/api/reconciliations';
const method = currentEditId ? 'PUT' : 'POST';
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (res.ok && data.ok) {
API.toast(currentEditId ? '更新成功' : '保存成功', 'success');
closeModal();
await loadReconciliations();
} else {
API.toast(data.error || '保存失败', 'error');
}
} catch (err) {
console.error('保存对账单失败:', err);
API.toast('保存失败', 'error');
}
}
async function editReconciliation(id) {
try {
const res = await fetch('/api/reconciliations');
const data = await res.json();
const item = data.list.find(r => r.id === id);
if (!item) {
API.toast('对账单不存在', 'error');
return;
}
openModal(item);
} catch (err) {
console.error('加载对账单失败:', err);
API.toast('加载失败', 'error');
}
}
async function deleteReconciliation(id) {
if (!confirm('确定要删除这条对账单吗?')) {
return;
}
try {
const res = await fetch(`/api/reconciliations/${id}`, {
method: 'DELETE'
});
const data = await res.json();
if (res.ok && data.ok) {
API.toast('删除成功', 'success');
await loadReconciliations();
} else {
API.toast(data.error || '删除失败', 'error');
}
} catch (err) {
console.error('删除对账单失败:', err);
API.toast('删除失败', 'error');
}
}
// 获取选中的对账单ID列表
function getSelectedReconciliationIds() {
const checkboxes = document.querySelectorAll('.reconciliation-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.dataset.id));
}
// 更新批量删除按钮的显示状态
function updateBatchDeleteButton() {
const selectedIds = getSelectedReconciliationIds();
const batchDeleteBtn = document.getElementById('batch-delete-btn');
if (batchDeleteBtn) {
if (selectedIds.length > 0) {
batchDeleteBtn.style.display = 'inline-block';
batchDeleteBtn.textContent = `批量删除 (${selectedIds.length})`;
} else {
batchDeleteBtn.style.display = 'none';
}
}
// 更新全选框状态
const selectAllCheckbox = document.getElementById('select-all-reconciliation');
const allCheckboxes = document.querySelectorAll('.reconciliation-checkbox');
if (selectAllCheckbox && allCheckboxes.length > 0) {
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
const someChecked = Array.from(allCheckboxes).some(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
}
}
// 批量删除对账单
async function batchDeleteReconciliations(ids) {
try {
API.toast('正在删除...', 'info');
const res = await fetch('/api/reconciliations/batch-delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids })
});
const data = await res.json();
if (res.ok && data.ok) {
API.toast(`成功删除 ${ids.length} 条对账单`, 'success');
await loadReconciliations();
} else {
API.toast(data.error || '批量删除失败', 'error');
}
} catch (err) {
console.error('批量删除对账单失败:', err);
API.toast('批量删除失败', 'error');
}
}
window.Reconciliation = {
openModal,
closeModal,
saveReconciliation,
editReconciliation,
deleteReconciliation,
updateBatchDeleteButton
};
})();