采购详情支持手动修改
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
// 采购相关功能
|
||||
let editPid = null; // 当前正在编辑的采购ID
|
||||
|
||||
async function loadPurchases() {
|
||||
if (!Store.currentRoom) return;
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/rooms/' + Store.currentRoom + '/purchases', { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
const res = await fetch('/api/rooms/' + Store.currentRoom + '/purchases', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const items = await res.json();
|
||||
renderPurchasePanel(items);
|
||||
}
|
||||
@@ -20,16 +22,16 @@ function renderPurchasePanel(items) {
|
||||
Object.keys(groups).sort().reverse().forEach(month => {
|
||||
const monthItems = groups[month];
|
||||
const monthTotal = monthItems.reduce((s, i) => s + (i.amount||0), 0);
|
||||
html += `<div class="purchase-month-group"><div class="purchase-month-header"><span>${month}</span><span>¥${monthTotal}</span></div>`;
|
||||
html += '<div class="purchase-month-group"><div class="purchase-month-header"><span>' + month + '</span><span>' + monthTotal + '</span></div>';
|
||||
monthItems.forEach(item => {
|
||||
const timeShort = item.created_at.split(' ')[1]?.substring(0,5) || '';
|
||||
const dateShort = item.created_at.split(' ')[0]?.substring(5) || '';
|
||||
html += `
|
||||
<div class="purchase-item" onclick="openDetail('${item.id}')">
|
||||
<div class="item-main"><span class="item-name">${escapeHtml(item.item)}</span><span class="item-amount">¥${item.amount}</span></div>
|
||||
<div class="item-main"><span class="item-name">${escapeHtml(item.item)}</span><span class="item-amount">${item.amount}</span></div>
|
||||
<div class="item-meta">
|
||||
<span>${dateShort} ${timeShort}</span>
|
||||
<span>×${item.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''}</span>
|
||||
<span>${item.quantity} ${item.invoice_type !== '无票' ? '· ' + item.invoice_type : ''}</span>
|
||||
<span class="status-badge ${item.status==='已完成'?'done':''}">${item.status}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -42,54 +44,210 @@ function renderPurchasePanel(items) {
|
||||
function formatMonth(dateStr) {
|
||||
const clean = dateStr.replace(/-/g, '/');
|
||||
const parts = clean.split('/');
|
||||
if (parts.length >= 2) return `${parts[0]}年${parseInt(parts[1], 10)}月`;
|
||||
if (parts.length >= 2) return parts[0] + '年' + parseInt(parts[1], 10) + '月';
|
||||
return dateStr.substring(0,7);
|
||||
}
|
||||
|
||||
// ============ 可编辑详情弹窗 ============
|
||||
|
||||
async function openDetail(pid) {
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/purchases/' + pid, { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
const res = await fetch('/api/purchases/' + pid, { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const p = await res.json();
|
||||
document.getElementById('detail-title').textContent = p.item;
|
||||
let attachHtml = '';
|
||||
if (p.attachments?.length) attachHtml = '<div class="attachments">' + p.attachments.map(a => `<img class="img-thumb" src="${a.file_path}" alt="${a.file_path}" ondblclick="onImgThumbDblClick(this)">`).join('') + '</div>';
|
||||
const historyHtml = p.history?.map(h => `<li><span class="history-time">${h.timestamp}</span> ${h.user}: ${h.action}</li>`).join('') || '';
|
||||
document.getElementById('detail-content').innerHTML = `
|
||||
<div class="detail-row"><strong>数量:</strong>${p.quantity}</div>
|
||||
<div class="detail-row"><strong>单价:</strong>¥${p.unit_price}</div>
|
||||
<div class="detail-row"><strong>邮费:</strong>¥${p.freight}</div>
|
||||
<div class="detail-row"><strong>总金额:</strong>¥${p.amount}</div>
|
||||
<div class="detail-row"><strong>付款方式:</strong>${p.payment_method || '未指定'}</div>
|
||||
<div class="detail-row"><strong>发票:</strong>${p.invoice_type || '无票'}</div>
|
||||
<div class="detail-row"><strong>状态:</strong>${p.status}</div>
|
||||
<div class="detail-row"><strong>申请人:</strong>${p.applicant}</div>
|
||||
<div class="detail-row"><strong>采购时间:</strong>${p.created_at}</div>
|
||||
<div class="detail-row"><strong>备注:</strong>${p.remarks || '无'}</div>
|
||||
${attachHtml}
|
||||
<h4 style="margin-top:12px;">操作历史</h4>
|
||||
<ul class="history-list">${historyHtml}</ul>`;
|
||||
editPid = pid;
|
||||
|
||||
document.getElementById('edit-item').value = p.item || '';
|
||||
document.getElementById('edit-quantity').value = p.quantity || 0;
|
||||
document.getElementById('edit-unit-price').value = p.unit_price || 0;
|
||||
document.getElementById('edit-freight').value = p.freight || 0;
|
||||
updateEditAmount();
|
||||
document.getElementById('edit-payment-method').value = p.payment_method || '未指定';
|
||||
document.getElementById('edit-invoice-type').value = p.invoice_type || '无票';
|
||||
document.getElementById('edit-status').value = p.status || '待付款';
|
||||
document.getElementById('edit-applicant').value = p.applicant || '';
|
||||
document.getElementById('edit-time').value = p.created_at || '';
|
||||
document.getElementById('edit-remarks').value = p.remarks || '';
|
||||
|
||||
// 附件
|
||||
renderEditAttachments(p.attachments || []);
|
||||
|
||||
// 操作历史
|
||||
const historyHtml = (p.history || []).map(h =>
|
||||
'<li><span class="history-time">' + h.timestamp + '</span> ' + escapeHtml(h.user) + ': ' + escapeHtml(h.action) + '</li>'
|
||||
).join('');
|
||||
document.getElementById('edit-history').innerHTML = historyHtml || '<li style="color:#888;">暂无操作记录</li>';
|
||||
document.getElementById('edit-history-section').open = false;
|
||||
|
||||
// 删除按钮仅管理员可见
|
||||
const isAdmin = localStorage.getItem('isAdmin') === 'true';
|
||||
document.getElementById('delete-purchase-btn').style.display = isAdmin ? '' : 'none';
|
||||
|
||||
document.getElementById('detail-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDetailModal() { document.getElementById('detail-modal').classList.add('hidden'); }
|
||||
function renderEditAttachments(attachments) {
|
||||
const container = document.getElementById('edit-attachments');
|
||||
if (!attachments || attachments.length === 0) {
|
||||
container.innerHTML = '<span style="color:#888;font-size:14px;">暂无附件</span>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = attachments.map(a =>
|
||||
'<div class="attach-thumb-wrap">' +
|
||||
'<img class="attach-thumb" src="' + a.file_path + '" ondblclick="onImgThumbDblClick(this)">' +
|
||||
'<button class="attach-del-btn" onclick="deleteAttachment(event, \'' + encodeURIComponent(a.file_path) + '\')">' +
|
||||
'<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' +
|
||||
'</button>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
}
|
||||
|
||||
function updateEditAmount() {
|
||||
const q = parseFloat(document.getElementById('edit-quantity').value) || 0;
|
||||
const up = parseFloat(document.getElementById('edit-unit-price').value) || 0;
|
||||
const f = parseFloat(document.getElementById('edit-freight').value) || 0;
|
||||
document.getElementById('edit-amount').textContent = (q * up + f).toFixed(2);
|
||||
}
|
||||
|
||||
async function savePurchase() {
|
||||
if (!editPid) return;
|
||||
const token = Store.getToken();
|
||||
const body = {
|
||||
item: document.getElementById('edit-item').value,
|
||||
quantity: parseFloat(document.getElementById('edit-quantity').value) || 0,
|
||||
unit_price: parseFloat(document.getElementById('edit-unit-price').value) || 0,
|
||||
freight: parseFloat(document.getElementById('edit-freight').value) || 0,
|
||||
payment_method: document.getElementById('edit-payment-method').value,
|
||||
invoice_type: document.getElementById('edit-invoice-type').value,
|
||||
status: document.getElementById('edit-status').value,
|
||||
applicant: document.getElementById('edit-applicant').value,
|
||||
remarks: document.getElementById('edit-remarks').value,
|
||||
created_at: document.getElementById('edit-time').value
|
||||
};
|
||||
const res = await fetch('/api/purchases/' + editPid, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
closeDetailModal();
|
||||
loadPurchases();
|
||||
} else {
|
||||
alert('保存失败:' + (result.error || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePurchaseDetail() {
|
||||
if (!editPid) return;
|
||||
if (!confirm('确定要删除此采购记录吗?')) return;
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/purchases/' + editPid, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
if (res.ok) {
|
||||
closeDetailModal();
|
||||
loadPurchases();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('删除失败:' + (err.error || '未知错误'));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAttachment(event, encodedPath) {
|
||||
event.stopPropagation();
|
||||
const filePath = decodeURIComponent(encodedPath);
|
||||
if (!confirm('确定删除此附件吗?')) return;
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/purchases/' + editPid + '/attachments', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_path: filePath })
|
||||
});
|
||||
if (res.ok) {
|
||||
// 重新加载详情
|
||||
openDetail(editPid);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDetailFileUpload(event) {
|
||||
const files = event.target.files;
|
||||
if (!files.length) return;
|
||||
const token = Store.getToken();
|
||||
const paths = [];
|
||||
try {
|
||||
for (let file of files) {
|
||||
let blob = file;
|
||||
if (file.size > 1 * 1024 * 1024) blob = await compressImage(file);
|
||||
const formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg');
|
||||
const res = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData });
|
||||
const data = await res.json();
|
||||
if (data.path) paths.push(data.path);
|
||||
}
|
||||
if (paths.length) {
|
||||
await fetch('/api/purchases/' + editPid + '/attachments', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_paths: paths })
|
||||
});
|
||||
openDetail(editPid);
|
||||
}
|
||||
} catch(e) { alert('上传失败,请重试'); }
|
||||
}
|
||||
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detail-modal').classList.add('hidden');
|
||||
editPid = null;
|
||||
}
|
||||
|
||||
// ============ 采购页面输入区 ============
|
||||
|
||||
async function handlePurchaseFileSelect(event) {
|
||||
const files = event.target.files;
|
||||
if (!files.length) return;
|
||||
const token = Store.getToken();
|
||||
try {
|
||||
for (let file of files) {
|
||||
let blob = file;
|
||||
if (file.size > 1 * 1024 * 1024) blob = await compressImage(file);
|
||||
const formData = new FormData(); formData.append('file', blob, file.name || 'image.jpg');
|
||||
const res = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: formData });
|
||||
const data = await res.json();
|
||||
if (data.path) Store.pendingUploads.push(data.path);
|
||||
}
|
||||
} catch(e) { alert('图片上传失败'); }
|
||||
}
|
||||
|
||||
function sendFromPurchase() {
|
||||
const input = document.getElementById('purchase-msg-input');
|
||||
const text = input.value.trim();
|
||||
if (!text && Store.pendingUploads.length === 0) return;
|
||||
// 记录文本,跳转到聊天页面后发送
|
||||
Store.pendingPurchaseText = text;
|
||||
input.value = '';
|
||||
openChatFromPurchase();
|
||||
}
|
||||
|
||||
// ============ 汇总 & 群聊管理 ============
|
||||
|
||||
async function openSummary() {
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/summary', { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
const res = await fetch('/api/summary', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const summary = await res.json();
|
||||
let html = '';
|
||||
if (summary.length === 0) html = '<p>暂无采购记录</p>';
|
||||
else {
|
||||
summary.forEach(s => {
|
||||
html += `<div class="summary-card" onclick="event.stopPropagation(); closeDetailModal(); openPurchasePage('${s.room_id}')" style="margin:8px 0;"><h4>${escapeHtml(s.room_name)}</h4><p>${s.purchase_count} 条,合计 ¥${s.total_amount}</p></div>`;
|
||||
html += '<div class="summary-card" onclick="event.stopPropagation(); closeSummaryModal(); openPurchasePage(\'' + s.room_id + '\')" style="margin:8px 0;"><h4>' + escapeHtml(s.room_name) + '</h4><p>' + s.purchase_count + ' 条,合计 ' + s.total_amount + '</p></div>';
|
||||
});
|
||||
}
|
||||
document.getElementById('detail-title').textContent = '采购汇总';
|
||||
document.getElementById('detail-content').innerHTML = html;
|
||||
document.getElementById('detail-modal').classList.remove('hidden');
|
||||
document.getElementById('summary-title').textContent = '采购汇总';
|
||||
document.getElementById('summary-content').innerHTML = html;
|
||||
document.getElementById('summary-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 群聊管理
|
||||
function closeSummaryModal() { document.getElementById('summary-modal').classList.add('hidden'); }
|
||||
|
||||
function openCreateRoom() { document.getElementById('create-modal').classList.remove('hidden'); }
|
||||
function closeCreateModal() { document.getElementById('create-modal').classList.add('hidden'); }
|
||||
async function confirmCreateRoom() {
|
||||
@@ -97,7 +255,7 @@ async function confirmCreateRoom() {
|
||||
if (!name) return alert('请输入名称');
|
||||
const whiteList = document.getElementById('white-list').value.trim();
|
||||
const token = Store.getToken();
|
||||
await fetch('/api/rooms', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name, whiteList }) });
|
||||
await fetch('/api/rooms', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ name, whiteList }) });
|
||||
closeCreateModal();
|
||||
loadRooms();
|
||||
}
|
||||
@@ -121,7 +279,7 @@ async function saveRoom() {
|
||||
if (!name) return alert('请输入群聊名称');
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/rooms/' + roomId, {
|
||||
method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
method: 'PUT', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, whiteList })
|
||||
});
|
||||
if (res.ok) { closeManageModal(); loadRooms(); }
|
||||
@@ -132,7 +290,7 @@ async function deleteRoom() {
|
||||
if (!confirm('确定要删除该群聊吗?所有消息和采购数据将被永久删除。')) return;
|
||||
const roomId = document.getElementById('manage-modal').dataset.roomId;
|
||||
const token = Store.getToken();
|
||||
const res = await fetch('/api/rooms/' + roomId, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } });
|
||||
const res = await fetch('/api/rooms/' + roomId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } });
|
||||
if (res.ok) {
|
||||
closeManageModal();
|
||||
loadRooms();
|
||||
@@ -140,17 +298,31 @@ async function deleteRoom() {
|
||||
} else { const err = await res.json(); alert(err.error || '删除失败'); }
|
||||
}
|
||||
|
||||
// 滑动手势
|
||||
// ============ 滑动手势 ============
|
||||
|
||||
function initSwipeGestures() {
|
||||
const purchasePage = document.getElementById('purchase-page');
|
||||
const chatPage = document.getElementById('chat-page');
|
||||
let startX = 0;
|
||||
|
||||
// 采购页:右滑回列表,左滑进聊天
|
||||
purchasePage.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }, { passive: true });
|
||||
purchasePage.addEventListener('touchend', (e) => {
|
||||
if (!startX) return;
|
||||
const diffX = e.changedTouches[0].clientX - startX;
|
||||
if (Math.abs(diffX) > 50) {
|
||||
if (diffX > 30) showList();
|
||||
else if (diffX < -30) openChatFromPurchase();
|
||||
}
|
||||
startX = 0;
|
||||
}, { passive: true });
|
||||
|
||||
// 聊天页:左右滑回采购页
|
||||
chatPage.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; }, { passive: true });
|
||||
chatPage.addEventListener('touchend', (e) => {
|
||||
if (!startX) return;
|
||||
const endX = e.changedTouches[0].clientX;
|
||||
const diffX = endX - startX;
|
||||
if (diffX > 50) backToPurchase();
|
||||
const diffX = e.changedTouches[0].clientX - startX;
|
||||
if (Math.abs(diffX) > 50) backToPurchase();
|
||||
startX = 0;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user