/** * 仓库管理模块 —— 借出单 / 借出还入单 */ window.Warehouse = (() => { let currentTab = 'borrow'; let currentUser = null; let _approvedBorrows = []; const statusLabel = (s) => { const map = { pending: '待确认', approved: '已批准', rejected: '已拒绝', received: '已同意', not_received: '未收到', partial: '部分收到', returned_back: '已退回' }; return map[s] || s; }; const statusColor = (s) => { const map = { pending: '#f59e0b', approved: '#10b981', rejected: '#ef4444', received: '#10b981', not_received: '#ef4444', partial: '#f59e0b', returned_back: '#6b7280' }; return map[s] || '#94a3b8'; }; const isSuperadmin = () => currentUser && currentUser.role === 'superadmin'; const isAdmin = () => currentUser && currentUser.role === 'admin'; // ── 主渲染 ────────────────────────────────── const render = async () => { return `
${tabBtn('borrow', '借出单', currentTab)} ${tabBtn('return', '借出还入单', currentTab)}
`; }; const tabBtn = (key, label, cur) => ``; const renderUnreturnedCard = async () => { try { const res = await fetch('/api/warehouse/unreturned-summary', { credentials: 'include' }); const data = res.ok ? await res.json() : { summary: [], total_unreturned: 0 }; const summary = data.summary || []; if (summary.length === 0) return ''; const rows = summary.map(s => ` ${s.model} 未还 ${s.unreturned} (借 ${s.borrowed} / 还 ${s.returned}) ` ).join(''); return `
⚠️ 未还汇总 ${rows}
`; } catch (e) { return ''; } }; const init = async () => { renderTab(); }; const switchTab = (tab) => { currentTab = tab; document.querySelectorAll('.wh-tab-btn').forEach(b => { b.classList.toggle('active', b.textContent.trim() === (tab === 'borrow' ? '借出单' : '借出还入单')); }); renderTab(); }; // ── 借出单列表 ────────────────────────────── const renderTab = async () => { const el = document.getElementById('warehouse-content'); if (!el) return; if (currentTab === 'borrow') await renderBorrowTab(el); else await renderReturnTab(el); }; const renderBorrowTab = async (el) => { el.innerHTML = '
加载中...
'; const [res, retRes] = await Promise.all([ fetch('/api/warehouse/borrow-orders', { credentials: 'include' }), fetch('/api/warehouse/return-orders', { credentials: 'include' }) ]); const data = res.ok ? await res.json() : { list: [] }; const retData = retRes.ok ? await retRes.json() : { list: [] }; const list = data.list || []; // 按borrow_order_no聚合已同意的还入数量 const receivedMap = {}; (retData.list || []).forEach(r => { if (r.status === 'received') { receivedMap[r.borrow_order_no] = (receivedMap[r.borrow_order_no] || 0) + (r.received_qty != null ? r.received_qty : r.qty); } }); const pendingCount = list.filter(r => r.status === 'pending').length; el.innerHTML = `
借出单列表 ${pendingCount > 0 ? `${pendingCount} 待确认` : ''}
${isSuperadmin() ? `` : ''}
${isAdmin() ? '' : ''} ${list.length === 0 ? `` : ''} ${list.map(r => { const received = receivedMap[r.order_no] || 0; const unreturned = r.status === 'approved' ? Math.max(r.qty - received, 0) : null; return ` ${isAdmin() ? `` : ''} `;}).join('')}
单号型号数量未还数借出理由 申请人申请时间状态拒绝原因确认人操作
暂无数据
${r.order_no} ${r.model} ${r.qty} ${unreturned == null ? '—' : unreturned} ${r.reason} ${r.created_by} ${(r.created_at || '').replace('T',' ').slice(0, 16)} ${statusLabel(r.status)} ${r.reject_reason || '—'} ${r.confirmed_by || '—'} ${r.status === 'pending' && currentUser.role === 'admin' ? ` ` : '—'}
`; }; // ── 还入单列表 ────────────────────────────── const renderReturnTab = async (el) => { el.innerHTML = '
加载中...
'; const [retRes, borrowRes] = await Promise.all([ fetch('/api/warehouse/return-orders', { credentials: 'include' }), fetch('/api/warehouse/borrow-orders?status=approved', { credentials: 'include' }) ]); const retData = retRes.ok ? await retRes.json() : { list: [] }; const borrowData = borrowRes.ok ? await borrowRes.json() : { list: [] }; const list = retData.list || []; _approvedBorrows = borrowData.list || []; const pendingCount = list.filter(r => r.status === 'pending').length; el.innerHTML = `
借出还入单列表 ${pendingCount > 0 ? `${pendingCount} 待确认` : ''}
${isSuperadmin() ? `` : ''}
${isAdmin() ? '' : ''} ${list.length === 0 ? `` : ''} ${list.map(r => ` ${isAdmin() ? `` : ''} `).join('')}
还入单号对应借出单型号还入数 申请人申请时间状态备注确认人操作
暂无数据
${r.order_no} ${r.borrow_order_no} ${r.model} ${r.qty} ${r.created_by} ${(r.created_at || '').replace('T',' ').slice(0, 16)} ${statusLabel(r.status)} ${r.reject_reason || '—'} ${r.confirmed_by || '—'} ${r.status === 'pending' && currentUser.role === 'admin' ? ` ` : '—'}
`; }; // ── 新建借出单弹窗 ────────────────────────── const showCreateBorrowModal = () => { const html = `
新建借出单
`; document.body.insertAdjacentHTML('beforeend', html); }; const submitBorrow = async () => { const model = document.getElementById('wh-b-model')?.value.trim(); const qty = document.getElementById('wh-b-qty')?.value; const reason = document.getElementById('wh-b-reason')?.value.trim(); if (!model || !qty || !reason) { API.toast('请填写所有必填项'); return; } const res = await fetch('/api/warehouse/borrow-orders', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model, qty: parseInt(qty), reason }) }); const data = await res.json(); if (data.ok) { API.toast(`借出单 ${data.order_no} 已提交,等待仓管确认`); closeModal(); renderTab(); } else { API.toast(data.error || '提交失败'); } }; // ── 新建还入单弹窗 ────────────────────────── const showCreateReturnModal = () => { const options = (_approvedBorrows || []).map(b => `` ).join(''); const html = `
新建借出还入单
`; document.body.insertAdjacentHTML('beforeend', html); }; const onBorrowSelect = (sel) => { const opt = sel.options[sel.selectedIndex]; const model = opt.getAttribute('data-model') || ''; const modelEl = document.getElementById('wh-r-model'); if (modelEl) modelEl.value = model; }; const submitReturn = async () => { const borrow_order_no = document.getElementById('wh-r-borrow')?.value; const model = document.getElementById('wh-r-model')?.value.trim(); const qty = document.getElementById('wh-r-qty')?.value; if (!borrow_order_no || !model || !qty) { API.toast('请填写所有必填项'); return; } const res = await fetch('/api/warehouse/return-orders', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ borrow_order_no, model, qty: parseInt(qty) }) }); const data = await res.json(); if (data.ok) { API.toast(`还入单 ${data.order_no} 已提交,等待仓管确认`); closeModal(); renderTab(); } else { API.toast(data.error || '提交失败'); } }; // ── 拒绝弹窗 ──────────────────────────────── const showRejectModal = (type, orderNo) => { const html = `
拒绝单据
单号:${orderNo}
`; document.body.insertAdjacentHTML('beforeend', html); }; const doReject = async (type, orderNo) => { const reject_reason = document.getElementById('wh-reject-reason')?.value.trim(); if (!reject_reason) { API.toast('请填写拒绝原因'); return; } const url = type === 'borrow' ? `/api/warehouse/borrow-orders/${orderNo}/confirm` : `/api/warehouse/return-orders/${orderNo}/confirm`; const res = await fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'reject', reject_reason }) }); const data = await res.json(); if (data.ok) { API.toast('已拒绝'); closeModal(); renderTab(); } else API.toast(data.error || '操作失败'); }; // ── 批准操作 ──────────────────────────────── const confirmBorrow = async (orderNo, action) => { const res = await fetch(`/api/warehouse/borrow-orders/${orderNo}/confirm`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }) }); const data = await res.json(); if (data.ok) { API.toast('操作成功'); renderTab(); } else API.toast(data.error || '操作失败'); }; const confirmReturn = async (orderNo, action) => { const res = await fetch(`/api/warehouse/return-orders/${orderNo}/confirm`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }) }); const data = await res.json(); if (data.ok) { API.toast('操作成功'); renderTab(); } else API.toast(data.error || '操作失败'); }; const showNotReceivedModal = (orderNo, totalQty) => { const html = `
退回还入单
`; document.body.insertAdjacentHTML('beforeend', html); }; const submitNotReceived = async (orderNo, totalQty) => { const note = document.getElementById('wh-not-recv-note')?.value || ''; const res = await fetch(`/api/warehouse/return-orders/${orderNo}/confirm`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'reject', note }) }); const data = await res.json(); if (data.ok) { closeModal(); API.toast('已退回'); renderTab(); } else API.toast(data.error || '操作失败'); }; const closeModal = () => { document.getElementById('wh-modal')?.remove(); }; const renderPage = async (tab) => { currentTab = tab; try { const res = await fetch('/api/auth/me', { credentials: 'include' }); currentUser = res.ok ? await res.json() : null; } catch (e) { currentUser = null; } return render(); }; return { render, renderPage, init, switchTab, showCreateBorrowModal, submitBorrow, showCreateReturnModal, onBorrowSelect, submitReturn, showRejectModal, doReject, confirmBorrow, confirmReturn, showNotReceivedModal, submitNotReceived, closeModal }; })(); Router.register('/warehouse/borrow', async () => { const html = await window.Warehouse.renderPage('borrow'); setTimeout(() => { window.Warehouse.init(); }, 0); return html; }); Router.register('/warehouse/return', async () => { const html = await window.Warehouse.renderPage('return'); setTimeout(() => { window.Warehouse.init(); }, 0); return html; });