/**
* 仓库管理模块 —— 借出单 / 借出还入单
*/
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 `
| ${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 || '—'} |
${isAdmin() ? `
${r.status === 'pending' && currentUser.role === 'admin'
? `
`
: '—'}
| ` : ''}
`;}).join('')}
`;
};
// ── 还入单列表 ──────────────────────────────
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 => `
| ${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 || '—'} |
${isAdmin() ? `
${r.status === 'pending' && currentUser.role === 'admin'
? `
`
: '—'}
| ` : ''}
`).join('')}
`;
};
// ── 新建借出单弹窗 ──────────────────────────
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;
});