feat: 完善采购申请流程 - 添加审批、执行、列表筛选排序功能

This commit is contained in:
System Administrator
2026-03-28 00:34:32 +07:00
parent 841f19e3f8
commit d437580500
306 changed files with 42669 additions and 27143 deletions
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Card, Table, Tag, Button, Space, Modal, Form, Input, message, Tabs, Badge, Descriptions, Divider, List, Upload } from 'antd';
import { CheckOutlined, CloseOutlined, EyeOutlined, EditOutlined, UndoOutlined, FileImageOutlined } from '@ant-design/icons';
import { CheckOutlined, CloseOutlined, EyeOutlined, EditOutlined, UndoOutlined, FileImageOutlined, DeleteOutlined } from '@ant-design/icons';
const { TextArea } = Input;
@@ -31,6 +31,7 @@ const ApprovalManagement: React.FC = () => {
const [editModalVisible, setEditModalVisible] = useState(false);
const [historyModalVisible, setHistoryModalVisible] = useState(false);
const [selectedRecord, setSelectedRecord] = useState<any>(null);
const [fullDetail, setFullDetail] = useState<any>(null);
const [approvalType, setApprovalType] = useState<'approve' | 'reject'>('approve');
const [form] = Form.useForm();
const [editForm] = Form.useForm();
@@ -64,7 +65,7 @@ const ApprovalManagement: React.FC = () => {
const historyData = [];
for (const type of types) {
const response = await fetch(`http://localhost:3005/api/${type}`);
const response = await fetch(`/api/${type}`);
const data = await response.json();
if (data.success && data.data) {
@@ -156,7 +157,7 @@ const ApprovalManagement: React.FC = () => {
// 获取项目列表
const fetchProjects = async () => {
try {
const response = await fetch('http://localhost:3005/api/projects');
const response = await fetch('/api/projects');
if (response.ok) {
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
@@ -174,29 +175,35 @@ const ApprovalManagement: React.FC = () => {
try {
console.log('开始获取待审批数据');
// 获取预支申请
const advancesRes = await fetch('http://localhost:3005/api/advances');
const advancesRes = await fetch('/api/advances');
console.log('Advances response status:', advancesRes.status);
const advancesData = await advancesRes.json();
console.log('Advances data:', advancesData);
// 获取报销申请
const reimbursementsRes = await fetch('http://localhost:3005/api/reimbursements');
const reimbursementsRes = await fetch('/api/reimbursements');
console.log('Reimbursements response status:', reimbursementsRes.status);
const reimbursementsData = await reimbursementsRes.json();
console.log('Reimbursements data:', reimbursementsData);
// 获取付款申请
const paymentsRes = await fetch('http://localhost:3005/api/payment-requests');
const paymentsRes = await fetch('/api/payment-requests');
console.log('Payments response status:', paymentsRes.status);
const paymentsData = await paymentsRes.json();
console.log('Payments data:', paymentsData);
// 获取核销申请
const verificationsRes = await fetch('http://localhost:3005/api/verifications');
const verificationsRes = await fetch('/api/verifications');
console.log('Verifications response status:', verificationsRes.status);
const verificationsData = await verificationsRes.json();
console.log('Verifications data:', verificationsData);
// 获取采购申请
const purchaseRes = await fetch('/api/purchase-requests');
console.log('Purchase requests response status:', purchaseRes.status);
const purchaseData = await purchaseRes.json();
console.log('Purchase requests data:', purchaseData);
// 合并数据
const allPendingData = [];
@@ -292,6 +299,29 @@ const ApprovalManagement: React.FC = () => {
});
}
// 添加采购申请
if (purchaseData.success && purchaseData.data) {
console.log('Purchase requests data length:', purchaseData.data.length);
purchaseData.data.forEach((item: any) => {
console.log('Purchase request item:', item);
if (item.status === 'pending') {
allPendingData.push({
key: `pur-${item.id}`,
id: item.id,
type: '采购申请',
code: item.request_code,
applicant: item.applicant,
amount: item.total_amount,
currency: item.currency,
date: item.request_date,
reason: item.brief_description || item.remark || '采购申请',
status: item.status,
rawData: item
});
}
});
}
console.log('Final pending data:', allPendingData);
setPendingData(allPendingData);
} catch (error) {
@@ -310,7 +340,7 @@ const ApprovalManagement: React.FC = () => {
// 获取类型标签
const getTypeTag = (type: string) => {
const colors: Record<string, string> = { '预支申请': 'blue', '报销申请': 'green', '付款申请': 'orange', '核销申请': 'purple' };
const colors: Record<string, string> = { '预支申请': 'blue', '报销申请': 'green', '付款申请': 'orange', '核销申请': 'purple', '采购申请': 'cyan' };
return <Tag color={colors[type] || 'default'}>{type}</Tag>;
};
@@ -334,10 +364,27 @@ const ApprovalManagement: React.FC = () => {
};
// 查看详情
const handleViewDetail = (record: any) => {
const handleViewDetail = async (record: any) => {
setSelectedRecord(record);
setApprovalType('approve');
form.resetFields();
// 获取完整详情
if (record.type === '采购申请') {
try {
const response = await fetch(`/api/purchase-requests/${record.id}`);
const data = await response.json();
if (data.success) {
setFullDetail(data.data);
}
} catch (error) {
console.error('获取采购申请详情失败:', error);
}
} else {
// 其他类型使用 rawData
setFullDetail(record.rawData);
}
setDetailModalVisible(true);
};
@@ -351,13 +398,15 @@ const ApprovalManagement: React.FC = () => {
const isReimbursement = selectedRecord.key.startsWith('reimb-');
const isPayment = selectedRecord.key.startsWith('pay-');
const isVerification = selectedRecord.key.startsWith('ver-');
const isPurchase = selectedRecord.key.startsWith('pur-');
const id = selectedRecord.id;
let url = '';
if (isAdvance) url = `http://localhost:3005/api/advances/${id}/approve`;
else if (isReimbursement) url = `http://localhost:3005/api/reimbursements/${id}/approve`;
else if (isPayment) url = `http://localhost:3005/api/payment-requests/${id}/approve`;
else if (isVerification) url = `http://localhost:3005/api/verifications/${id}/approve`;
if (isAdvance) url = `/api/advances/${id}/approve`;
else if (isReimbursement) url = `/api/reimbursements/${id}/approve`;
else if (isPayment) url = `/api/payment-requests/${id}/approve`;
else if (isVerification) url = `/api/verifications/${id}/approve`;
else if (isPurchase) url = `/api/purchase-requests/${id}/approve`;
// 发送API请求
const res = await fetch(url, {
@@ -391,13 +440,15 @@ const ApprovalManagement: React.FC = () => {
const isReimbursement = selectedRecord.key.startsWith('reimb-');
const isPayment = selectedRecord.key.startsWith('pay-');
const isVerification = selectedRecord.key.startsWith('ver-');
const isPurchase = selectedRecord.key.startsWith('pur-');
const id = selectedRecord.id;
let url = '';
if (isAdvance) url = `http://localhost:3005/api/advances/${id}/reject`;
else if (isReimbursement) url = `http://localhost:3005/api/reimbursements/${id}/reject`;
else if (isPayment) url = `http://localhost:3005/api/payment-requests/${id}/reject`;
else if (isVerification) url = `http://localhost:3005/api/verifications/${id}/reject`;
if (isAdvance) url = `/api/advances/${id}/reject`;
else if (isReimbursement) url = `/api/reimbursements/${id}/reject`;
else if (isPayment) url = `/api/payment-requests/${id}/reject`;
else if (isVerification) url = `/api/verifications/${id}/reject`;
else if (isPurchase) url = `/api/purchase-requests/${id}/reject`;
// 发送API请求
const res = await fetch(url, {
@@ -456,6 +507,7 @@ const ApprovalManagement: React.FC = () => {
if (key.startsWith('reimb-')) return 'reimbursements';
if (key.startsWith('pay-')) return 'payment-requests';
if (key.startsWith('ver-')) return 'verifications';
if (key.startsWith('pur-')) return 'purchase-requests';
return '';
};
@@ -588,12 +640,10 @@ const ApprovalManagement: React.FC = () => {
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (v: string) => getStatusTag(v) },
{ title: '编号', dataIndex: 'code', key: 'code', width: 140 },
{
title: '操作', key: 'action', width: 200,
title: '操作', key: 'action', width: 100,
render: (_: any, record: any) => (
<Space wrap>
<Space>
<Button size="small" type="primary" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}></Button>
<Button size="small" icon={<UndoOutlined />} onClick={() => handleWithdraw(record)}></Button>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button>
</Space>
)
}
@@ -618,14 +668,6 @@ const ApprovalManagement: React.FC = () => {
{ key: 'history', label: <span> <Badge count={approvalHistory.length} style={{ marginLeft: 8 }} /></span>, children: <Table columns={historyColumns} dataSource={approvalHistory} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 1400 }} /> },
];
// 获取完整的申请详情
const getFullDetail = () => {
if (!selectedRecord || !selectedRecord.rawData) return null;
return selectedRecord.rawData;
};
const fullDetail = getFullDetail();
return (
<div style={{ padding: 24 }}>
<div style={{ marginBottom: 24 }}>
@@ -731,7 +773,59 @@ const ApprovalManagement: React.FC = () => {
)}
</>
)}
{/* 采购申请特有字段 */}
{selectedRecord.type === '采购申请' && (
<>
<Descriptions.Item label="采购类型">
{fullDetail.purchase_type === 'project' ? '项目采购' : '库存采购'}
</Descriptions.Item>
{fullDetail.purchase_type === 'project' && fullDetail.project_id && (
<Descriptions.Item label="关联项目">{getProjectName(fullDetail.project_id)}</Descriptions.Item>
)}
<Descriptions.Item label="供应商">{fullDetail.supplier_name || '-'}</Descriptions.Item>
<Descriptions.Item label="支出分类">
{fullDetail.expense_category === 'material' ? '材料' :
fullDetail.expense_category === 'equipment' ? '设备' :
fullDetail.expense_category === 'pole' ? '电杆' : '其他'}
</Descriptions.Item>
<Descriptions.Item label="币种">{fullDetail.currency}</Descriptions.Item>
<Descriptions.Item label="申请日期">{fullDetail.request_date}</Descriptions.Item>
<Descriptions.Item label="事由" span={2}>{fullDetail.brief_description || '-'}</Descriptions.Item>
{fullDetail.remark && (
<Descriptions.Item label="备注" span={2}>{fullDetail.remark}</Descriptions.Item>
)}
</>
)}
</Descriptions>
{/* 采购申请商品明细 */}
{selectedRecord.type === '采购申请' && fullDetail.items && fullDetail.items.length > 0 && (
<>
<Divider></Divider>
<List
size="small"
bordered
dataSource={fullDetail.items}
renderItem={(item: any, index: number) => (
<List.Item>
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span><strong>{index + 1}. {item.product_name}</strong></span>
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>
{fullDetail.currency} {item.total_price?.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
</span>
</div>
<div style={{ fontSize: 13, color: '#666' }}>
: {item.specification || '-'} | : {item.unit || '-'} |
: {item.quantity} | : {fullDetail.currency} {item.unit_price?.toLocaleString('zh-CN', {minimumFractionDigits: 2})}
</div>
</div>
</List.Item>
)}
/>
</>
)}
{/* 明细清单 */}
{fullDetail.detail_items && fullDetail.detail_items.length > 0 && (