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
@@ -49,19 +49,24 @@ const ExecutionManagement: React.FC = () => {
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [selectedRecord, setSelectedRecord] = useState<any>(null);
const [fullDetail, setFullDetail] = useState<any>(null);
const [form] = Form.useForm();
const [editForm] = Form.useForm();
const [voucherFiles, setVoucherFiles] = useState<any[]>([]);
const [isRejecting, setIsRejecting] = useState(false);
// 待执行数据
const [pendingData, setPendingData] = useState([]);
// 已执行数据
const [executedData, setExecutedData] = useState([]);
// 已执行列表筛选状态
const [searchKeyword, setSearchKeyword] = useState('');
const [filterType, setFilterType] = useState<string | null>(null);
const [sortField, setSortField] = useState<string>('executeDate');
const [sortOrder, setSortOrder] = useState<'ascend' | 'descend'>('descend');
// 项目列表
const [projects, setProjects] = useState<any[]>([]);
@@ -70,7 +75,7 @@ const ExecutionManagement: React.FC = () => {
const fetchPendingData = async () => {
setLoading(true);
try {
const response = await fetch('http://localhost:3005/api/executions/pending');
const response = await fetch('/api/executions/pending');
if (response.ok) {
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
@@ -100,7 +105,7 @@ const ExecutionManagement: React.FC = () => {
useEffect(() => {
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)) {
@@ -120,7 +125,7 @@ const ExecutionManagement: React.FC = () => {
const fetchExecutedData = async () => {
setLoading(true);
try {
const response = await fetch('http://localhost:3005/api/executions/executed');
const response = await fetch('/api/executions/executed');
if (response.ok) {
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
@@ -163,7 +168,7 @@ const ExecutionManagement: 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>;
};
@@ -187,12 +192,31 @@ const ExecutionManagement: React.FC = () => {
// 查看详情
const handleViewDetail = (record: any) => {
const handleViewDetail = async (record: any) => {
setSelectedRecord(record);
form.resetFields();
form.setFieldsValue({ execute_date: dayjs(), execute_method: 'bank' });
setVoucherFiles([]);
setIsRejecting(false);
// 获取完整详情
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);
// 如果获取失败,使用record中的数据
setFullDetail(record);
}
} else {
// 其他类型使用record中的数据
setFullDetail(record);
}
setDetailModalVisible(true);
};
@@ -212,7 +236,7 @@ const ExecutionManagement: React.FC = () => {
setLoading(true);
// 调用执行API
const executeResponse = await fetch('http://localhost:3005/api/executions', {
const executeResponse = await fetch('/api/executions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -230,7 +254,7 @@ const ExecutionManagement: React.FC = () => {
// 刷新已执行数据
const fetchExecutedData = async () => {
try {
const response = await fetch('http://localhost:3005/api/executions/executed');
const response = await fetch('/api/executions/executed');
if (response.ok) {
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
@@ -275,12 +299,12 @@ const ExecutionManagement: React.FC = () => {
console.log('凭证文件URL列表:', voucherFileUrls);
// 调用执行API
const executeResponse = await fetch('http://localhost:3005/api/executions', {
const executeResponse = await fetch('/api/executions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apply_id: selectedRecord.id,
apply_type: selectedRecord.type === '预支申请' ? 'advance' : selectedRecord.type === '报销申请' ? 'reimbursement' : selectedRecord.type === '付款申请' ? 'payment' : 'verification',
apply_type: selectedRecord.type === '预支申请' ? 'advance' : selectedRecord.type === '报销申请' ? 'reimbursement' : selectedRecord.type === '付款申请' ? 'payment' : selectedRecord.type === '采购申请' ? 'purchase' : 'verification',
action: 'execute',
execute_method: isRefundVerification ? 'refund' : values.execute_method,
voucher_files: voucherFileUrls,
@@ -293,7 +317,7 @@ const ExecutionManagement: React.FC = () => {
// 刷新已执行数据
const fetchExecutedData = async () => {
try {
const response = await fetch('http://localhost:3005/api/executions/executed');
const response = await fetch('/api/executions/executed');
if (response.ok) {
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
@@ -333,7 +357,7 @@ const ExecutionManagement: React.FC = () => {
setLoading(true);
// 调用退回API
const rejectResponse = await fetch('http://localhost:3005/api/executions', {
const rejectResponse = await fetch('/api/executions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -514,6 +538,45 @@ const ExecutionManagement: React.FC = () => {
}
];
// 筛选和排序已执行数据
const getFilteredExecutedData = () => {
let data = [...executedData];
// 按事由搜索
if (searchKeyword) {
data = data.filter(item =>
(item.reason || '').toLowerCase().includes(searchKeyword.toLowerCase()) ||
(item.code || '').toLowerCase().includes(searchKeyword.toLowerCase()) ||
(item.applicant || '').toLowerCase().includes(searchKeyword.toLowerCase())
);
}
// 按类型筛选
if (filterType) {
data = data.filter(item => item.type === filterType);
}
// 排序
data.sort((a, b) => {
let aValue = a[sortField];
let bValue = b[sortField];
// 处理日期排序
if (sortField === 'executeDate') {
aValue = a.execute_date || a.executeDate || '';
bValue = b.execute_date || b.executeDate || '';
}
if (sortOrder === 'ascend') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
return data;
};
const executedColumns = [
{ title: '事由', dataIndex: 'reason', key: 'reason', ellipsis: true, render: (v: string, r: any) => <a onClick={() => handleViewDetail(r)}>{v || '-'}</a> },
{ title: '类型', dataIndex: 'type', key: 'type', width: 100, render: (v: string) => getTypeTag(v) },
@@ -524,30 +587,80 @@ const ExecutionManagement: React.FC = () => {
{r.currency !== 'CNY' && r.amount_cny && <div style={{ color: '#999', fontSize: 12 }}> ¥{r.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}</div>}
</>
) },
{ title: '执行日期', dataIndex: 'executeDate', key: 'executeDate', width: 100, render: (v: string) => v || '-' },
{ title: '执行日期', dataIndex: 'executeDate', key: 'executeDate', width: 100, sorter: true, render: (v: string) => v || '-' },
{ title: '执行方式', dataIndex: 'executeMethod', key: 'executeMethod', width: 100, render: (v: string) => v || '-' },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (v: string) => getStatusTag(v) },
{ title: '编号', dataIndex: 'code', key: 'code', width: 140 },
];
// 已执行列表的筛选和排序控件
const ExecutedListControls = () => (
<div style={{ marginBottom: 16, display: 'flex', gap: 16, flexWrap: 'wrap' }}>
<Input.Search
placeholder="搜索事由、编号或申请人"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onSearch={(value) => setSearchKeyword(value)}
style={{ width: 250 }}
allowClear
/>
<Select
placeholder="筛选类型"
value={filterType}
onChange={(value) => setFilterType(value)}
style={{ width: 150 }}
allowClear
>
<Select.Option value="预支申请"></Select.Option>
<Select.Option value="报销申请"></Select.Option>
<Select.Option value="付款申请"></Select.Option>
<Select.Option value="核销申请"></Select.Option>
<Select.Option value="采购申请"></Select.Option>
</Select>
<Select
placeholder="排序方式"
value={`${sortField}_${sortOrder}`}
onChange={(value) => {
const [field, order] = (value as string).split('_');
setSortField(field);
setSortOrder(order as 'ascend' | 'descend');
}}
style={{ width: 180 }}
>
<Select.Option value="executeDate_descend"></Select.Option>
<Select.Option value="executeDate_ascend"></Select.Option>
<Select.Option value="amount_descend"></Select.Option>
<Select.Option value="amount_ascend"></Select.Option>
</Select>
</div>
);
const tabItems = [
{ key: 'pending', label: <span> <Badge count={pendingData.length} style={{ marginLeft: 8 }} /></span>, children: <Table columns={pendingColumns} dataSource={pendingData} loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 1300 }} /> },
{ key: 'executed', label: '已执行', children: <Table columns={executedColumns} dataSource={executedData} loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 1400 }} /> },
{ key: 'executed', label: '已执行', children: (
<>
<ExecutedListControls />
<Table
columns={executedColumns}
dataSource={getFilteredExecutedData()}
loading={loading}
pagination={{ pageSize: 10 }}
scroll={{ x: 1400 }}
onChange={(pagination, filters, sorter: any) => {
if (sorter.field) {
setSortField(sorter.field);
setSortOrder(sorter.order || 'descend');
}
}}
/>
</>
)},
];
// 获取完整的申请详情
const getFullDetail = () => {
if (!selectedRecord || !selectedRecord.rawData) return selectedRecord;
return selectedRecord.rawData;
};
const fullDetail = getFullDetail();
// 上传配置
const uploadProps = {
name: 'file',
action: 'http://localhost:3005/api/upload/single',
action: '/api/upload/single',
headers: {
authorization: 'authorization-text',
},
@@ -672,7 +785,80 @@ const ExecutionManagement: 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.supplier_payment_infos && fullDetail.supplier_payment_infos.length > 0 && (
<>
<Divider></Divider>
<Descriptions bordered column={2} size="small">
{fullDetail.supplier_payment_infos.filter((p: any) => p.is_primary).map((payment: any, index: number) => (
<React.Fragment key={index}>
<Descriptions.Item label="收款户名">{payment.account_name || '-'}</Descriptions.Item>
<Descriptions.Item label="银行账号">{payment.bank_account || '-'}</Descriptions.Item>
<Descriptions.Item label="开户银行">{payment.bank_name || '-'}</Descriptions.Item>
{payment.qr_code && (
<Descriptions.Item label="收款码">
<img src={payment.qr_code} alt="收款码" style={{ width: 100, height: 100, objectFit: 'contain' }} />
</Descriptions.Item>
)}
</React.Fragment>
))}
</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 && (