feat: 完善采购申请流程 - 添加审批、执行、列表筛选排序功能
This commit is contained in:
@@ -10,9 +10,9 @@ export const API_CONFIG = {
|
||||
// API端点
|
||||
export const API_ENDPOINTS = {
|
||||
auth: {
|
||||
login: '/v1/auth/login',
|
||||
logout: '/v1/auth/logout',
|
||||
me: '/v1/auth/me',
|
||||
login: '/auth/login',
|
||||
logout: '/auth/logout',
|
||||
me: '/auth/me',
|
||||
},
|
||||
products: '/products',
|
||||
customers: '/customers',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import {
|
||||
Card, Descriptions, Tag, Spin, Empty, Row, Col, Statistic, Table, Button, Divider, Typography, Badge
|
||||
} from 'antd'
|
||||
import {
|
||||
ArrowLeftOutlined, ShopOutlined, FileTextOutlined, DollarOutlined, UserOutlined, PhoneOutlined
|
||||
ArrowLeftOutlined, ShopOutlined, FileTextOutlined, DollarOutlined, UserOutlined, PhoneOutlined, BankOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
@@ -16,6 +16,15 @@ interface Contact {
|
||||
is_primary?: boolean
|
||||
}
|
||||
|
||||
interface PaymentInfo {
|
||||
id: number
|
||||
account_name: string
|
||||
bank_account: string
|
||||
bank_name: string
|
||||
qr_code?: string
|
||||
is_primary: boolean
|
||||
}
|
||||
|
||||
interface Supplier {
|
||||
id: number
|
||||
code: string
|
||||
@@ -23,6 +32,7 @@ interface Supplier {
|
||||
supply_category: string
|
||||
country: string
|
||||
contacts: Contact[]
|
||||
payment_infos: PaymentInfo[]
|
||||
remark: string
|
||||
total_purchase_amount: number
|
||||
total_paid: number
|
||||
@@ -142,7 +152,36 @@ const SupplierDetail: React.FC = () => {
|
||||
{(supplier.contacts || []).length === 0 && <Empty description="暂无联系人" image={Empty.PRESENTED_IMAGE_SIMPLE} />}
|
||||
</Card>
|
||||
|
||||
{/* ========== 卡片2:关联项目 ========== */}
|
||||
{/* ========== 卡片2:收款信息 ========== */}
|
||||
<Card title={<><BankOutlined /> 收款信息</>} style={{ marginBottom: 24, borderRadius: 8 }} headStyle={{ background: '#f5f5f5', fontWeight: 600 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{(supplier.payment_infos || []).map((payment, i) => (
|
||||
<Col key={i} xs={24} sm={12} lg={8}>
|
||||
<Card size="small" style={{ borderLeft: payment.is_primary ? '3px solid #1890ff' : '3px solid #d9d9d9', background: payment.is_primary ? '#f0f5ff' : '#fff' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<Text strong>{payment.bank_name || '未命名'}</Text>
|
||||
{payment.is_primary && <Tag color="blue" size="small">主要收款账户</Tag>}
|
||||
</div>
|
||||
<div style={{ color: '#666', fontSize: 13 }}>
|
||||
{payment.account_name && <div>户名:{payment.account_name}</div>}
|
||||
{payment.bank_account && <div>账号:{payment.bank_account}</div>}
|
||||
{payment.qr_code && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary">收款码:</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<img src={payment.qr_code} alt="收款码" style={{ maxWidth: '100px', maxHeight: '100px' }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
{(supplier.payment_infos || []).length === 0 && <Empty description="暂无收款信息" image={Empty.PRESENTED_IMAGE_SIMPLE} />}
|
||||
</Card>
|
||||
|
||||
{/* ========== 卡片3:关联项目 ========== */}
|
||||
<Card title={<><FileTextOutlined /> 关联项目</>} style={{ marginBottom: 24, borderRadius: 8 }} headStyle={{ background: '#f5f5f5', fontWeight: 600 }}>
|
||||
{projects.length > 0 ? (
|
||||
<Table columns={projectColumns} dataSource={projects} rowKey="id" size="small" pagination={false} bordered />
|
||||
|
||||
@@ -348,7 +348,18 @@ const SupplierPage: React.FC = () => {
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item {...restField} name={[name, 'qr_code']} label="收款码" style={{ marginBottom: 0 }}>
|
||||
<FileUpload maxCount={1} accept="image/*" />
|
||||
<FileUpload
|
||||
maxCount={1}
|
||||
accept="image/*"
|
||||
value={form.getFieldValue([name, 'qr_code']) ? [form.getFieldValue([name, 'qr_code'])] : []}
|
||||
onChange={(urls) => {
|
||||
form.setFieldsValue({
|
||||
payment_infos: form.getFieldValue('payment_infos').map((info: any, i: number) => {
|
||||
return i === Number(name) ? { ...info, qr_code: urls[0] || '' } : info
|
||||
})
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{fields.length > 0 && (
|
||||
<Button type="link" danger onClick={() => remove(name)} style={{ marginTop: 8 }}>删除此收款信息</Button>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -80,11 +80,11 @@ export default defineConfig({
|
||||
})
|
||||
],
|
||||
server: {
|
||||
port: 3002,
|
||||
port: 3006,
|
||||
host: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3005',
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user