// 对账单管理 (() => { Router.register('/plan-mgmt/reconciliation', async () => { const html = `
序号 下单时间 合同编号 物料名称 规格型号 运输单号 数量 单位 含税单价 含税金额 交货日期 出货日期 操作
加载中...
`; 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 = '暂无数据'; 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 ` ${index + 1} ${item.order_date || '—'} ${item.contract_no || '—'} ${item.material_name || '—'} ${item.spec_model || '—'} ${item.transport_no || '—'} ${item.quantity || 0} ${item.unit || '—'} ${formatNumber(item.unit_price)} ${formatNumber(item.total_amount)} ${item.delivery_date || '—'} ${item.shipment_date || '—'} `; }).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 = '加载失败,请刷新重试'; } 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 }; })();